feat: complete supported Sketcher and PartDesign ABI contract
This commit is contained in:
@@ -17,9 +17,11 @@ import { runTopologyMutationReplay } from '../src/facade/topologyReplay'
|
||||
import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory'
|
||||
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
|
||||
import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, nativeNamingAbiCapabilities, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol'
|
||||
import { probeFreeCadPrivateNamingAbi } from '../src/facade/nativeNamingAbi'
|
||||
import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient'
|
||||
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
|
||||
import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher'
|
||||
import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, FREECAD_SKETCHER_CONSTRAINT_TYPES, FREECAD_SKETCHER_GEOMETRY_TYPES, FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher'
|
||||
import { PARTDESIGN_PARAMETER_SPACE, partDesignParameterSpaceCoverage, validatePartDesignParameterSet } from '../src/facade/partDesignParameters'
|
||||
import { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay, type SketchSolverProvider, type SketchSolverRequest } from '../src/facade/sketchSolverProtocol'
|
||||
import { solvePlanegcsSubset, type PlanegcsWasmModule } from '../src/facade/planegcsAdapter'
|
||||
import { PlanegcsWorkerProvider } from '../src/facade/planegcsWorkerClient'
|
||||
@@ -87,7 +89,7 @@ test('production facade starts from an empty document unless an application boot
|
||||
assert.deepEqual(state.selectedObjectIds, [])
|
||||
assert.equal(facade.runtime.mode, 'production')
|
||||
assert.equal(facade.runtime.geometry.compatibility, 'none')
|
||||
assert.equal(facade.runtime.naming.nativeAbi, 'not-exposed')
|
||||
assert.equal(facade.runtime.naming.nativeAbi, 'freecad-private-v1-optional-worker')
|
||||
facade.geometry.dispose()
|
||||
})
|
||||
|
||||
@@ -684,7 +686,7 @@ test('native OCCT history protocol validates STEP context and isolates stale gen
|
||||
resultStep: objectStep, records: [{ relation: operation === 'cut' ? 'modified' : 'generated', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
|
||||
}),
|
||||
})
|
||||
assert.deepEqual(provider.capabilities(), { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'], transport: 'step-text' })
|
||||
assert.deepEqual(provider.capabilities(), { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'], transport: 'step-text', naming: { mappedNameRef: 'unavailable', stringHasher: 'unavailable', elementMap2: 'unavailable', tokenGeneration: 'forbidden', reason: 'Native module does not export the versioned FreeCAD naming callbacks.' } })
|
||||
const coordinator = new NativeOcctHistoryCoordinator(() => 2)
|
||||
const execution = await coordinator.capture(provider, { documentId: 'doc', documentVersion: 2, operationId: 'cut-1', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||
assert.equal(execution.status, 'completed')
|
||||
@@ -699,8 +701,38 @@ test('native OCCT history protocol validates STEP context and isolates stale gen
|
||||
|
||||
test('native naming ABI capabilities forbid synthetic FreeCAD tokens unless a provider declares native evidence', () => {
|
||||
const base = { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available' as const, operations: ['cut' as const], transport: 'step-text' as const }
|
||||
assert.deepEqual(nativeNamingAbiCapabilities(base), { mappedNameRef: 'unavailable', stringHasher: 'unavailable', tokenGeneration: 'forbidden' })
|
||||
assert.deepEqual(nativeNamingAbiCapabilities({ ...base, naming: { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', tokenGeneration: 'native-only' } }), { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', tokenGeneration: 'native-only' })
|
||||
assert.deepEqual(nativeNamingAbiCapabilities(base), { mappedNameRef: 'unavailable', stringHasher: 'unavailable', elementMap2: 'unavailable', tokenGeneration: 'forbidden' })
|
||||
assert.deepEqual(nativeNamingAbiCapabilities({ ...base, naming: { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' } }), { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' })
|
||||
})
|
||||
|
||||
test('versioned FreeCAD private naming ABI is capability-gated and attaches validated native evidence', async () => {
|
||||
const elementMap2 = parseElementMap2([
|
||||
'BeginElementMap v1', '1 PostfixCount 1', 'Edge', 'MapCount 1', 'ElementMap 1 1 1', 'Edge',
|
||||
'ChildCount 1', '1 0 1 0 0 Edge 0.54', 'NameCount 1', '$#36:2.1.37 0', 'EndMap', '',
|
||||
].join('\n'))
|
||||
const stringHasher = parseStringHasherTable(['StringTableStart v1 2', '-36.0 0:prefix', '-1.0 0:suffix', ''].join('\n'))
|
||||
let capturedRequest: Record<string, unknown> | undefined
|
||||
const module = {
|
||||
occtVersion: () => '8.0.0',
|
||||
booleanHistoryFromStep: (objectStep: string) => ({ provider: 'occt-native' as const, occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, resultStep: objectStep, records: [{ relation: 'modified' as const, source: 'object' as const, kind: 'edge' as const, sourceIndex: 0, resultIndex: 0 }] }),
|
||||
freecadNamingAbiVersion: () => 1,
|
||||
freecadNamingCapabilitiesJson: () => JSON.stringify({ schemaVersion: 1, freecadVersion: '1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', mappedNameRef: true, stringHasher: true, elementMap2: true, operations: ['cut'] }),
|
||||
freecadNamingEvidenceJson: (requestJson: string) => {
|
||||
capturedRequest = JSON.parse(requestJson) as Record<string, unknown>
|
||||
return JSON.stringify({ schemaVersion: 1, stageId: 'cut-native:stage:0', resultObjectId: 'cut-native', status: 'native-evidence', mappedNames: [{ kind: 'edge', resultIndex: 0, resultPersistentId: 'edge-native-0', relation: 'modified', reference: { name: 'Edge1', stringIds: [0x36] }, sourceRefs: [{ objectId: 'object', persistentId: 'Edge1' }] }], stringHasher, elementMap2 })
|
||||
},
|
||||
}
|
||||
assert.deepEqual(probeFreeCadPrivateNamingAbi(module).availability, 'available')
|
||||
const provider = new DirectNativeOcctHistoryProvider(module)
|
||||
assert.deepEqual(provider.capabilities().naming, { mappedNameRef: 'available', stringHasher: 'available', elementMap2: 'available', tokenGeneration: 'native-only', abiVersion: 1, freecadVersion: '1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', operations: ['cut'] })
|
||||
const response = await provider.capture({ protocolVersion: 1, requestId: 'naming-1', documentId: 'doc', documentVersion: 2, operationId: 'cut-native', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' }, new AbortController().signal)
|
||||
assert.equal(response.history.namingEvidence?.status, 'native-evidence')
|
||||
assert.equal(response.history.namingEvidence?.mappedNames?.[0].resultPersistentId, 'edge-native-0')
|
||||
assert.equal(capturedRequest?.operation, 'cut')
|
||||
assert.equal((capturedRequest?.history as { records: unknown[] }).records.length, 1)
|
||||
|
||||
const invalid = { ...module, freecadNamingCapabilitiesJson: () => JSON.stringify({ schemaVersion: 1, freecadVersion: '1.1.1', sourceCommit: 'wrong', mappedNameRef: true, stringHasher: true, elementMap2: true, operations: ['cut'] }) }
|
||||
assert.equal(probeFreeCadPrivateNamingAbi(invalid).availability, 'unavailable')
|
||||
})
|
||||
|
||||
test('native OCCT history coordinator reports timeout and cancellation', async () => {
|
||||
@@ -713,6 +745,27 @@ test('native OCCT history coordinator reports timeout and cancellation', async (
|
||||
assert.equal((await pending).status, 'cancelled')
|
||||
})
|
||||
|
||||
test('Sketcher and supported PartDesign parameter contracts cover every declared native partition', () => {
|
||||
assert.deepEqual(FREECAD_SKETCHER_GEOMETRY_TYPES, ['point', 'line', 'arc', 'circle', 'ellipse', 'arcEllipse', 'arcHyperbola', 'arcParabola', 'bspline'])
|
||||
assert.equal(FREECAD_SKETCHER_CONSTRAINT_TYPES.length, 19)
|
||||
assert.equal(FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES.length, 10)
|
||||
assert.equal(new Set(FREECAD_SKETCHER_CONSTRAINT_TYPES).size, FREECAD_SKETCHER_CONSTRAINT_TYPES.length)
|
||||
assert.equal(new Set(FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES).size, FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES.length)
|
||||
assert.deepEqual(partDesignParameterSpaceCoverage(), { schemaVersion: 1, baseline: 'FreeCAD 1.1.1', families: 21, properties: 202, partitions: 72, duplicateTypeIds: 0, familiesWithoutPartitions: [] })
|
||||
for (const family of PARTDESIGN_PARAMETER_SPACE) {
|
||||
assert.equal(new Set(family.properties).size, family.properties.length, `${family.typeId} has duplicate properties`)
|
||||
assert.equal(new Set(family.partitions.map((entry) => entry.id)).size, family.partitions.length, `${family.typeId} has duplicate partitions`)
|
||||
for (const partition of family.partitions) for (const propertyName of Object.keys(partition.values)) assert.ok(family.properties.includes(propertyName), `${family.typeId}.${partition.id} uses undeclared ${propertyName}`)
|
||||
}
|
||||
|
||||
const validPad = validatePartDesignParameterSet('PartDesign::Pad', { Profile: 'Sketch', Length: 10, Length2: 5, Type: 'TwoLengths', Type2: 'Dimension', SideType: 'Two sides', UpToFace: null, UpToFace2: null, TaperAngle: 0, TaperAngle2: 0, Reversed: false, Midplane: false, ReferenceAxis: null, AlongSketchNormal: true, UseCustomVector: false, Direction: { x: 0, y: 0, z: 1 }, Offset: 0, Offset2: 0 })
|
||||
assert.equal(validPad.valid, true)
|
||||
assert.equal(validatePartDesignParameterSet('PartDesign::Pad', { ...Object.fromEntries(PARTDESIGN_PARAMETER_SPACE.find((entry) => entry.typeId === 'PartDesign::Pad')!.properties.map((name) => [name, null])), Type: 'TwoLengths', SideType: 'Two sides', Length: 10, Length2: 5, Midplane: true, TaperAngle: 0, TaperAngle2: 0 }).valid, false)
|
||||
assert.equal(validatePartDesignParameterSet('PartDesign::Hole', { Diameter: 5, DepthType: 'ThroughAll', Direction: { x: 0, y: 0, z: 1 }, HoleCutType: 'None', Threaded: false, ModelThread: true }, { requireComplete: false }).issues.some((issue) => issue.propertyName === 'ModelThread'), true)
|
||||
assert.equal(validatePartDesignParameterSet('PartDesign::Draft', { Base: 'Pad', Angle: 5, Reversed: false }, { requireComplete: false }).valid, true)
|
||||
assert.equal(validatePartDesignParameterSet('PartDesign::LinearPattern', { Originals: ['Pad'], Occurrences: 2, Mode: 'Extent', Length: 10, Direction: 'Horizontal' }, { requireComplete: false }).valid, true)
|
||||
})
|
||||
|
||||
test('native OCCT Worker provider preserves versioned request and response context', async () => {
|
||||
const messageListeners = new Set<(event: MessageEvent) => void>()
|
||||
const errorListeners = new Set<(event: ErrorEvent) => void>()
|
||||
@@ -4666,6 +4719,33 @@ test('FCStd Sketcher codec round-trips ellipse and B-spline knot internal geomet
|
||||
assert.throws(() => serializeFcstdMetadataArchive({ ...document, objects: [{ ...document.objects[0], sketch: invalidEllipseIndex }] }), /invalid ellipse internal index/)
|
||||
})
|
||||
|
||||
test('FCStd Sketcher codec round-trips all conic arc geometry and private internal alignments', () => {
|
||||
const sketch = createSketch('Sketch', [
|
||||
{ id: 'arc-ellipse', type: 'arcEllipse', center: { x: 1, y: 2 }, majorRadius: 4, minorRadius: 2, rotation: 0.2, startAngle: -1, endAngle: 1 },
|
||||
{ id: 'arc-hyperbola', type: 'arcHyperbola', center: { x: 8, y: 2 }, majorRadius: 3, minorRadius: 1.5, rotation: 0.4, startAngle: -0.8, endAngle: 0.8 },
|
||||
{ id: 'arc-parabola', type: 'arcParabola', center: { x: 14, y: 2 }, focal: 2, rotation: 0.6, startAngle: -1.2, endAngle: 1.2 },
|
||||
], [
|
||||
{ id: 'ellipse-major', type: 'internalAlignment', geometryId: 'arc-ellipse', internalGeometryIndex: 0, alignmentType: 'ellipse-major' },
|
||||
{ id: 'hyperbola-major', type: 'internalAlignment', geometryId: 'arc-hyperbola', internalGeometryIndex: 0, alignmentType: 'hyperbola-major' },
|
||||
{ id: 'hyperbola-minor', type: 'internalAlignment', geometryId: 'arc-hyperbola', internalGeometryIndex: 0, alignmentType: 'hyperbola-minor' },
|
||||
{ id: 'hyperbola-focus', type: 'internalAlignment', geometryId: 'arc-hyperbola', internalGeometryIndex: 0, alignmentType: 'hyperbola-focus' },
|
||||
{ id: 'parabola-focus', type: 'internalAlignment', geometryId: 'arc-parabola', internalGeometryIndex: 0, alignmentType: 'parabola-focus' },
|
||||
{ id: 'parabola-axis', type: 'internalAlignment', geometryId: 'arc-parabola', internalGeometryIndex: 0, alignmentType: 'parabola-focal-axis' },
|
||||
])
|
||||
const document = recomputeDocumentFixture()
|
||||
document.objects = [{ id: 'Sketch', typeId: 'Sketcher::SketchObject', properties: [], sketch }]
|
||||
const archive = serializeFcstdMetadataArchive(document)
|
||||
const written = new TextDecoder().decode(unzipSync(archive)['Document.xml'])
|
||||
assert.match(written, /type="Part::GeomArcOfEllipse"/)
|
||||
assert.match(written, /type="Part::GeomArcOfHyperbola"/)
|
||||
assert.match(written, /type="Part::GeomArcOfParabola"/)
|
||||
assert.deepEqual([...written.matchAll(/InternalAlignmentType="(\d+)"/g)].map((match) => Number(match[1])), [1, 5, 6, 7, 8, 11])
|
||||
const restored = inspectFcstdArchive(archive).objects[0].sketch
|
||||
assert.ok(restored)
|
||||
assert.deepEqual(restored.geometry, sketch.geometry)
|
||||
assert.deepEqual(restored.constraints, sketch.constraints.map((constraint) => ({ ...constraint, driving: true })))
|
||||
})
|
||||
|
||||
test('FCStd Sketcher codec round-trips native external Edge/Vertex projections and stable TopoRefs', () => {
|
||||
const sketch = createSketch('Sketch', [{ id: 'profile', type: 'line', start: { x: 0, y: 0 }, end: { x: 2, y: 0 } }])
|
||||
const edgeSource = { schemaVersion: 1 as const, objectId: 'Source', kind: 'edge' as const, persistentId: 'Edge1', topologyVersion: 7, generation: 4, status: 'stable' as const, signature: 'edge-signature', candidates: ['Edge1'] }
|
||||
|
||||
Reference in New Issue
Block a user