feat: add structured placement properties

This commit is contained in:
2026-08-03 00:00:42 -04:00
parent a8f902ceb5
commit 293cff6c36
10 changed files with 95 additions and 10 deletions

View File

@@ -54,7 +54,7 @@ import {
ZoomOut,
} from 'lucide-react'
import { menuDefinitions, pinnedWorkbenches, workbenchDefinitions, type MenuName, type WorkbenchId } from './freecadManifest'
import { createMockFacade, type BitBybitViewportAdapter, type BitBybitWebCadFacade, type DiagnosticTreeNode, type DocumentSnapshot, type FcstdInspection, type ModelTreeItem, type ObjectPropertySnapshot, type ProjectSummary, type PropertyValue, type ShapeHandle } from './facade'
import { createMockFacade, type BitBybitViewportAdapter, type BitBybitWebCadFacade, type DiagnosticTreeNode, type DocumentSnapshot, type FcstdInspection, type ModelTreeItem, type ObjectPropertySnapshot, type PlacementValue, type ProjectSummary, type PropertyValue, type ShapeHandle } from './facade'
type Page = 'start' | 'projects' | 'workspace' | 'import' | 'export' | 'settings' | 'help' | 'diagnostics' | 'sync'
type Workbench = WorkbenchId
@@ -451,9 +451,23 @@ function PropertyEditor({ facade, objectId, property, showNotice }: { facade: Bi
const ids = [...new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))]
editor = <select className="property-control property-link-control" value={String(property.value ?? '')} aria-label={property.label} onChange={(event) => commit(event.target.value || null)}><option value="">None</option>{ids.filter((id) => id !== objectId).map((id) => <option value={id} key={id}>{document.tree.find((item) => item.id === id)?.label ?? id}</option>)}</select>
} else if (property.type === 'App::PropertyLinkSub') {
const ref = property.value && typeof property.value === 'object' ? property.value : null
const ref = property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'schemaVersion' in property.value ? property.value : null
editor = ref ? <div className="property-link-sub"><span title={ref.persistentId}>{ref.objectId} / {ref.kind} · {ref.status}</span><IconButton icon={X} label="Clear subshape reference" onClick={() => commit(null)} /></div> : <span className="property-readonly">None</span>
} else if (property.type === 'App::PropertyColor') editor = <label className="property-color"><input type="color" value={String(property.value)} aria-label={property.label} onChange={(event) => commit(event.target.value)} /><span>{String(property.value).toUpperCase()}</span></label>
} else if (property.type === 'App::PropertyPlacement') {
const placement = property.value as PlacementValue
const updatePlacement = (path: 'position' | 'axis' | 'angle', key: 'x' | 'y' | 'z' | null, next: number) => {
const value: PlacementValue = { position: { ...placement.position }, rotation: { axis: { ...placement.rotation.axis }, angle: placement.rotation.angle } }
if (path === 'position' && key) value.position[key] = next
else if (path === 'axis' && key) value.rotation.axis[key] = next
else value.rotation.angle = next
return commit(value)
}
editor = <details className="property-placement"><summary>{placement.position.x}, {placement.position.y}, {placement.position.z}</summary><div className="property-placement-grid"><span>Position</span>{(['x', 'y', 'z'] as const).map((key) => <input key={`p-${key}`} type="number" aria-label={`${property.label} position ${key}`} defaultValue={placement.position[key]} onBlur={(event) => updatePlacement('position', key, Number(event.target.value))} />)}<span>Axis</span>{(['x', 'y', 'z'] as const).map((key) => <input key={`a-${key}`} type="number" aria-label={`${property.label} axis ${key}`} defaultValue={placement.rotation.axis[key]} onBlur={(event) => updatePlacement('axis', key, Number(event.target.value))} />)}<span>Angle</span><input type="number" aria-label={`${property.label} angle`} defaultValue={placement.rotation.angle} onBlur={(event) => updatePlacement('angle', null, Number(event.target.value))} /><span /><span /></div></details>
} else if (property.type === 'App::PropertyVector') {
const vector = property.value as { x: number; y: number; z: number }
editor = <div className="property-vector">{(['x', 'y', 'z'] as const).map((key) => <input key={key} type="number" aria-label={`${property.label} ${key}`} defaultValue={vector[key]} onBlur={(event) => commit({ ...vector, [key]: Number(event.target.value) })} />)}</div>
} else if (property.type === 'App::PropertyLinkList' || property.type === 'App::PropertyStringList') editor = <input className="property-control" defaultValue={(property.value as string[]).join(', ')} aria-label={property.label} onBlur={(event) => commit(event.target.value.split(',').map((entry) => entry.trim()).filter(Boolean))} />
else if (property.type === 'App::PropertyColor') editor = <label className="property-color"><input type="color" value={String(property.value)} aria-label={property.label} onChange={(event) => commit(event.target.value)} /><span>{String(property.value).toUpperCase()}</span></label>
else if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger') editor = <label className="property-number"><input className="property-control" type="number" defaultValue={Number(property.value)} min={property.name === 'Occurrences' ? 2 : property.type === 'App::PropertyLength' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyPercent' ? 0 : undefined} max={property.name === 'Occurrences' ? 100 : property.type === 'App::PropertyAngle' ? 360 : property.type === 'App::PropertyPercent' ? 100 : undefined} step={property.type === 'App::PropertyLength' ? 0.1 : 1} aria-label={property.label} onBlur={(event) => { if (!commit(Number(event.target.value))) event.target.value = String(property.value) }} onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }} /><span>{property.unit}</span></label>
else editor = <input className="property-control" defaultValue={String(property.value ?? '')} aria-label={property.label} onBlur={(event) => { if (!commit(event.target.value)) event.target.value = String(property.value ?? '') }} onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }} />
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>}</>