P5: add facade sketch model solver and persistence

This commit is contained in:
2026-08-02 10:10:22 -04:00
parent 6a0bd534ea
commit 6a1bd18384
11 changed files with 334 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ import { PersistenceWriteQueue, ProjectAutosaveScheduler } from '../src/facade/p
import { DependencyGraph } from '../src/facade/dependencyGraph'
import { evaluateQuantityExpression, quantityFromNumber } from '../src/facade/units'
import { createSubshapeRefs, matchSubshapes, signatureForFace } from '../src/facade/topologyNaming'
import { createSketch, solveSketch } from '../src/facade/sketcher'
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateConeInput, validateCylinderInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
import type { ShapeHandle } from '../src/facade/types'
@@ -74,8 +75,8 @@ test('Bitbybit face meshes are normalized into a facade-owned indexed asset', ()
})
test('project schema is versioned and covers the FreeCAD document graph', () => {
assert.equal(PROJECT_SCHEMA_VERSION, 3)
assert.deepEqual(PROJECT_SCHEMA_MIGRATIONS.map((migration) => migration.version), [1, 2, 3])
assert.equal(PROJECT_SCHEMA_VERSION, 4)
assert.deepEqual(PROJECT_SCHEMA_MIGRATIONS.map((migration) => migration.version), [1, 2, 3, 4])
for (const table of ['projects', 'documents', 'objects', 'object_properties', 'dependencies', 'transactions', 'resources']) assert.match(PROJECT_SCHEMA_SQL, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`))
assert.match(PROJECT_SCHEMA_SQL, /CREATE INDEX IF NOT EXISTS objects_document_ordinal/)
})
@@ -110,6 +111,40 @@ test('topology signatures are independent of transient face indexes and flag amb
assert.equal(matches[0].previousId, first.refs[0].persistentId)
})
test('basic sketch solver applies geometric constraints and reports remaining degrees of freedom', () => {
const sketch = createSketch('sketch-test', [{ id: 'line-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 0 } }], [
{ id: 'horizontal-1', type: 'horizontal', geometryId: 'line-1' },
{ id: 'length-1', type: 'distance', first: { geometryId: 'line-1', point: 'start' }, second: { geometryId: 'line-1', point: 'end' }, value: 5 },
])
const result = solveSketch(sketch)
assert.equal(result.status, 'under-constrained')
assert.ok(result.residual <= 1e-7)
assert.equal(result.snapshot.geometry[0].type, 'line')
assert.equal((result.snapshot.geometry[0] as Extract<typeof result.snapshot.geometry[number], { type: 'line' }>).end.x, 5)
const conflict = solveSketch(createSketch('conflict', [{ id: 'line-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 1 } }], [
{ id: 'block-1', type: 'block', geometryId: 'line-1' },
{ id: 'horizontal-1', type: 'horizontal', geometryId: 'line-1' },
]))
assert.equal(conflict.status, 'conflicting')
assert.ok(conflict.diagnostics.some((diagnostic) => diagnostic.code === 'SOLVER_NOT_CONVERGED'))
})
test('sketcher operations are facade transactions and survive the project fallback store', async () => {
const facade = createMockFacade()
const before = facade.app.document.getActive().version
const added = facade.app.sketcher.addGeometry('sketch', { id: 'line-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 0 } })
assert.equal(added.geometry.length, 1)
facade.app.sketcher.addConstraint('sketch', { id: 'horizontal-1', type: 'horizontal', geometryId: 'line-1' })
const solved = facade.app.sketcher.solve('sketch')
assert.equal(solved.status, 'under-constrained')
assert.ok(facade.app.document.getActive().version >= before + 3)
assert.match(String(facade.app.document.getObject('sketch')?.properties.find((property) => property.name === 'ConstraintStatus')?.value), /Under-constrained/)
await facade.project.save()
const loaded = await facade.project.load('doc-pump-housing')
assert.equal(loaded?.objects.find((object) => object.id === 'sketch')?.sketch?.constraints[0].id, 'horizontal-1')
})
test('dependency graph propagates dirty state and orders dependencies', () => {
const graph = new DependencyGraph([
{ sourceId: 'pocket', targetId: 'pad', relation: 'link' },