feat: resolve topology reference candidates
This commit is contained in:
21
src/App.tsx
21
src/App.tsx
@@ -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>
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const cloneDiagnostic = (diagnostic: Diagnostic): Diagnostic => ({
|
||||
...diagnostic,
|
||||
dependencyPath: diagnostic.dependencyPath ? [...diagnostic.dependencyPath] : undefined,
|
||||
repairActions: diagnostic.repairActions?.map(cloneAction),
|
||||
topologyRepair: diagnostic.topologyRepair ? { ...diagnostic.topologyRepair, candidates: [...diagnostic.topologyRepair.candidates] } : undefined,
|
||||
})
|
||||
|
||||
const rootCausePath = (objectId: string, graph: DependencyGraph, states: Record<string, RecomputeState>) => {
|
||||
|
||||
@@ -7,9 +7,9 @@ export { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION,
|
||||
export type { ProjectMigrationTransaction, ProjectSchemaMigration } from './projectSchema'
|
||||
export { assessResourceQuota, planResourceSweep } from './resourcePolicy'
|
||||
export type { ResourceQuotaAssessment, ResourceSweepPlan, ResourceSweepRecord } from './resourcePolicy'
|
||||
export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, Diagnostic, DiagnosticRepairAction, DiagnosticRepairResult, DiagnosticTreeNode, DocumentObjectSnapshot, DocumentSnapshot, FacadeEvent, FacadeState, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, ModelTreeItem, ObjectPropertySnapshot, ObjectTopologySnapshot, PadInput, PersistenceCapabilities, Placement, PlacementValue, PlanarProfile, PocketInput, Point3, ProjectRecoveryReport, ProjectResource, ProjectResourceSweepReport, ProjectSaveResult, ProjectSummary, PropertyValue, RecomputeResult, RevolutionInput, SetExpressionInput, SetPropertyInput, ShapeHandle, SubshapeRef, SubshapeSignature, SubshapeTopology, TaskSnapshot, TopoRefValue, TopologyMigrationMatch, TopologySnapshotEntry, VectorValue } from './types'
|
||||
export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, Diagnostic, DiagnosticRepairAction, DiagnosticRepairResult, DiagnosticTreeNode, DocumentObjectSnapshot, DocumentSnapshot, FacadeEvent, FacadeState, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, ModelTreeItem, ObjectPropertySnapshot, ObjectTopologySnapshot, PadInput, PersistenceCapabilities, Placement, PlacementValue, PlanarProfile, PocketInput, Point3, ProjectRecoveryReport, ProjectResource, ProjectResourceSweepReport, ProjectSaveResult, ProjectSummary, PropertyValue, RecomputeResult, ResolveTopologyReferenceInput, RevolutionInput, SetExpressionInput, SetPropertyInput, ShapeHandle, SubshapeRef, SubshapeSignature, SubshapeTopology, TaskSnapshot, TopoRefValue, TopologyMigrationMatch, TopologySnapshotEntry, VectorValue } from './types'
|
||||
export { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from './topologyNaming'
|
||||
export { cloneObjectTopologySnapshot, createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveTopoRef, serializeTopoRef } from './topologyReferences'
|
||||
export { cloneObjectTopologySnapshot, createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from './topologyReferences'
|
||||
export type { DocumentTopologyReferenceMigration, PersistedTopoRef, TopologyMigration, TopologyReferenceMigrationIssue, TopoRefResolution } from './topologyReferences'
|
||||
export { captureSignatureTopologyHistory } from './topologyHistory'
|
||||
export type { TopologyHistoryEntry, TopologyHistoryRelation, TopologyHistoryResult } from './topologyHistory'
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
SetPropertyInput,
|
||||
SetExpressionInput,
|
||||
RecomputeResult,
|
||||
ResolveTopologyReferenceInput,
|
||||
ShapeHandle,
|
||||
TaskSnapshot,
|
||||
Unsubscribe,
|
||||
@@ -32,7 +33,7 @@ import { cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch,
|
||||
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, type RecomputeExecutionOptions } from './recomputeEngine'
|
||||
import { inspectFcstdArchive } from './fcstd'
|
||||
import { buildDiagnosticTree, buildRecomputeDiagnostics, cloneDiagnostic, replaceRecomputeDiagnostics } from './diagnostics'
|
||||
import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef } from './topologyReferences'
|
||||
import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef, resolveDocumentTopologyReference } from './topologyReferences'
|
||||
|
||||
const initialTree: ModelTreeItem[] = [
|
||||
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
|
||||
@@ -464,6 +465,18 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
commit({ ...state, document })
|
||||
notify(`${sourceProperty.label} updated`)
|
||||
}
|
||||
const resolveTopologyReference = (input: ResolveTopologyReferenceInput) => {
|
||||
const document = cloneDocumentSnapshot(state.document)
|
||||
const resolved = resolveDocumentTopologyReference(document, input)
|
||||
document.dependencies = collectDependencyEdges(document)
|
||||
markDocumentTouched(document, [input.ownerObjectId])
|
||||
document.version += 1
|
||||
document.dirty = true
|
||||
const diagnostics = state.diagnostics.filter((diagnostic) => diagnostic.topologyRepair?.ownerObjectId !== input.ownerObjectId || diagnostic.topologyRepair.referenceName !== input.referenceName)
|
||||
commit({ ...state, document, diagnostics })
|
||||
notify(`${input.referenceName} topology reference replaced`)
|
||||
return { ...resolved, candidates: resolved.candidates ? [...resolved.candidates] : undefined }
|
||||
}
|
||||
const setExpression = ({ objectId, propertyName, expression }: SetExpressionInput) => {
|
||||
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
|
||||
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
|
||||
@@ -566,7 +579,12 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
if (status) status.value = topologyMigration.issues.some((issue) => issue.ownerObjectId === objectId) ? 'Topology reference requires repair' : 'Touched'
|
||||
}
|
||||
const nextDiagnostics = buildRecomputeDiagnostics({ document, generation: result.generation, affected: result.affected, objectStates: result.objectStates, errors: result.errors })
|
||||
const topologyDiagnostics: Diagnostic[] = topologyMigration.issues.map((issue, index) => ({
|
||||
const topologyDiagnostics: Diagnostic[] = topologyMigration.issues.map((issue, index) => {
|
||||
const sourceTopology = document.objects.find((object) => object.id === issue.sourceObjectId)?.topology
|
||||
const candidates = issue.candidates?.length
|
||||
? issue.candidates
|
||||
: sourceTopology?.entries.filter((entry) => entry.ref.kind === issue.kind && entry.ref.status !== 'deleted').map((entry) => entry.ref.persistentId) ?? []
|
||||
return {
|
||||
id: `topology:${document.id}:${result.generation}:${issue.ownerObjectId}:${issue.referenceName}:${index}`,
|
||||
source: 'geometry',
|
||||
severity: issue.status === 'deleted' ? 'error' : 'warning',
|
||||
@@ -580,11 +598,12 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
generation: result.generation,
|
||||
rootCauseObjectId: issue.sourceObjectId,
|
||||
dependencyPath: [issue.ownerObjectId, issue.sourceObjectId],
|
||||
topologyRepair: candidates.length > 0 ? { ownerObjectId: issue.ownerObjectId, referenceName: issue.referenceName, candidates: [...new Set(candidates)] } : undefined,
|
||||
repairActions: [
|
||||
{ id: 'select-object', label: 'Select reference owner', targetObjectId: issue.ownerObjectId, enabled: true },
|
||||
{ id: 'recompute-root', label: 'Recompute after replacing reference', targetObjectId: issue.ownerObjectId, enabled: false, reason: 'Select one current subshape and replace the ambiguous or deleted reference first.' },
|
||||
],
|
||||
}))
|
||||
}})
|
||||
const replaced = replaceRecomputeDiagnostics(state.diagnostics, document.id, result.affected, nextDiagnostics)
|
||||
const topologyOwners = new Set(topologyMigration.issues.map((issue) => issue.ownerObjectId))
|
||||
const diagnostics = [...replaced.filter((diagnostic) => !diagnostic.code.startsWith('TOPOLOGY_REFERENCE_') || diagnostic.documentId !== document.id || !diagnostic.objectId || !topologyOwners.has(diagnostic.objectId)), ...topologyDiagnostics]
|
||||
@@ -738,7 +757,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
}
|
||||
|
||||
const facade: BitBybitWebCadFacade = {
|
||||
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
|
||||
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, resolveTopologyReference, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
|
||||
history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; clearFeatureShapes(); redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; clearFeatureShapes(); undoStack.push(getState()); state = next; emitState(); notify('Redo applied') } },
|
||||
gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId, state.document.objects.find((object) => object.id === state.selectedObjectId)?.typeId), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
|
||||
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DocumentSnapshot, ObjectTopologySnapshot, SubshapeRef, TopoRefValue } from './types'
|
||||
import type { DocumentSnapshot, ObjectTopologySnapshot, ResolveTopologyReferenceInput, SubshapeRef, TopoRefValue } from './types'
|
||||
import { matchSubshapes, type SubshapeMatch, type SubshapeSignature } from './topologyNaming'
|
||||
|
||||
export type PersistedTopoRef = TopoRefValue
|
||||
@@ -19,6 +19,7 @@ export type TopologyReferenceMigrationIssue = {
|
||||
ownerObjectId: string
|
||||
sourceObjectId: string
|
||||
referenceName: string
|
||||
kind: SubshapeRef['kind']
|
||||
status: 'ambiguous' | 'deleted'
|
||||
persistentId: string
|
||||
candidates?: string[]
|
||||
@@ -166,7 +167,7 @@ export const migrateDocumentTopologyReferences = (
|
||||
if (!source) return record
|
||||
const migrated = migrateReferenceThroughSnapshot(record, source)
|
||||
if (migrated.generation !== record.generation || migrated.status !== record.status || migrated.persistentId !== record.persistentId) changedOwnerIds.add(ownerObjectId)
|
||||
if (migrated.status === 'ambiguous' || migrated.status === 'deleted') issues.push({ ownerObjectId, sourceObjectId: record.objectId, referenceName, status: migrated.status, persistentId: migrated.persistentId, candidates: migrated.candidates ? [...migrated.candidates] : undefined })
|
||||
if (migrated.status === 'ambiguous' || migrated.status === 'deleted') issues.push({ ownerObjectId, sourceObjectId: record.objectId, referenceName, kind: record.kind, status: migrated.status, persistentId: migrated.persistentId, candidates: migrated.candidates ? [...migrated.candidates] : undefined })
|
||||
return migrated
|
||||
}
|
||||
for (const object of document.objects) {
|
||||
@@ -178,3 +179,32 @@ export const migrateDocumentTopologyReferences = (
|
||||
}
|
||||
return { changedOwnerIds: [...changedOwnerIds], issues }
|
||||
}
|
||||
|
||||
export const resolveDocumentTopologyReference = (
|
||||
document: DocumentSnapshot,
|
||||
input: ResolveTopologyReferenceInput,
|
||||
): PersistedTopoRef => {
|
||||
const owner = document.objects.find((object) => object.id === input.ownerObjectId)
|
||||
if (!owner) throw new Error(`Topology reference owner does not exist: ${input.ownerObjectId}`)
|
||||
let current: PersistedTopoRef | null = null
|
||||
let replace: ((record: PersistedTopoRef) => void) | null = null
|
||||
if (input.referenceName.startsWith('ExternalGeometry:')) {
|
||||
const externalId = input.referenceName.slice('ExternalGeometry:'.length)
|
||||
const external = owner.sketch?.externalGeometry.find((candidate) => candidate.id === externalId)
|
||||
if (external) { current = external.source; replace = (record) => { external.source = record } }
|
||||
} else {
|
||||
const property = owner.properties.find((candidate) => candidate.name === input.referenceName && candidate.type === 'App::PropertyLinkSub')
|
||||
if (property?.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'schemaVersion' in property.value) {
|
||||
current = property.value
|
||||
replace = (record) => { property.value = record }
|
||||
}
|
||||
}
|
||||
if (!current || !replace) throw new Error(`Topology reference does not exist: ${input.ownerObjectId}.${input.referenceName}`)
|
||||
const source = document.objects.find((object) => object.id === current?.objectId)
|
||||
if (!source?.topology) throw new Error(`Source object has no current topology snapshot: ${current.objectId}`)
|
||||
const candidate = source.topology.entries.find((entry) => entry.ref.persistentId === input.candidatePersistentId && entry.ref.kind === current?.kind)
|
||||
if (!candidate) throw new RangeError(`Topology candidate is not available for ${input.referenceName}: ${input.candidatePersistentId}`)
|
||||
const resolved = createPersistedTopoRef(current.objectId, { ...candidate.ref, status: 'stable', candidates: undefined }, source.topology.generation)
|
||||
replace(resolved)
|
||||
return resolved
|
||||
}
|
||||
|
||||
@@ -90,6 +90,12 @@ export type SetPropertyInput = {
|
||||
value: PropertyValue
|
||||
}
|
||||
|
||||
export type ResolveTopologyReferenceInput = {
|
||||
ownerObjectId: string
|
||||
referenceName: string
|
||||
candidatePersistentId: string
|
||||
}
|
||||
|
||||
export type PersistenceCapabilities = {
|
||||
mode: 'sqlite-opfs' | 'sqlite-memory' | 'unavailable'
|
||||
sqliteWasm: boolean
|
||||
@@ -381,6 +387,11 @@ export type Diagnostic = {
|
||||
rootCauseObjectId?: string
|
||||
dependencyPath?: string[]
|
||||
repairActions?: DiagnosticRepairAction[]
|
||||
topologyRepair?: {
|
||||
ownerObjectId: string
|
||||
referenceName: string
|
||||
candidates: string[]
|
||||
}
|
||||
resolved?: boolean
|
||||
}
|
||||
|
||||
@@ -457,6 +468,7 @@ export interface BitBybitWebCadFacade {
|
||||
load(documentId: string): Promise<DocumentSnapshot | null>
|
||||
markDirty(): void
|
||||
setProperty(input: SetPropertyInput): void
|
||||
resolveTopologyReference(input: ResolveTopologyReferenceInput): TopoRefValue
|
||||
setExpression(input: SetExpressionInput): void
|
||||
recompute(): RecomputeResult
|
||||
recomputeAsync(options?: RecomputeExecutionOptions): Promise<RecomputeExecutionResult>
|
||||
|
||||
@@ -378,8 +378,10 @@ button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px s
|
||||
.property-number .property-control { min-width: 0; }
|
||||
.property-link-control { color: var(--cyan); }
|
||||
.property-link-sub { display: flex; min-width: 0; align-items: center; gap: 4px; color: var(--cyan); font-size: 10px; }
|
||||
.property-link-sub > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.property-link-sub > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.property-link-sub .icon-button { width: 24px; height: 24px; flex: 0 0 24px; }
|
||||
.property-topology-candidates { min-width: 0; max-width: 112px; }
|
||||
.topology-repair { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; margin: 8px 0 0 34px; }
|
||||
.property-color { display: flex; align-items: center; gap: 5px; color: var(--text-muted); font-size: 9px; }
|
||||
.property-color input { width: 22px; height: 18px; padding: 0; border: 1px solid var(--line); border-radius: 2px; background: transparent; }
|
||||
.property-expression { min-height: 20px; display: flex; align-items: center; justify-content: flex-end; gap: 5px; color: var(--cyan); font-size: 9px; }
|
||||
|
||||
Reference in New Issue
Block a user