P3/P4: execute OCCT features and export shapes
This commit is contained in:
41
src/App.tsx
41
src/App.tsx
@@ -289,7 +289,7 @@ function Workspace({ workbench, setWorkbench, leftTab, setLeftTab, rightTab, set
|
||||
{leftTab === 'model' ? <div className="combo-model"><ModelTree document={document} selectedObject={selectedObject} setSelectedObject={setSelectedObject} showNotice={showNotice} /><div className="combo-property"><div className="property-heading"><div><span className="eyebrow">Property view</span><h2>{document.tree.find((item) => item.id === selectedObject)?.label || 'No selection'}</h2></div><IconButton icon={MoreHorizontal} label="More object actions" /></div><div className="panel-tabs property-tabs"><button className={rightTab === 'data' ? 'is-active' : ''} onClick={() => setRightTab('data')}>Data</button><button className={rightTab === 'view' ? 'is-active' : ''} onClick={() => setRightTab('view')}>View</button></div><PropertyPanel key={rightTab} facade={facade} objectId={selectedObject} scope={rightTab} showNotice={showNotice} /></div></div> : <div className="model-task-summary"><span className="eyebrow">Combo View task tab</span><p>Use the right Task panel for command parameters. This tab stays available for selection and dependency context.</p><button className="button button-outline" onClick={() => showNotice('Selection filter enabled')}><Search size={14} />Selection filter</button></div>}
|
||||
</aside>
|
||||
<section className="viewport-region">
|
||||
<Viewport selectedObject={selectedObject} setSelectedObject={setSelectedObject} workbench={workbench} facade={facade} showNotice={showNotice} />
|
||||
<Viewport selectedObject={selectedObject} setSelectedObject={setSelectedObject} workbench={workbench} facade={facade} documentVersion={document.version} recomputeGeneration={document.recompute?.generation ?? 0} showNotice={showNotice} />
|
||||
<div className="viewport-bottom-left"><div className="view-chip"><Circle size={8} fill="currentColor" /> Perspective</div><div className="view-chip">Grid 10 mm</div></div>
|
||||
<div className="viewport-bottom-right"><div className="axis-widget"><span className="axis-x">X</span><span className="axis-y">Y</span><span className="axis-z">Z</span><div className="axis-origin" /></div></div>
|
||||
</section>
|
||||
@@ -382,7 +382,7 @@ function PropertyEditor({ facade, objectId, property, showNotice }: { facade: Bi
|
||||
return <><div className="property-row"><span className="property-label">{property.label}</span><div className="property-editor">{editor}</div></div>{property.expression && <div className="property-expression"><Code2 size={12} /><span>{property.expression}</span></div>}</>
|
||||
}
|
||||
|
||||
function Viewport({ selectedObject, setSelectedObject, workbench, facade, showNotice }: { selectedObject: string; setSelectedObject: (id: string) => void; workbench: Workbench; facade: BitBybitWebCadFacade; showNotice: (message: string) => void }) {
|
||||
function Viewport({ selectedObject, setSelectedObject, workbench, facade, documentVersion, recomputeGeneration, showNotice }: { selectedObject: string; setSelectedObject: (id: string) => void; workbench: Workbench; facade: BitBybitWebCadFacade; documentVersion: number; recomputeGeneration: number; showNotice: (message: string) => void }) {
|
||||
const hostRef = useRef<HTMLDivElement>(null)
|
||||
const adapterRef = useRef<BitBybitViewportAdapter | null>(null)
|
||||
useEffect(() => {
|
||||
@@ -392,6 +392,7 @@ function Viewport({ selectedObject, setSelectedObject, workbench, facade, showNo
|
||||
adapterRef.current = adapter
|
||||
let cancelled = false
|
||||
const shapes: ShapeHandle[] = []
|
||||
let ownsShapes = false
|
||||
try {
|
||||
adapter.mount(host)
|
||||
} catch (error) {
|
||||
@@ -404,6 +405,14 @@ function Viewport({ selectedObject, setSelectedObject, workbench, facade, showNo
|
||||
const capabilities = await facade.geometry.initialize()
|
||||
if (capabilities.status !== 'ready') throw new Error(capabilities.reason || 'OCCT geometry runtime unavailable')
|
||||
const document = facade.app.document.getActive()
|
||||
const featureIds = [selectedObject, 'fillet', 'pocket', 'pad'].filter(Boolean)
|
||||
const storedShape = featureIds.map((objectId) => facade.geometry.getObjectShape(objectId)).find((shape): shape is ShapeHandle => Boolean(shape))
|
||||
if (storedShape) {
|
||||
const mesh = await facade.geometry.mesh(storedShape, 0.05)
|
||||
if (!cancelled) adapter.setMesh(mesh)
|
||||
return
|
||||
}
|
||||
ownsShapes = true
|
||||
const profile = { outer: [[-1.35, -0.7, 0], [1.35, -0.7, 0], [1.35, 0.7, 0], [-1.35, 0.7, 0]] as [number, number, number][] }
|
||||
const pad = await facade.geometry.pad({ profile, length: 1.6, direction: [0, 0, 1], documentId: document.id, documentVersion: document.version })
|
||||
shapes.push(pad)
|
||||
@@ -422,11 +431,11 @@ function Viewport({ selectedObject, setSelectedObject, workbench, facade, showNo
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
void Promise.all(shapes.map((entry) => facade.geometry.release(entry)))
|
||||
if (ownsShapes) void Promise.all(shapes.map((entry) => facade.geometry.release(entry)))
|
||||
adapter.dispose()
|
||||
adapterRef.current = null
|
||||
}
|
||||
}, [facade, showNotice])
|
||||
}, [facade, showNotice, selectedObject, documentVersion, recomputeGeneration])
|
||||
|
||||
useEffect(() => adapterRef.current?.setSelection(selectedObject), [selectedObject])
|
||||
return <div className="viewport"><div className="viewport-header"><div className="viewport-title"><span className="eyebrow">{workbench}</span><strong>Body / Fillet</strong></div><div className="viewport-actions"><IconButton icon={ZoomOut} label="Zoom out" /><IconButton icon={ZoomIn} label="Zoom in" /><IconButton icon={Rotate3D} label="Orbit view" active /></div></div><div className="viewport-grid" onClick={() => setSelectedObject('')}><div className="three-viewport-host" ref={hostRef} aria-label="Three.js viewport" /></div><div className="viewport-legend"><span><span className="legend-swatch selected" />Selected</span><span><span className="legend-swatch edge" />Edges</span><span><span className="legend-swatch datum" />Datum</span></div></div>
|
||||
@@ -474,6 +483,8 @@ function ProjectsPage({ onNavigate, onOpenWorkspace, showNotice }: { onNavigate:
|
||||
function FileFlowPage({ mode, onNavigate, showNotice, facade }: { mode: 'import' | 'export'; onNavigate: (page: Page) => void; showNotice: (message: string) => void; facade: BitBybitWebCadFacade }) {
|
||||
const isImport = mode === 'import'
|
||||
const [fcstdReport, setFcstdReport] = useState<FcstdInspection | null>(null)
|
||||
const [selectedFormat, setSelectedFormat] = useState<'STEP' | 'STL' | 'GLB / GLTF' | 'Web CAD package'>('STEP')
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const inspectFile = async (file: File) => {
|
||||
if (!file.name.toLowerCase().endsWith('.fcstd')) { showNotice('Only FCStd inspection is connected to this import boundary'); return }
|
||||
@@ -483,7 +494,27 @@ function FileFlowPage({ mode, onNavigate, showNotice, facade }: { mode: 'import'
|
||||
showNotice(`FCStd inspected: ${report.compatibility.level}`)
|
||||
} catch (error) { setFcstdReport(null); showNotice(`FCStd rejected: ${error instanceof Error ? error.message : String(error)}`) }
|
||||
}
|
||||
return <div className="flow-page"><PageHeader eyebrow={isImport ? 'Import model' : 'Export project'} title={isImport ? 'Bring a model into your workspace.' : 'Export a clean deliverable.'} description={isImport ? 'Review format, units and compatibility before the document is created.' : 'Choose an exchange format and keep the native project available for future edits.'} onBack={() => onNavigate('start')} /><div className="flow-layout"><div className="flow-steps"><FlowStep index="01" title="Choose file" active={!isImport} complete={isImport} /><FlowStep index="02" title="Review mapping" active={isImport} complete={false} /><FlowStep index="03" title="Confirm" active={false} complete={false} /></div><section className="flow-card panel-surface"><div className="flow-card-header"><div><span className="section-kicker">Step 01 / 03</span><h2>{isImport ? 'Select a CAD file' : 'Select an export format'}</h2></div><Badge tone="cyan">Local only</Badge></div>{isImport ? <><div className="drop-zone"><div className="drop-icon"><Upload size={20} /></div><strong>Drop a model here</strong><span>or browse from your device</span><input ref={fileInputRef} type="file" accept=".FCStd,.fcstd" hidden onChange={(event) => { const file = event.target.files?.[0]; if (file) void inspectFile(file); event.currentTarget.value = '' }} /><button className="button button-outline" onClick={() => fileInputRef.current?.click()}><FolderOpen size={15} />Browse files</button><small>STEP · IGES · STL · OBJ · PLY · FCStd</small></div><div className="flow-note"><AlertTriangle size={15} /><span>FCStd is currently a read-only metadata inspection boundary. Python/macros are isolated and never executed.</span></div>{fcstdReport && <div className="flow-note"><CheckCircle2 size={15} className="icon-green" /><span>{fcstdReport.label} · {fcstdReport.objects.length} objects · {fcstdReport.compatibility.level}; {fcstdReport.compatibility.warnings.join(' ') || 'No compatibility warnings.'}</span></div>}</> : <div className="format-grid">{['STEP', 'IGES', 'STL', 'GLB / GLTF', 'Web CAD package'].map((format, index) => <button key={format} className={`format-card ${index === 0 ? 'is-selected' : ''}`} onClick={() => showNotice(`${format} selected`)}><span className="format-icon">{index === 4 ? <FileBox size={18} /> : <FileText size={18} />}</span><strong>{format}</strong><small>{index === 0 ? 'Best for editable solids' : index === 2 ? 'Mesh export' : 'Exchange format'}</small>{index === 0 && <Check size={15} className="format-check" />}</button>)}</div>}<div className="flow-footer"><button className="button button-quiet" onClick={() => onNavigate('start')}>Cancel</button><button className="button button-primary" onClick={() => showNotice(isImport ? (fcstdReport ? 'Metadata review complete; import mapping remains disabled' : 'Choose a file to review') : 'Export queued')}><span>{isImport ? 'Continue' : 'Export'}</span><ArrowRight size={15} /></button></div></section></div></div>
|
||||
const exportModel = async () => {
|
||||
if (selectedFormat !== 'STEP' && selectedFormat !== 'STL') { showNotice(`${selectedFormat} export is not implemented yet`); return }
|
||||
const objectIds = ['fillet', 'chamfer', 'pocket', 'pad']
|
||||
const shape = objectIds.map((objectId) => facade.geometry.getObjectShape(objectId)).find((candidate): candidate is ShapeHandle => Boolean(candidate))
|
||||
if (!shape) { showNotice('Recompute a valid Part Design feature before exporting'); return }
|
||||
setExporting(true)
|
||||
try {
|
||||
const result = selectedFormat === 'STEP' ? await facade.geometry.exportStep(shape, `${facade.app.document.getActive().label}.step`) : await facade.geometry.exportStl(shape, `${facade.app.document.getActive().label}.stl`)
|
||||
const blob = new Blob([result.text], { type: result.mediaType })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = result.fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
showNotice(`${result.format.toUpperCase()} export downloaded`)
|
||||
} catch (error) {
|
||||
showNotice(`Export failed: ${error instanceof Error ? error.message : String(error)}`)
|
||||
} finally { setExporting(false) }
|
||||
}
|
||||
return <div className="flow-page"><PageHeader eyebrow={isImport ? 'Import model' : 'Export project'} title={isImport ? 'Bring a model into your workspace.' : 'Export a clean deliverable.'} description={isImport ? 'Review format, units and compatibility before the document is created.' : 'Choose an exchange format and keep the native project available for future edits.'} onBack={() => onNavigate('start')} /><div className="flow-layout"><div className="flow-steps"><FlowStep index="01" title="Choose file" active={!isImport} complete={isImport} /><FlowStep index="02" title="Review mapping" active={isImport} complete={false} /><FlowStep index="03" title="Confirm" active={false} complete={false} /></div><section className="flow-card panel-surface"><div className="flow-card-header"><div><span className="section-kicker">Step 01 / 03</span><h2>{isImport ? 'Select a CAD file' : 'Select an export format'}</h2></div><Badge tone="cyan">Local only</Badge></div>{isImport ? <><div className="drop-zone"><div className="drop-icon"><Upload size={20} /></div><strong>Drop a model here</strong><span>or browse from your device</span><input ref={fileInputRef} type="file" accept=".FCStd,.fcstd" hidden onChange={(event) => { const file = event.target.files?.[0]; if (file) void inspectFile(file); event.currentTarget.value = '' }} /><button className="button button-outline" onClick={() => fileInputRef.current?.click()}><FolderOpen size={15} />Browse files</button><small>STEP · IGES · STL · OBJ · PLY · FCStd</small></div><div className="flow-note"><AlertTriangle size={15} /><span>FCStd is currently a read-only metadata inspection boundary. Python/macros are isolated and never executed.</span></div>{fcstdReport && <div className="flow-note"><CheckCircle2 size={15} className="icon-green" /><span>{fcstdReport.label} · {fcstdReport.objects.length} objects · {fcstdReport.compatibility.level}; {fcstdReport.compatibility.warnings.join(' ') || 'No compatibility warnings.'}</span></div>}</> : <div className="format-grid">{['STEP', 'IGES', 'STL', 'GLB / GLTF', 'Web CAD package'].map((format, index) => <button key={format} className={`format-card ${selectedFormat === format ? 'is-selected' : ''}`} disabled={format === 'IGES' || format === 'GLB / GLTF' || format === 'Web CAD package'} onClick={() => { setSelectedFormat(format as typeof selectedFormat); showNotice(`${format} selected`) }}><span className="format-icon">{index === 4 ? <FileBox size={18} /> : <FileText size={18} />}</span><strong>{format}</strong><small>{index === 0 ? 'Best for editable solids' : index === 2 ? 'Mesh export' : 'Not implemented'}</small>{selectedFormat === format && <Check size={15} className="format-check" />}</button>)}</div>}<div className="flow-footer"><button className="button button-quiet" onClick={() => onNavigate('start')}>Cancel</button><button className="button button-primary" disabled={isImport ? false : exporting} onClick={() => { if (isImport) showNotice(fcstdReport ? 'Metadata review complete; import mapping remains disabled' : 'Choose a file to review'); else void exportModel() }}><span>{isImport ? 'Continue' : exporting ? 'Exporting' : `Export ${selectedFormat}`}</span>{exporting ? <RefreshCw size={15} /> : <ArrowRight size={15} />}</button></div></section></div></div>
|
||||
}
|
||||
|
||||
function FlowStep({ index, title, active, complete }: { index: string; title: string; active: boolean; complete: boolean }) {
|
||||
|
||||
Reference in New Issue
Block a user