feat: prepare FreeCAD private naming worker linkage
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-11 23:16:22 -04:00
parent aa607451ad
commit 97967041e2
30 changed files with 505 additions and 84 deletions

View File

@@ -52,6 +52,7 @@ export type FcstdObjectSummary = {
name: string
label: string
typeId: string
nativeObjectTag?: number
propertyCount: number
properties: FcstdPropertySummary[]
support: FcstdObjectSupport
@@ -1103,16 +1104,32 @@ export const serializeFcstdMetadataArchive = (document: DocumentSnapshot, option
const objectIds = document.objects.map((object) => object.id.trim())
if (objectIds.some((objectId) => !objectId)) throw new TypeError('FCStd metadata writer requires non-empty object ids.')
if (new Set(objectIds).size !== objectIds.length) throw new TypeError('FCStd metadata writer requires unique object ids.')
const explicitObjectTags = new Set<number>()
for (const object of document.objects) {
if (object.nativeObjectTag === undefined) continue
if (!Number.isSafeInteger(object.nativeObjectTag) || object.nativeObjectTag <= 0) throw new RangeError(`FCStd object ${object.id} nativeObjectTag must be a positive safe integer.`)
if (explicitObjectTags.has(object.nativeObjectTag)) throw new RangeError(`FCStd nativeObjectTag ${object.nativeObjectTag} is assigned to more than one object.`)
explicitObjectTags.add(object.nativeObjectTag)
}
let nextObjectTag = 1
const objectTags = new Map(document.objects.map((object) => {
if (object.nativeObjectTag !== undefined) return [object.id, object.nativeObjectTag] as const
while (explicitObjectTags.has(nextObjectTag)) nextObjectTag += 1
const tag = nextObjectTag
explicitObjectTags.add(tag)
nextObjectTag += 1
return [object.id, tag] as const
}))
const objectIdSet = new Set(objectIds)
const dependencies = new Map(objectIds.map((objectId) => [objectId, [] as string[]]))
for (const edge of document.dependencies ?? []) {
if (!objectIdSet.has(edge.sourceId) || !objectIdSet.has(edge.targetId)) throw new Error(`FCStd dependency ${edge.sourceId} -> ${edge.targetId} references an undeclared object.`)
if (edge.relation !== 'view') dependencies.get(edge.sourceId)?.push(edge.targetId)
}
const objectDeclarations = document.objects.map((object, objectIndex) => {
const objectDeclarations = document.objects.map((object) => {
const targets = dependencies.get(object.id) ?? []
const dependencyXml = targets.length === 0 ? `<ObjectDeps Name="${xmlEscape(object.id)}" Count="0"/>` : `<ObjectDeps Name="${xmlEscape(object.id)}" Count="${targets.length}">${targets.map((target) => `<Dep Name="${xmlEscape(target)}"/>`).join('')}</ObjectDeps>`
return `${dependencyXml}<Object name="${xmlEscape(object.id)}" type="${xmlEscape(nativeFcstdTypeId(object.typeId))}" id="${objectIndex + 1}" Touched="1"/>`
return `${dependencyXml}<Object name="${xmlEscape(object.id)}" type="${xmlEscape(nativeFcstdTypeId(object.typeId))}" id="${objectTags.get(object.id)}" Touched="1"/>`
}).join('')
const objectData = document.objects.map((object) => {
const sketchGeneratedPropertyNames = object.sketch ? new Set(['Geometry', 'Constraints', 'ExternalGeo', 'ExternalGeometry', 'ExternalTypes', 'WebGeometryIds', 'WebSyntheticConstraintIds', 'WebExternalIds', 'WebExternalProjectionIds', 'WebExternalSources']) : new Set<string>()
@@ -2014,9 +2031,18 @@ const parseDocumentXml = (bytes: Uint8Array, limits: FcstdArchiveLimits) => {
const dataByName = new Map(objectData.map((data) => [attribute(data, 'name'), data]))
const documentProperties = asArray((((root.Properties as Record<string, unknown> | undefined)?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
const labelProperty = documentProperties.find((property) => attribute(property, 'name') === 'Label')
const nativeObjectTags = new Set<number>()
const objects = objectDeclarations.map((declaration): FcstdObjectSummary => {
const name = attribute(declaration, 'name') || '<unnamed>'
const typeId = attribute(declaration, 'type') || 'App::DocumentObject'
const objectTagText = attribute(declaration, 'id')
let nativeObjectTag: number | undefined
if (objectTagText) {
if (!/^\d+$/.test(objectTagText) || !Number.isSafeInteger(Number(objectTagText)) || Number(objectTagText) <= 0) throw new Error(`FCStd object ${name} has an invalid native id.`)
nativeObjectTag = Number(objectTagText)
if (nativeObjectTags.has(nativeObjectTag)) throw new Error(`FCStd object native id ${nativeObjectTag} is duplicated.`)
nativeObjectTags.add(nativeObjectTag)
}
const data = dataByName.get(name)
const propertiesContainer = data?.Properties as Record<string, unknown> | undefined
const properties = asArray(((propertiesContainer?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
@@ -2029,7 +2055,7 @@ const parseDocumentXml = (bytes: Uint8Array, limits: FcstdArchiveLimits) => {
const support: FcstdObjectSupport = blockedTypeId(typeId) ? 'blocked' : recognizedTypeIds.has(typeId) ? 'recognized' : 'proxy'
const summaries = propertySummaries(properties).map((property) => expressions.has(property.name) ? { ...property, expression: expressions.get(property.name) } : property)
const sketch = typeId === 'Sketcher::SketchObject' ? sketchFromPropertySummaries(name, summaries) : undefined
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, propertyCount: properties.length, properties: summaries, support, extensions, ...(sketch ? { sketch } : {}) }
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, ...(nativeObjectTag === undefined ? {} : { nativeObjectTag }), propertyCount: properties.length, properties: summaries, support, extensions, ...(sketch ? { sketch } : {}) }
})
return {
schemaVersion: attribute(root, 'SchemaVersion') || attribute(root, 'schemaVersion') || 'unknown',
@@ -2348,6 +2374,7 @@ const createProxyDocument = (inspection: Omit<FcstdInspection, 'proxyDocument'>)
return {
id: object.name,
typeId: object.typeId,
...(object.nativeObjectTag === undefined ? {} : { nativeObjectTag: object.nativeObjectTag }),
properties: [
createProxyProperty('Label', 'Label', object.label),
createProxyProperty('TypeId', 'Type', object.typeId),

View File

@@ -1324,9 +1324,12 @@ export class BitbybitGeometryRuntime {
const stageTransport = input.stages?.length ? {
inputs: input.inputs.map((source, index) => ({
inputId: source.inputId ?? `${input.operationId}:input:${index}`,
objectId: source.objectId,
role: source.role,
stageId: source.stageId,
step: stepByObjectId.get(source.objectId)!.text,
objectTag: source.objectTag,
namingEvidence: source.namingEvidence,
})),
stages: input.stages.map((stage) => ({
stageId: stage.stageId,
@@ -1339,12 +1342,12 @@ export class BitbybitGeometryRuntime {
ordinal: stage.ordinal,
})),
} : {}
const captureHistory = (request: Parameters<NativeOcctHistoryCoordinator['capture']>[1]) => nativeHistory.coordinator.capture(nativeHistory.provider, { ...request, ...stageTransport })
const mapHistoryRecords = (response: Parameters<typeof mapNativeOcctHistoryRecords>[0], sourceIds: { object: string; tool: string } | Record<string, string>) => mapNativeOcctHistoryRecords(response, sourceIds, input)
type NativeStageRequest = Pick<NativeOcctHistoryRequest, 'objectStep'> & Partial<Pick<NativeOcctHistoryRequest, 'toolStep' | 'direction' | 'axisOrigin' | 'angle'>>
const finalResultObjectId = () => input.stages?.length
? input.stages[input.stages.length - 1].resultObjectId ?? `${input.operationId}:result`
: `${input.operationId}:result`
const captureHistory = (request: Parameters<NativeOcctHistoryCoordinator['capture']>[1]) => nativeHistory.coordinator.capture(nativeHistory.provider, { ...request, ...stageTransport, resultObjectId: finalResultObjectId(), resultObjectTag: input.resultObjectTag })
const mapHistoryRecords = (response: Parameters<typeof mapNativeOcctHistoryRecords>[0], sourceIds: { object: string; tool: string } | Record<string, string>) => mapNativeOcctHistoryRecords(response, sourceIds, input)
type NativeStageRequest = Pick<NativeOcctHistoryRequest, 'objectStep'> & Partial<Pick<NativeOcctHistoryRequest, 'toolStep' | 'direction' | 'axisOrigin' | 'angle'>>
const namingEvidenceForResponse = (response: NativeOcctHistoryResponse, stageId: string, resultObjectId: string) => response.namingEvidence
? assertNativeNamingEvidence({ ...response.namingEvidence, stageId, resultObjectId })
: createFinalShapeOnlyNamingEvidence(stageId, resultObjectId)
@@ -1358,11 +1361,24 @@ export class BitbybitGeometryRuntime {
inputs: NativeTopologyHistoryInput['inputs']
request: NativeStageRequest
}) => {
const transportInputs = stage.inputs.map((source, index) => ({
inputId: `${stage.stageId}:input:${index}`,
objectId: source.objectId,
role: index === 0 ? 'object' : 'tool',
stageId: source.stageId,
step: index === 0 ? stage.request.objectStep : stage.request.toolStep ?? stage.request.objectStep,
objectTag: source.objectTag,
namingEvidence: source.namingEvidence,
}))
const execution = await nativeHistory.coordinator.capture(nativeHistory.provider, {
documentId: input.documentId,
documentVersion: input.documentVersion,
operationId: stage.stageId,
operation: stage.operation,
resultObjectId: stage.resultObjectId,
...(stage.resultObjectId === finalResultObjectId() && input.resultObjectTag !== undefined ? { resultObjectTag: input.resultObjectTag } : {}),
inputs: transportInputs,
stages: [{ stageId: stage.stageId, operation: stage.operation, inputIds: transportInputs.map(({ inputId }) => inputId), ordinal: stage.ordinal }],
...stage.request,
})
if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT ${stage.operation} stage history ${execution.status}.`)
@@ -1445,8 +1461,8 @@ export class BitbybitGeometryRuntime {
ordinal: 2,
sourceIds: { object: firstObjectId, tool: secondObjectId },
inputs: [
{ objectId: firstObjectId, shape: profileInput.shape, stageId: firstStageId },
{ objectId: secondObjectId, shape: profileInput.shape, stageId: secondStageId },
{ objectId: firstObjectId, shape: profileInput.shape, stageId: firstStageId, namingEvidence: first.capture.namingEvidence },
{ objectId: secondObjectId, shape: profileInput.shape, stageId: secondStageId, namingEvidence: second.capture.namingEvidence },
],
request: { objectStep: first.resultStep, toolStep: second.resultStep },
})
@@ -1498,8 +1514,8 @@ export class BitbybitGeometryRuntime {
ordinal: 2,
sourceIds: { object: toolOneObjectId, tool: toolTwoObjectId },
inputs: [
{ objectId: toolOneObjectId, shape: profileInput.shape, stageId: toolOneStageId },
{ objectId: toolTwoObjectId, shape: profileInput.shape, stageId: toolTwoStageId },
{ objectId: toolOneObjectId, shape: profileInput.shape, stageId: toolOneStageId, namingEvidence: toolOne.capture.namingEvidence },
{ objectId: toolTwoObjectId, shape: profileInput.shape, stageId: toolTwoStageId, namingEvidence: toolTwo.capture.namingEvidence },
],
request: { objectStep: toolOne.resultStep, toolStep: toolTwo.resultStep },
})
@@ -1512,7 +1528,7 @@ export class BitbybitGeometryRuntime {
sourceIds: { object: baseInput.objectId, tool: fusedToolObjectId },
inputs: [
baseInput,
{ objectId: fusedToolObjectId, shape: profileInput.shape, stageId: fuseStageId },
{ objectId: fusedToolObjectId, shape: profileInput.shape, stageId: fuseStageId, namingEvidence: fusedTool.capture.namingEvidence },
],
request: { objectStep: baseStep, toolStep: fusedTool.resultStep },
})
@@ -1554,7 +1570,7 @@ export class BitbybitGeometryRuntime {
resultObjectId: finalResultObjectId(),
ordinal: 1,
sourceIds: { object: rotatedProfileObjectId, tool: rotatedProfileObjectId },
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId }],
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId, namingEvidence: rotated.capture.namingEvidence }],
request: { objectStep: rotated.resultStep, axisOrigin, direction: parameters.direction, angle: parameters.totalAngle },
})
return withStageCaptures([rotated.capture, revolution.capture])
@@ -1592,7 +1608,7 @@ export class BitbybitGeometryRuntime {
resultObjectId: revolutionToolObjectId,
ordinal: 1,
sourceIds: { object: rotatedProfileObjectId, tool: rotatedProfileObjectId },
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId }],
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId, namingEvidence: rotated.capture.namingEvidence }],
request: { objectStep: rotated.resultStep, axisOrigin, direction: parameters.direction, angle: parameters.totalAngle },
})
const cut = await captureStage({
@@ -1602,7 +1618,7 @@ export class BitbybitGeometryRuntime {
resultObjectId: finalResultObjectId(),
ordinal: 2,
sourceIds: { object: baseInput.objectId, tool: revolutionToolObjectId },
inputs: [baseInput, { objectId: revolutionToolObjectId, shape: profileInput.shape, stageId: revolutionStageId }],
inputs: [baseInput, { objectId: revolutionToolObjectId, shape: profileInput.shape, stageId: revolutionStageId, namingEvidence: revolution.capture.namingEvidence }],
request: { objectStep: baseStep, toolStep: revolution.resultStep },
})
return withStageCaptures([rotated.capture, revolution.capture, cut.capture])
@@ -1639,6 +1655,9 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: toolStageId,
operation: 'pad',
resultObjectId: toolObjectId,
inputs: [{ inputId: `${toolStageId}:input:0`, objectId: profileInput.objectId, role: 'object', step: profileStep.text, objectTag: profileInput.objectTag, namingEvidence: profileInput.namingEvidence }],
stages: [{ stageId: toolStageId, operation: 'pad', inputIds: [`${toolStageId}:input:0`], ordinal: 0 }],
objectStep: profileStep.text,
direction: sides[0].direction,
})
@@ -1661,6 +1680,13 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: cutStageId,
operation: 'cut',
resultObjectId,
...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }),
inputs: [
{ inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence },
{ inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolResponse.resultStep, namingEvidence: toolResponse.namingEvidence },
],
stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }],
objectStep: baseStep.text,
toolStep: toolResponse.resultStep,
})
@@ -1745,6 +1771,9 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: toolStageId,
operation: 'revolution',
resultObjectId: toolObjectId,
inputs: [{ inputId: `${toolStageId}:input:0`, objectId: profileInput.objectId, role: 'object', step: profileStep.text, objectTag: profileInput.objectTag, namingEvidence: profileInput.namingEvidence }],
stages: [{ stageId: toolStageId, operation: 'revolution', inputIds: [`${toolStageId}:input:0`], ordinal: 0 }],
objectStep: profileStep.text,
axisOrigin,
direction: sides[0].direction,
@@ -1769,6 +1798,13 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: cutStageId,
operation: 'cut',
resultObjectId,
...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }),
inputs: [
{ inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence },
{ inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolResponse.resultStep, namingEvidence: toolResponse.namingEvidence },
],
stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }],
objectStep: baseStep.text,
toolStep: toolResponse.resultStep,
})
@@ -1909,6 +1945,13 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: cutStageId,
operation: 'cut',
resultObjectId,
...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }),
inputs: [
{ inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence },
{ inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolStep.text, namingEvidence: createFinalShapeOnlyNamingEvidence(toolStageId, toolObjectId, 'Hole tool was synthesized by the geometry runtime; provider did not capture its builder naming evidence.') },
],
stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }],
objectStep: baseStep.text,
toolStep: toolStep.text,
})
@@ -2050,6 +2093,7 @@ export class BitbybitGeometryRuntime {
let previousStep = baseStep.text
let previousObjectId = baseInput.objectId
let previousStageId: string | undefined
let previousNamingEvidence = baseInput.namingEvidence
const declaredResultObjectId = input.stages?.length ? input.stages[input.stages.length - 1].resultObjectId : undefined
for (let index = 0; index < input.transforms.length; index += 1) {
const step = input.transforms[index]
@@ -2062,6 +2106,10 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: `${input.operationId}:native-stage:${index}`,
operation,
resultObjectId,
...(index === input.transforms.length - 1 && input.resultObjectTag !== undefined ? { resultObjectTag: input.resultObjectTag } : {}),
inputs: [{ inputId: `${stageId}:input:0`, objectId: previousObjectId, role: 'object', stageId: previousStageId, step: previousStep, ...(index === 0 && baseInput.objectTag !== undefined ? { objectTag: baseInput.objectTag } : {}), namingEvidence: previousNamingEvidence }],
stages: [{ stageId, operation, inputIds: [`${stageId}:input:0`], ordinal: index }],
objectStep: previousStep,
...(step.type === 'linear' ? { direction: step.direction } : {}),
...(step.type === 'polar' ? { axisOrigin: step.axisOrigin, direction: step.direction, angle: step.angle } : {}),
@@ -2085,6 +2133,7 @@ export class BitbybitGeometryRuntime {
previousStep = response.resultStep
previousObjectId = resultObjectId
previousStageId = stageId
previousNamingEvidence = namingEvidenceForResponse(response, stageId, resultObjectId)
}
const records = captures[captures.length - 1].records as NativeTopologyHistoryRecords
Object.defineProperty(records, 'stageCaptures', { value: captures, enumerable: false, configurable: false, writable: false })

View File

@@ -81,7 +81,7 @@ export { NATIVE_OCCT_NAMING_ABI_UNAVAILABLE, nativeNamingAbiCapabilities, native
export type { NativeNamingAbiCapabilities, NativeOcctHistoryCapabilities, NativeOcctHistoryExecution, NativeOcctHistoryInputTransport, NativeOcctHistoryProvider, NativeOcctHistoryProtocolResponse, NativeOcctHistoryRequest, NativeOcctHistoryStageTransport } from './nativeHistoryProtocol'
export { captureFreeCadPrivateNamingEvidence, createFreeCadPrivateNamingAbiRequest, FREECAD_PRIVATE_NAMING_ABI_VERSION, FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES, FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES, probeFreeCadPrivateNamingAbi } from './nativeNamingAbi'
export type { FreeCadPrivateNamingAbiDescriptor, FreeCadPrivateNamingAbiProbe, FreeCadPrivateNamingAbiRequest, NativeFreeCadNamingAbiModule } from './nativeNamingAbi'
export { assertNativeNamingEvidence, createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, hasNativeMappedNameEvidence, validateNativeNamingEvidence } from './nativeNamingEvidence'
export { assertNativeNamingEvidence, cloneNativeNamingEvidence, createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, hasNativeMappedNameEvidence, validateNativeNamingEvidence } from './nativeNamingEvidence'
export type { NativeMappedNameRef, NativeMappedNameRelation, NativeNamingEvidenceIssue, NativeNamingEvidenceReport, NativeNamingEvidenceStatus, NativeStageNamingEvidence } from './nativeNamingEvidence'
export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient'
export type { NativeOcctHistoryWorkerOptions } from './nativeHistoryWorkerClient'

View File

@@ -390,7 +390,7 @@ const expressionReferences = (expression: string): string[] => {
const createDocumentFromTemplate = (template: DocumentTemplate): DocumentSnapshot => {
const tree = template.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined }))
const objects = tree.map(createObjectSnapshot)
const objects = tree.map((item, index) => ({ ...createObjectSnapshot(item), nativeObjectTag: index + 1 }))
const document: DocumentSnapshot = { id: template.id, label: template.label, version: template.version, dirty: template.dirty, readOnly: template.readOnly, units: template.units, tree, objects }
document.dependencies = collectDependencyEdges(document)
document.recompute = createRecomputeSnapshot(objects.map((object) => object.id))
@@ -799,6 +799,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
tree.push(item)
}
const objectSnapshot = createObjectSnapshot(item)
objectSnapshot.nativeObjectTag = document.objects.reduce((maximum, object) => Math.max(maximum, object.nativeObjectTag ?? 0), 0) + 1
if (commandId === 'create-body') {
const tip = objectSnapshot.properties.find((property) => property.name === 'Tip')
if (tip) tip.value = null

View File

@@ -1,5 +1,6 @@
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse, NativeOcctHistoryStepProvider, NativeOcctMultiTransformStep } from './nativeHistoryProvider'
import { captureFreeCadPrivateNamingEvidence, createFreeCadPrivateNamingAbiRequest, probeFreeCadPrivateNamingAbi } from './nativeNamingAbi'
import type { NativeStageNamingEvidence } from './nativeNamingEvidence'
export const NATIVE_OCCT_HISTORY_PROTOCOL_VERSION = 1 as const
@@ -61,6 +62,8 @@ export type NativeOcctHistoryRequest = {
documentId: string
documentVersion: number
operationId: string
/** Document object produced by this native stage; distinct from the request operation ID. */
resultObjectId?: string
operation: NativeOcctHistoryOperation
transformKind?: 'linear' | 'polar' | 'mirrored'
transforms?: NativeOcctMultiTransformStep[]
@@ -70,6 +73,7 @@ export type NativeOcctHistoryRequest = {
inputs?: NativeOcctHistoryInputTransport[]
stages?: NativeOcctHistoryStageTransport[]
resultStepByStage?: Record<string, string>
resultObjectTag?: number
direction?: [number, number, number]
axisOrigin?: [number, number, number]
angle?: number
@@ -87,9 +91,13 @@ export type NativeOcctHistoryRequest = {
export type NativeOcctHistoryInputTransport = {
inputId: string
/** Stable document object identity represented by this transport input. */
objectId?: string
role?: string
stageId?: string
step: string
objectTag?: number
namingEvidence?: NativeStageNamingEvidence
}
export type NativeOcctHistoryStageTransport = {
@@ -129,6 +137,7 @@ const booleanOperations: readonly NativeOcctHistoryOperation[] = ['fuse', 'cut',
const assertRequest = (request: NativeOcctHistoryRequest) => {
if (request.protocolVersion !== NATIVE_OCCT_HISTORY_PROTOCOL_VERSION) throw new RangeError(`Unsupported native OCCT history protocol version: ${request.protocolVersion}.`)
if (request.resultObjectId !== undefined && !request.resultObjectId.trim()) throw new TypeError('Native OCCT history resultObjectId must be non-empty when provided.')
if (!request.requestId.trim() || !request.documentId.trim() || !request.operationId.trim()) throw new TypeError('Native OCCT history request IDs must be non-empty strings.')
if (!Number.isSafeInteger(request.documentVersion) || request.documentVersion < 0) throw new RangeError('Native OCCT history documentVersion must be a non-negative integer.')
if (!['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'].includes(request.operation)) throw new RangeError(`Unsupported native OCCT history operation: ${String(request.operation)}.`)
@@ -162,6 +171,11 @@ const assertRequest = (request: NativeOcctHistoryRequest) => {
if (!step.startsWith('ISO-10303-21;')) throw new TypeError(`Native OCCT result step for '${stageId}' requires STEP text.`)
}
}
if (request.resultObjectTag !== undefined && (!Number.isSafeInteger(request.resultObjectTag) || request.resultObjectTag <= 0)) throw new RangeError('Native OCCT history resultObjectTag must be a positive safe integer.')
for (const input of request.inputs ?? []) {
if (input.objectId !== undefined && !input.objectId.trim()) throw new TypeError(`Native OCCT history input ${input.inputId} objectId must be non-empty when provided.`)
if (input.objectTag !== undefined && (!Number.isSafeInteger(input.objectTag) || input.objectTag <= 0)) throw new RangeError(`Native OCCT history input ${input.inputId} objectTag must be a positive safe integer.`)
}
if (booleanOperations.includes(request.operation)) {
if (!request.toolStep?.startsWith('ISO-10303-21;')) throw new TypeError('Native Boolean history transport requires object and tool STEP text.')
} else if (request.operation === 'pocket') {
@@ -359,8 +373,9 @@ export class DirectNativeOcctHistoryProvider implements NativeOcctHistoryProvide
operationId: request.operationId,
operation: request.operation,
stageId,
resultObjectId: request.operationId,
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, role, stageId: inputStageId, step }) => ({ inputId, step, ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}) })),
resultObjectId: request.resultObjectId ?? request.operationId,
...(request.resultObjectTag === undefined ? {} : { resultObjectTag: request.resultObjectTag }),
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, objectId, role, stageId: inputStageId, step, objectTag, namingEvidence }) => ({ inputId, step, ...(objectId ? { objectId } : {}), ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}), ...(objectTag === undefined ? {} : { objectTag }), ...(namingEvidence ? { namingEvidence } : {}) })),
stages: orderedStages.map((stage) => ({ ...stage, inputIds: [...stage.inputIds] })),
...(stagedHistory.resultStep ? { resultStep: stagedHistory.resultStep } : {}),
...(stagedHistory.resultBrep ? { resultBrep: stagedHistory.resultBrep } : {}),

View File

@@ -94,8 +94,9 @@ scope.onmessage = ({ data }: MessageEvent<WorkerRequest>) => {
operationId: request.operationId,
operation: request.operation,
stageId,
resultObjectId: request.operationId,
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, role, stageId: inputStageId, step }) => ({ inputId, step, ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}) })),
resultObjectId: request.resultObjectId ?? request.operationId,
...(request.resultObjectTag === undefined ? {} : { resultObjectTag: request.resultObjectTag }),
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, objectId, role, stageId: inputStageId, step, objectTag, namingEvidence }) => ({ inputId, step, ...(objectId ? { objectId } : {}), ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}), ...(objectTag === undefined ? {} : { objectTag }), ...(namingEvidence ? { namingEvidence } : {}) })),
stages: orderedStages.map((stage) => ({ ...stage, inputIds: [...stage.inputIds] })),
...(history.resultStep ? { resultStep: history.resultStep } : {}),
...(history.resultBrep ? { resultBrep: history.resultBrep } : {}),

View File

@@ -1,5 +1,5 @@
import { migrateElementMap2Schema, validateElementMap2 } from './elementMap2'
import { assertNativeNamingEvidence, type NativeStageNamingEvidence } from './nativeNamingEvidence'
import { assertNativeNamingEvidence, cloneNativeNamingEvidence, type NativeStageNamingEvidence } from './nativeNamingEvidence'
import { migrateStringHasherSchema, validateElementMap2StringHasherEvidence, validateStringHasherTable } from './stringHasher'
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from './nativeHistoryProvider'
@@ -42,7 +42,8 @@ export type FreeCadPrivateNamingAbiRequest = {
operation: NativeOcctHistoryOperation
stageId: string
resultObjectId: string
inputs: Array<{ inputId: string; role?: string; stageId?: string; step: string }>
resultObjectTag?: number
inputs: Array<{ inputId: string; objectId?: string; role?: string; stageId?: string; step: string; objectTag?: number; namingEvidence?: NativeStageNamingEvidence }>
stages: Array<{ stageId: string; operation?: NativeOcctHistoryOperation; inputIds: string[]; resultStageId?: string; ordinal: number }>
resultStep?: string
resultBrep?: string
@@ -91,12 +92,15 @@ export const probeFreeCadPrivateNamingAbi = (module: NativeFreeCadNamingAbiModul
export const createFreeCadPrivateNamingAbiRequest = (input: Omit<FreeCadPrivateNamingAbiRequest, 'schemaVersion'>): FreeCadPrivateNamingAbiRequest => {
if (!input.requestId.trim() || !input.documentId.trim() || !input.operationId.trim() || !input.stageId.trim() || !input.resultObjectId.trim()) throw new TypeError('FreeCAD naming ABI request IDs must be non-empty.')
if (!Number.isSafeInteger(input.documentVersion) || input.documentVersion < 0) throw new RangeError('FreeCAD naming ABI documentVersion must be a non-negative integer.')
if (input.resultObjectTag !== undefined && (!Number.isSafeInteger(input.resultObjectTag) || input.resultObjectTag <= 0)) throw new RangeError('FreeCAD naming ABI resultObjectTag must be a positive safe integer.')
return {
schemaVersion: 1,
...input,
inputs: input.inputs.map((entry) => {
if (!entry.inputId.trim() || !entry.step.trim()) throw new TypeError('FreeCAD naming ABI inputs require non-empty inputId and STEP text.')
return { ...entry }
if (entry.objectId !== undefined && !entry.objectId.trim()) throw new TypeError(`FreeCAD naming ABI input ${entry.inputId} objectId must be non-empty when provided.`)
if (entry.objectTag !== undefined && (!Number.isSafeInteger(entry.objectTag) || entry.objectTag <= 0)) throw new RangeError(`FreeCAD naming ABI input ${entry.inputId} objectTag must be a positive safe integer.`)
return { ...entry, ...(entry.namingEvidence ? { namingEvidence: cloneNativeNamingEvidence(assertNativeNamingEvidence(entry.namingEvidence)) } : {}) }
}),
stages: input.stages.map((entry) => ({ ...entry, inputIds: [...entry.inputIds] })),
...(input.resultStepByStage ? { resultStepByStage: { ...input.resultStepByStage } } : {}),

View File

@@ -1,5 +1,5 @@
import type { AnyElementMap2Document, ElementMap2MappedNameReference } from './elementMap2'
import { migrateStringHasherSchema, validateStringHasherTable, type AnyStringHasherTable } from './stringHasher'
import { cloneElementMap2, migrateElementMap2Schema, type AnyElementMap2Document, type ElementMap2MappedNameReference } from './elementMap2'
import { cloneStringHasherTable, migrateStringHasherSchema, validateStringHasherTable, type AnyStringHasherTable } from './stringHasher'
export type NativeMappedNameRelation = 'preserved' | 'modified' | 'generated' | 'deleted' | 'ambiguous'
@@ -119,6 +119,22 @@ export const assertNativeNamingEvidence = <T extends NativeStageNamingEvidence>(
return evidence
}
export const cloneNativeNamingEvidence = (evidence: NativeStageNamingEvidence): NativeStageNamingEvidence => ({
...evidence,
mappedNames: evidence.mappedNames?.map((mapped) => ({
...mapped,
reference: {
...mapped.reference,
indexedName: mapped.reference.indexedName ? { ...mapped.reference.indexedName } : undefined,
stringIds: mapped.reference.stringIds ? [...mapped.reference.stringIds] : undefined,
},
sourceRefs: mapped.sourceRefs?.map((source) => ({ ...source })),
candidates: mapped.candidates?.map((candidate) => ({ ...candidate })),
})),
stringHasher: evidence.stringHasher ? cloneStringHasherTable(evidence.stringHasher) : undefined,
elementMap2: evidence.elementMap2 ? cloneElementMap2(migrateElementMap2Schema(evidence.elementMap2)) : undefined,
})
export const createFinalShapeOnlyNamingEvidence = (stageId: string, resultObjectId: string, reason = 'Native provider returned final Shape without MappedNameRef/StringHasher evidence.'): NativeStageNamingEvidence => ({
schemaVersion: 1,
stageId,

View File

@@ -90,6 +90,13 @@ const initialize = async (requestedPath?: string, migrationFailureVersion?: numb
const saveDocument = (document: DocumentSnapshot) => {
if (!database) throw new Error('Persistence database is not initialized.')
const nativeObjectTags = new Set<number>()
for (const object of document.objects) {
if (object.nativeObjectTag === undefined) continue
if (!Number.isSafeInteger(object.nativeObjectTag) || object.nativeObjectTag <= 0) throw new RangeError(`Object ${object.id} nativeObjectTag must be a positive safe integer.`)
if (nativeObjectTags.has(object.nativeObjectTag)) throw new RangeError(`nativeObjectTag ${object.nativeObjectTag} is assigned to more than one object.`)
nativeObjectTags.add(object.nativeObjectTag)
}
const now = Date.now()
database.exec('BEGIN;')
try {
@@ -98,7 +105,7 @@ const saveDocument = (document: DocumentSnapshot) => {
database.exec({ sql: 'DELETE FROM objects WHERE document_id = ?', bind: [document.id] })
const parentByChild = new Map<string, string>()
for (const item of document.tree) for (const childId of item.children || []) parentByChild.set(childId, item.id)
document.tree.forEach((item, ordinal) => { const object = document.objects.find((candidate) => candidate.id === item.id); database?.exec({ sql: 'INSERT INTO objects(id, document_id, parent_id, label, object_type, state, detail, children_json, ordinal, sketch_json, topology_json) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', bind: [item.id, document.id, parentByChild.get(item.id) || null, item.label, item.type, item.state || null, item.detail || null, JSON.stringify(item.children || []), ordinal, object?.sketch ? JSON.stringify(object.sketch) : null, object?.topology ? JSON.stringify(object.topology) : null] }) })
document.tree.forEach((item, ordinal) => { const object = document.objects.find((candidate) => candidate.id === item.id); database?.exec({ sql: 'INSERT INTO objects(id, document_id, parent_id, label, object_type, state, detail, children_json, ordinal, sketch_json, topology_json, native_object_tag) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', bind: [item.id, document.id, parentByChild.get(item.id) || null, item.label, item.type, item.state || null, item.detail || null, JSON.stringify(item.children || []), ordinal, object?.sketch ? JSON.stringify(object.sketch) : null, object?.topology ? JSON.stringify(object.topology) : null, object?.nativeObjectTag ?? null] }) })
for (const object of document.objects) for (const property of object.properties) database.exec({ sql: 'INSERT INTO object_properties(document_id, object_id, name, value_json, property_type, updated_at) VALUES(?, ?, ?, ?, ?, ?)', bind: [document.id, object.id, property.name, JSON.stringify(property), property.type, now] })
database.exec({ sql: 'DELETE FROM dependencies WHERE document_id = ?', bind: [document.id] })
for (const edge of document.dependencies ?? []) database.exec({ sql: 'INSERT INTO dependencies(document_id, source_id, target_id, relation, property_name, reference) VALUES(?, ?, ?, ?, ?, ?)', bind: [document.id, edge.sourceId, edge.targetId, edge.relation, edge.propertyName ?? null, edge.reference ?? null] })
@@ -132,7 +139,7 @@ const loadDocument = (documentId: string): DocumentSnapshot | null => {
const documents = database.exec({ sql: 'SELECT id, label, version, dirty, read_only, units, recompute_json FROM documents WHERE id = ?', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number | null>>
const row = documents[0]
if (!row) return null
const objects = database.exec({ sql: 'SELECT id, label, object_type, state, detail, children_json, sketch_json, topology_json FROM objects WHERE document_id = ? ORDER BY ordinal', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number | null>>
const objects = database.exec({ sql: 'SELECT id, label, object_type, state, detail, children_json, sketch_json, topology_json, native_object_tag FROM objects WHERE document_id = ? ORDER BY ordinal', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number | null>>
const propertyRows = database.exec({ sql: 'SELECT object_id, value_json FROM object_properties WHERE document_id = ? ORDER BY object_id, name', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string>>
const dependencyRows = database.exec({ sql: 'SELECT source_id, target_id, relation, property_name, reference FROM dependencies WHERE document_id = ?', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | null>>
const propertiesByObject = new Map<string, ObjectPropertySnapshot[]>()
@@ -142,11 +149,29 @@ const loadDocument = (documentId: string): DocumentSnapshot | null => {
propertiesByObject.set(String(propertyRow.object_id), properties)
}
const tree: ModelTreeItem[] = objects.map((object) => ({ id: String(object.id), label: String(object.label), type: String(object.object_type) as ModelTreeItem['type'], state: object.state ? String(object.state) as ModelTreeItem['state'] : undefined, detail: object.detail ? String(object.detail) : undefined, children: object.children_json ? JSON.parse(String(object.children_json)) as string[] : undefined }))
const usedNativeObjectTags = new Set<number>()
const persistedNativeObjectTags = new Map<string, number>()
for (const object of objects) {
if (object.native_object_tag === null || object.native_object_tag === undefined) continue
const nativeObjectTag = Number(object.native_object_tag)
if (!Number.isSafeInteger(nativeObjectTag) || nativeObjectTag <= 0) throw new Error(`Persisted object ${String(object.id)} has an invalid native object tag.`)
if (usedNativeObjectTags.has(nativeObjectTag)) throw new Error(`Persisted native object tag ${nativeObjectTag} is duplicated.`)
usedNativeObjectTags.add(nativeObjectTag)
persistedNativeObjectTags.set(String(object.id), nativeObjectTag)
}
let nextNativeObjectTag = 1
const objectSnapshots: DocumentObjectSnapshot[] = tree.map((item) => {
const properties = propertiesByObject.get(item.id) ?? []
const typeId = properties.find((property) => property.name === 'TypeId')?.value
const row = objects.find((candidate) => String(candidate.id) === item.id)
return { id: item.id, typeId: typeof typeId === 'string' ? typeId : item.type, properties, sketch: row?.sketch_json ? JSON.parse(String(row.sketch_json)) : undefined, topology: row?.topology_json ? JSON.parse(String(row.topology_json)) : undefined }
let nativeObjectTag = persistedNativeObjectTags.get(item.id)
if (nativeObjectTag === undefined) {
while (usedNativeObjectTags.has(nextNativeObjectTag)) nextNativeObjectTag += 1
nativeObjectTag = nextNativeObjectTag
usedNativeObjectTags.add(nativeObjectTag)
nextNativeObjectTag += 1
}
return { id: item.id, typeId: typeof typeId === 'string' ? typeId : item.type, nativeObjectTag, properties, sketch: row?.sketch_json ? JSON.parse(String(row.sketch_json)) : undefined, topology: row?.topology_json ? JSON.parse(String(row.topology_json)) : undefined }
})
return {
id: String(row.id),

View File

@@ -1,4 +1,4 @@
export const PROJECT_SCHEMA_VERSION = 6
export const PROJECT_SCHEMA_VERSION = 7
export type ProjectSchemaMigration = { version: number; sql: string }
@@ -141,4 +141,5 @@ export const PROJECT_SCHEMA_MIGRATIONS = [
{ version: 4, sql: 'ALTER TABLE objects ADD COLUMN sketch_json TEXT;' },
{ version: 5, sql: 'CREATE TABLE IF NOT EXISTS document_checkpoints (document_id TEXT NOT NULL, version INTEGER NOT NULL, snapshot_json TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (document_id, version), FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE); CREATE INDEX IF NOT EXISTS checkpoints_document_created ON document_checkpoints(document_id, created_at DESC);' },
{ version: 6, sql: 'ALTER TABLE objects ADD COLUMN topology_json TEXT;' },
{ version: 7, sql: 'ALTER TABLE objects ADD COLUMN native_object_tag INTEGER;' },
] as const

View File

@@ -580,8 +580,16 @@ const topologyForObject = async (
documentVersion: context.documentVersion,
operationId,
operation: nativeOperation,
inputs: orderedNativeInputs.map(({ source, shape: inputShape }, index) => ({ objectId: source.id, shape: inputShape, inputId: `${object.id}:input:${index}`, role: index === 0 ? 'object' : 'tool' })),
inputs: orderedNativeInputs.map(({ source, shape: inputShape }, index) => ({
objectId: source.id,
shape: inputShape,
inputId: `${object.id}:input:${index}`,
role: index === 0 ? 'object' : 'tool',
...(source.nativeObjectTag === undefined ? {} : { objectTag: source.nativeObjectTag }),
namingEvidence: source.topology?.history?.namingEvidence?.at(-1),
})),
stages: [{ stageId: `${object.id}:stage:0`, operation: nativeOperation, inputObjectIds: orderedNativeInputs.map(({ source }) => source.id), resultObjectId: object.id, ordinal: 0 }],
...(object.nativeObjectTag === undefined ? {} : { resultObjectTag: object.nativeObjectTag }),
...(nativeOperation === 'fillet' ? { radius: numericProperty('Radius', 1) } : {}),
...(nativeOperation === 'chamfer' ? { distance: numericProperty('Size', numericProperty('Distance', 1)) } : {}),
...(nativeOperation === 'hole' ? {

View File

@@ -108,6 +108,8 @@ export type ObjectPropertySnapshot = {
export type DocumentObjectSnapshot = {
id: string
typeId: string
/** Stable positive App::DocumentObject tag persisted by FreeCAD's Document.xml. */
nativeObjectTag?: number
properties: ObjectPropertySnapshot[]
sketch?: SketchSnapshot
topology?: ObjectTopologySnapshot
@@ -407,8 +409,10 @@ export type NativeTopologyHistoryInput = GeometryDocumentContext & {
ruled?: boolean
offset?: number
joinType?: 'Arc' | 'Intersection'
inputs: Array<{ objectId: string; shape: ShapeHandle; inputId?: string; role?: string; stageId?: string }>
inputs: Array<{ objectId: string; shape: ShapeHandle; inputId?: string; role?: string; stageId?: string; objectTag?: number; namingEvidence?: NativeStageNamingEvidence }>
stages?: NativeTopologyHistoryStage[]
/** Stable FreeCAD owner tag used when encoding the result ElementMap. */
resultObjectTag?: number
result: ShapeHandle
}