feat: resolve topology reference candidates

This commit is contained in:
2026-08-03 01:28:16 -04:00
parent ffbe2c69dc
commit 56903dbeb2
12 changed files with 160 additions and 22 deletions

View File

@@ -452,7 +452,16 @@ function PropertyEditor({ facade, objectId, property, showNotice }: { facade: Bi
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' && !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>
const source = ref ? facade.app.document.getObject(ref.objectId) : null
const candidates = ref ? (ref.candidates?.length ? ref.candidates : source?.topology?.entries.filter((entry) => entry.ref.kind === ref.kind && entry.ref.status !== 'deleted').map((entry) => entry.ref.persistentId) ?? []) : []
const replace = (candidatePersistentId: string) => {
if (!candidatePersistentId) return
try {
facade.app.document.resolveTopologyReference({ ownerObjectId: objectId, referenceName: property.name, candidatePersistentId })
void facade.app.document.recomputeAsync().then((result) => showNotice(result.status === 'completed' ? `${property.label} reference replaced` : `Reference replaced; recompute ${result.status}`))
} catch (error) { showNotice(error instanceof Error ? error.message : String(error)) }
}
editor = ref ? <div className="property-link-sub"><span title={ref.persistentId}>{ref.objectId} / {ref.kind} · {ref.status}</span>{(ref.status === 'ambiguous' || ref.status === 'deleted') && candidates.length > 0 ? <select className="property-control property-topology-candidates" aria-label={`${property.label} replacement`} defaultValue="" onChange={(event) => replace(event.target.value)}><option value="">Replace</option>{candidates.map((candidate) => <option value={candidate} key={candidate}>{candidate}</option>)}</select> : null}<IconButton icon={X} label="Clear subshape reference" onClick={() => commit(null)} /></div> : <span className="property-readonly">None</span>
} 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) => {
@@ -670,12 +679,22 @@ function DiagnosticsPage({ onNavigate, showNotice, facade }: { onNavigate: (page
function DocumentDiagnosticNode({ node, facade, showNotice, child = false }: { node: DiagnosticTreeNode; facade: BitBybitWebCadFacade; showNotice: (message: string) => void; child?: boolean }) {
const diagnostic = node.diagnostic
const topologyRepair = diagnostic.topologyRepair
const [topologyCandidate, setTopologyCandidate] = useState(topologyRepair?.candidates[0] ?? '')
const runRepair = (actionId: 'select-object' | 'recompute-root' | 'suppress-root') => {
void facade.diagnostics.repair(diagnostic.id, actionId).then((result) => showNotice(result.message))
}
const replaceTopologyReference = () => {
if (!topologyRepair || !topologyCandidate) return
try {
facade.app.document.resolveTopologyReference({ ...topologyRepair, candidatePersistentId: topologyCandidate })
void facade.app.document.recomputeAsync().then((result) => showNotice(result.status === 'completed' ? 'Topology reference replaced' : `Reference replaced; recompute ${result.status}`))
} catch (error) { showNotice(error instanceof Error ? error.message : String(error)) }
}
return <div className={`document-diagnostic ${child ? 'is-child' : ''}`}>
<div className="document-diagnostic-main"><span className={`diagnostic-severity ${diagnostic.severity}`}><AlertTriangle size={14} /></span><div><strong>{diagnostic.code}</strong><span>{diagnostic.message}</span><small>{diagnostic.objectId || diagnostic.source}{diagnostic.dependencyPath && diagnostic.dependencyPath.length > 1 ? ` · ${diagnostic.dependencyPath.join(' → ')}` : ''}{diagnostic.generation ? ` · generation ${diagnostic.generation}` : ''}</small></div></div>
{diagnostic.repairActions?.length ? <div className="diagnostic-actions">{diagnostic.repairActions.map((action) => <button key={action.id} className="button button-quiet" disabled={!action.enabled} title={action.reason || action.label} onClick={() => runRepair(action.id)}>{action.id === 'select-object' ? <Search size={13} /> : action.id === 'recompute-root' ? <RefreshCw size={13} /> : <Pause size={13} />}{action.label}</button>)}</div> : null}
{topologyRepair ? <div className="topology-repair"><select className="property-control" aria-label="Replacement subshape" value={topologyCandidate} onChange={(event) => setTopologyCandidate(event.target.value)}>{topologyRepair.candidates.map((candidate) => <option value={candidate} key={candidate}>{candidate}</option>)}</select><button className="button button-quiet" onClick={replaceTopologyReference} disabled={!topologyCandidate}><Check size={13} />Replace reference</button></div> : null}
{node.children.length > 0 ? <div className="diagnostic-children">{node.children.map((entry) => <DocumentDiagnosticNode key={entry.diagnostic.id} node={entry} facade={facade} showNotice={showNotice} child />)}</div> : null}
</div>
}