feat: persist LinkSub topology references

This commit is contained in:
2026-08-02 23:41:16 -04:00
parent 9def4708ea
commit d3d88b297b
11 changed files with 69 additions and 26 deletions

View File

@@ -31,6 +31,7 @@ import { cloneSketch, createSketch, solveSketch, type SketchConstraint, type Ske
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, type RecomputeExecutionOptions } from './recomputeEngine'
import { inspectFcstdArchive } from './fcstd'
import { buildDiagnosticTree, buildRecomputeDiagnostics, cloneDiagnostic, replaceRecomputeDiagnostics } from './diagnostics'
import { parseTopoRef } from './topologyReferences'
const initialTree: ModelTreeItem[] = [
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
@@ -96,6 +97,7 @@ const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
{ name: 'Length', label: 'Length', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 18, unit: 'mm', recompute: true },
{ name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'sketch', recompute: true },
{ name: 'Base', label: 'Base', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'pad', recompute: true },
{ name: 'UpToFace', label: 'Up to face', group: 'Parameters', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
{ name: 'Reversed', label: 'Reversed', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
]
if (item.id.startsWith('revolution')) return [
@@ -141,10 +143,12 @@ const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
const createObjectSnapshot = (item: ModelTreeItem): DocumentObjectSnapshot => ({ id: item.id, typeId: typeIdForItem(item), properties: [...commonProperties(item), ...featureProperties(item), ...viewProperties()], sketch: item.type === 'sketch' ? createSketch(item.id) : undefined })
const clonePropertyValue = (value: PropertyValue): PropertyValue => value && typeof value === 'object' ? { ...value, candidates: value.candidates ? [...value.candidates] : undefined } : value
const cloneDocumentSnapshot = (document: DocumentSnapshot): DocumentSnapshot => ({
...document,
tree: document.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined })),
objects: document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })),
objects: document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })),
dependencies: document.dependencies?.map((edge) => ({ ...edge })),
recompute: document.recompute ? { ...document.recompute, dirtyObjects: [...document.recompute.dirtyObjects], order: [...document.recompute.order], objectStates: { ...document.recompute.objectStates }, errors: document.recompute.errors.map((error) => ({ ...error })) } : undefined,
})
@@ -155,6 +159,7 @@ const collectDependencyEdges = (document: Pick<DocumentSnapshot, 'objects'>): De
for (const object of document.objects) {
for (const property of object.properties) {
if (property.type === 'App::PropertyLink' && typeof property.value === 'string' && objectIds.has(property.value)) edges.push({ sourceId: object.id, targetId: property.value, relation: 'link', propertyName: property.name })
if (property.type === 'App::PropertyLinkSub' && property.value && typeof property.value === 'object' && objectIds.has(property.value.objectId)) edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'topo-ref', propertyName: property.name, reference: property.value.persistentId })
if (property.expression) {
for (const reference of expressionReferences(property.expression)) {
const separator = reference.lastIndexOf('.')
@@ -260,6 +265,13 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope
const knownIds = new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))
if (value !== null && !knownIds.has(value)) throw new RangeError(`${property.label} target does not exist in this document.`)
}
if (property.type === 'App::PropertyLinkSub') {
if (value !== null && (typeof value !== 'object' || Array.isArray(value))) throw new TypeError(`${property.label} requires a TopoRef value.`)
if (value !== null) {
const topoRef = parseTopoRef(JSON.stringify(value))
if (!document.objects.some((object) => object.id === topoRef.objectId)) throw new RangeError(`${property.label} target does not exist in this document.`)
}
}
if (property.name === 'Label' && !(value as string).trim()) throw new RangeError('Label cannot be empty.')
}
@@ -383,7 +395,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
if (Number(objectSnapshot.properties.find((property) => property.name === 'Diameter')?.value) <= 0) throw new RangeError('Hole diameter must be greater than zero.')
if (Number(objectSnapshot.properties.find((property) => property.name === 'Depth')?.value) <= 0) throw new RangeError('Hole depth must be greater than zero.')
}
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value) })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
const tipObject = type === 'feature' && partDesignCommands.has(commandId) && commandId !== 'create-sketch' ? objects.find((object) => object.id === 'body') : undefined
if (tipObject) {
const tip = tipObject.properties.find((property) => property.name === 'Tip')
@@ -407,7 +419,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
const document = cloneDocumentSnapshot(state.document)
const object = document.objects[objectIndex]
object.properties[propertyIndex] = { ...object.properties[propertyIndex], value, expression: undefined, expressionError: undefined }
object.properties[propertyIndex] = { ...object.properties[propertyIndex], value: clonePropertyValue(value), expression: undefined, expressionError: undefined }
const treeItem = document.tree.find((item) => item.id === objectId)
if (propertyName === 'Label' && treeItem) treeItem.label = String(value)
document.dependencies = collectDependencyEdges(document)