P6-02: project document tree and properties

This commit is contained in:
2026-08-02 03:34:39 -04:00
parent 91e6414d2d
commit bdaea21574

View File

@@ -276,7 +276,7 @@ function Workspace({ workbench, setWorkbench, leftTab, setLeftTab, rightTab, set
<div className="workspace-content">
<aside className="combo-panel left-panel">
<div className="panel-tabs"><button className={leftTab === 'model' ? 'is-active' : ''} onClick={() => setLeftTab('model')}><ListTree size={14} />Model</button><button className={leftTab === 'tasks' ? 'is-active' : ''} onClick={() => setLeftTab('tasks')}><SlidersHorizontal size={14} />Tasks <span className="tab-count">1</span></button></div>
{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>{selectedObject === 'pad' ? 'Pad' : selectedObject === 'pocket' ? 'Pocket' : 'Body'}</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>{rightTab === 'data' ? <DataProperties selectedObject={selectedObject} showNotice={showNotice} /> : <ViewProperties 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>}
{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>{rightTab === 'data' ? <DataProperties selectedObject={selectedObject} selectedItem={document.tree.find((item) => item.id === selectedObject)} showNotice={showNotice} /> : <ViewProperties 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} />
@@ -305,16 +305,18 @@ function FunctionRail({ workbench, facade, showNotice }: { workbench: Workbench;
function ModelTree({ document, selectedObject, setSelectedObject, showNotice }: { document: DocumentSnapshot; selectedObject: string; setSelectedObject: (id: string) => void; showNotice: (message: string) => void }) {
const [expanded, setExpanded] = useState<Record<string, boolean>>({ origin: false, body: true, reference: false })
const itemsById = new Map(document.tree.map((item) => [item.id, item]))
const childIds = new Set(document.tree.flatMap((item) => item.children || []))
return <div className="model-tree">
<div className="tree-toolbar"><div className="tree-search"><Search size={14} /><input placeholder="Filter objects" /></div><IconButton icon={MoreHorizontal} label="Model tree actions" /></div>
<div className="tree-root"><div className="tree-document-row"><ChevronDown size={14} /><FileBox size={15} className="icon-cyan" /><span>{document.label}</span><span className="tree-version">v{document.version}</span></div>
{document.tree.map((item) => <TreeItem key={item.id} item={item} level={1} expanded={expanded[item.id]} onToggle={() => setExpanded((current) => ({ ...current, [item.id]: !current[item.id] }))} selectedObject={selectedObject} setSelectedObject={setSelectedObject} showNotice={showNotice} />)}
{document.tree.filter((item) => !childIds.has(item.id)).map((item) => <TreeItem key={item.id} item={item} level={1} expanded={expanded[item.id]} onToggle={() => setExpanded((current) => ({ ...current, [item.id]: !current[item.id] }))} itemsById={itemsById} selectedObject={selectedObject} setSelectedObject={setSelectedObject} showNotice={showNotice} />)}
</div>
<div className="tree-footer"><span><span className="legend-dot valid" />Valid</span><span><span className="legend-dot warning" />Needs review</span></div>
</div>
}
function TreeItem({ item, level, expanded, onToggle, selectedObject, setSelectedObject, showNotice }: { item: ModelTreeItem; level: number; expanded?: boolean; onToggle: () => void; selectedObject: string; setSelectedObject: (id: string) => void; showNotice: (message: string) => void }) {
function TreeItem({ item, level, expanded, onToggle, itemsById, selectedObject, setSelectedObject, showNotice }: { item: ModelTreeItem; level: number; expanded?: boolean; onToggle: () => void; itemsById: Map<string, ModelTreeItem>; selectedObject: string; setSelectedObject: (id: string) => void; showNotice: (message: string) => void }) {
const hasChildren = item.children && item.children.length > 0
const itemIcon = item.type === 'body' ? Box : item.type === 'sketch' ? CircleDot : item.type === 'feature' ? SquareStack : Layers3
const ItemIcon = itemIcon
@@ -326,7 +328,7 @@ function TreeItem({ item, level, expanded, onToggle, selectedObject, setSelected
{item.state === 'warning' && <AlertTriangle size={13} className="icon-amber" />}
{item.state === 'active' && <span className="active-marker" />}
</button>
{expanded && hasChildren && <div className="tree-children">{item.children?.map((child) => <div className="tree-row tree-child" key={child}><span className="tree-spacer" /><Circle size={8} className="icon-muted" /><span className="tree-label">{child}</span></div>)}</div>}
{expanded && hasChildren && <div className="tree-children">{item.children?.map((childId) => { const child = itemsById.get(childId); const ChildIcon = child?.type === 'sketch' ? CircleDot : child?.type === 'feature' ? SquareStack : Circle; return child ? <button className={`tree-row tree-child ${selectedObject === child.id ? 'is-selected' : ''}`} key={child.id} onClick={() => { setSelectedObject(child.id); if (child.state === 'warning') showNotice('This feature has a reference warning') }} onDoubleClick={() => showNotice(`Editing ${child.label}`)}><span className="tree-spacer" /><ChildIcon size={child.type === 'feature' ? 15 : 13} className={child.state === 'warning' ? 'icon-amber' : 'icon-muted'} /><span className="tree-label">{child.label}</span>{child.state === 'warning' && <AlertTriangle size={13} className="icon-amber" />}</button> : <div className="tree-row tree-child" key={childId}><span className="tree-spacer" /><Circle size={8} className="icon-muted" /><span className="tree-label">{childId}</span></div> })}</div>}
</>
}
@@ -336,9 +338,13 @@ function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; fa
return <div className="task-panel"><div className="task-actions-top"><button className="button button-primary" onClick={() => { facade.task.apply(); showNotice('Task accepted') }}>OK</button><button className="button button-outline" onClick={() => { facade.task.update({ preview: true }); showNotice('Preview applied') }}>Apply</button><button className="button button-quiet" onClick={() => { facade.task.cancel(); showNotice('Task cancelled') }}>Cancel</button></div><div className="task-header"><div className="task-icon"><Pencil size={16} /></div><div><span className="eyebrow">Active command</span><h2>{isSketch ? 'Edit Sketch' : definition.taskTitle}</h2></div><Badge tone="cyan">Preview</Badge></div><div className="task-body"><div className="task-step"><span className="step-index">1</span><div><strong>{isSketch ? 'Geometry and constraints' : definition.objectType}</strong><span>{definition.taskSummary}</span></div></div><label className="field-label">Primary value <span className="field-unit">mm</span><input className="field-input" value={isSketch ? 'Fully constrained' : '42.00'} readOnly /></label><label className="field-label">Operation<select className="field-input"><option>{workbench === 'Part Design' ? 'Dimension' : 'Contextual preview'}</option><option>Through all</option><option>Up to face</option></select></label><label className="check-row"><input type="checkbox" defaultChecked /><span>Preview result in viewport</span></label><div className="task-note"><AlertTriangle size={14} /><span>Changes remain local until the document is recomputed.</span></div></div></div>
}
function DataProperties({ selectedObject, showNotice }: { selectedObject: string; showNotice: (message: string) => void }) {
const isPocket = selectedObject === 'pocket'
return <div className="properties-scroll"><div className="property-group"><div className="property-group-title">Identity <ChevronDown size={14} /></div><PropertyRow label="Label" value={isPocket ? 'Pocket' : 'Pad'} editable onClick={() => showNotice('Label editor opened')} /><PropertyRow label="Type" value={isPocket ? 'PartDesign::Pocket' : 'PartDesign::Pad'} /><PropertyRow label="Status" value={isPocket ? 'Warning' : 'Valid'} tone={isPocket ? 'amber' : 'green'} /></div><div className="property-group"><div className="property-group-title">Parameters <ChevronDown size={14} /></div><PropertyRow label="Length" value={isPocket ? 'Through all' : '42.00 mm'} editable onClick={() => showNotice('Length editor opened')} /><PropertyRow label="Profile" value="Sketch" link /><PropertyRow label="Direction" value="Normal sketch axis" /><PropertyRow label="Reversed" value="false" editable onClick={() => showNotice('Boolean editor opened')} /></div><div className="property-group"><div className="property-group-title">Dependencies <ChevronDown size={14} /></div><PropertyRow label="Base" value="Body" link /><PropertyRow label="Support" value="XY_Plane" link /><PropertyRow label="Children" value="Pocket, Fillet" link /></div><div className="property-group"><div className="property-group-title">Expressions <ChevronDown size={14} /></div><div className="expression-row"><Code2 size={13} /><span>Length</span><span className="expression-value">42 mm</span><button title="Edit expression" onClick={() => showNotice('Expression editor opened')}><Pencil size={13} /></button></div></div></div>
function DataProperties({ selectedObject, selectedItem, showNotice }: { selectedObject: string; selectedItem?: ModelTreeItem; showNotice: (message: string) => void }) {
const label = selectedItem?.label || 'Body'
const isPocket = label.toLowerCase().startsWith('pocket')
const isSketch = selectedItem?.type === 'sketch'
const objectType = selectedItem?.type === 'body' ? 'PartDesign::Body' : isSketch ? 'Sketcher::SketchObject' : isPocket ? 'PartDesign::Pocket' : 'PartDesign::Feature'
const status = selectedItem?.state === 'warning' ? 'Warning' : selectedItem?.state === 'readonly' ? 'Read-only' : 'Valid'
return <div className="properties-scroll"><div className="property-group"><div className="property-group-title">Identity <ChevronDown size={14} /></div><PropertyRow label="Label" value={label} editable onClick={() => showNotice('Label editor opened')} /><PropertyRow label="Type" value={objectType} /><PropertyRow label="Status" value={status} tone={status === 'Warning' ? 'amber' : 'green'} /></div><div className="property-group"><div className="property-group-title">Parameters <ChevronDown size={14} /></div><PropertyRow label="Length" value={isSketch ? 'Fully constrained' : isPocket ? 'Through all' : selectedItem?.detail || '42.00 mm'} editable onClick={() => showNotice('Length editor opened')} /><PropertyRow label="Profile" value="Sketch" link /><PropertyRow label="Direction" value="Normal sketch axis" /><PropertyRow label="Reversed" value="false" editable onClick={() => showNotice('Boolean editor opened')} /></div><div className="property-group"><div className="property-group-title">Dependencies <ChevronDown size={14} /></div><PropertyRow label="Base" value="Body" link /><PropertyRow label="Support" value="XY_Plane" link /><PropertyRow label="Children" value="Pocket, Fillet" link /></div><div className="property-group"><div className="property-group-title">Expressions <ChevronDown size={14} /></div><div className="expression-row"><Code2 size={13} /><span>Length</span><span className="expression-value">42 mm</span><button title="Edit expression" onClick={() => showNotice('Expression editor opened')}><Pencil size={13} /></button></div></div></div>
}
function PropertyRow({ label, value, editable = false, link = false, tone, onClick }: { label: string; value: string; editable?: boolean; link?: boolean; tone?: 'amber' | 'green'; onClick?: () => void }) {
@@ -364,7 +370,7 @@ function Viewport({ selectedObject, setSelectedObject, workbench, facade, showNo
adapter.dispose()
}
}, [facade, showNotice, 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 className="grid-lines" /><div className={`mock-part ${selectedObject === 'pad' ? 'part-selected' : ''}`} onClick={(event) => { event.stopPropagation(); setSelectedObject('pad'); showNotice('Selected Pad') }}><div className="part-top" /><div className="part-front"><span className="part-hole one" /><span className="part-hole two" /><span className="part-cut" /></div><div className="part-side" /></div><div className="mock-datum-plane" /><div className="selection-callout"><span className="callout-line" /><span>Pad</span></div></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>
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 className="grid-lines" /><div className={`mock-part ${selectedObject === 'pad' || selectedObject.startsWith('pad') ? 'part-selected' : ''}`} onClick={(event) => { event.stopPropagation(); setSelectedObject('pad'); showNotice('Selected Pad') }}><div className="part-top" /><div className="part-front"><span className="part-hole one" /><span className="part-hole two" /><span className="part-cut" /></div><div className="part-side" /></div><div className="mock-datum-plane" /><div className="selection-callout"><span className="callout-line" /><span>Pad</span></div></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>
}
function PageFrame({ page, onNavigate, onOpenWorkspace, showNotice }: { page: Page; onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void }) {