P3-04: add document-scoped OCCT booleans

This commit is contained in:
2026-08-02 07:19:22 -04:00
parent 9e08f99429
commit 9dc5cfe3fd
9 changed files with 174 additions and 43 deletions

View File

@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import { createMockFacade } from '../src/facade/mockFacade'
import { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION } from '../src/facade/projectSchema'
import { PersistenceWriteQueue, ProjectAutosaveScheduler } from '../src/facade/projectStore'
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBoxInput, validateConeInput, validateCylinderInput, validatePlacementInput, validateSphereInput } from '../src/facade/geometryRuntime'
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateConeInput, validateCylinderInput, validatePlacementInput, validateSphereInput } from '../src/facade/geometryRuntime'
import type { ShapeHandle } from '../src/facade/types'
test('facade exposes a stable initial document projection', () => {
@@ -17,24 +17,34 @@ test('facade exposes a stable initial document projection', () => {
})
test('geometry boundary validates dimensions before invoking OCCT', () => {
assert.throws(() => validateBoxInput({ width: 0, length: 2, height: 3, documentVersion: 1 }), /width/)
assert.throws(() => validateBoxInput({ width: 1, length: 2, height: 3, center: [0, Number.NaN, 0], documentVersion: 1 }), /center/)
assert.doesNotThrow(() => validateBoxInput({ width: 1, length: 2, height: 3, documentVersion: 0 }))
assert.throws(() => validateCylinderInput({ radius: 1, height: 2, direction: [0, 0, 0], documentVersion: 1 }), /direction/)
assert.throws(() => validateSphereInput({ radius: -1, documentVersion: 1 }), /radius/)
assert.throws(() => validateConeInput({ radius1: 0, radius2: 0, height: 2, documentVersion: 1 }), /radius/)
assert.throws(() => validatePlacementInput({ shape: { id: 'shape', kernel: 'bitbybit-occt', kind: 'solid', documentVersion: 1 }, placement: { translation: [0, 0, 0], rotationAxis: [0, 0, 0], rotationAngle: 0 }, documentVersion: 2 }), /rotationAxis/)
assert.throws(() => validateBoxInput({ width: 0, length: 2, height: 3, documentId: 'doc', documentVersion: 1 }), /width/)
assert.throws(() => validateBoxInput({ width: 1, length: 2, height: 3, center: [0, Number.NaN, 0], documentId: 'doc', documentVersion: 1 }), /center/)
assert.doesNotThrow(() => validateBoxInput({ width: 1, length: 2, height: 3, documentId: 'doc', documentVersion: 0 }))
assert.throws(() => validateCylinderInput({ radius: 1, height: 2, direction: [0, 0, 0], documentId: 'doc', documentVersion: 1 }), /direction/)
assert.throws(() => validateSphereInput({ radius: -1, documentId: 'doc', documentVersion: 1 }), /radius/)
assert.throws(() => validateConeInput({ radius1: 0, radius2: 0, height: 2, documentId: 'doc', documentVersion: 1 }), /radius/)
assert.throws(() => validatePlacementInput({ shape: { id: 'shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 }, placement: { translation: [0, 0, 0], rotationAxis: [0, 0, 0], rotationAngle: 0 }, documentId: 'doc', documentVersion: 2 }), /rotationAxis/)
})
test('geometry handles reject forged document ownership', () => {
const expected: ShapeHandle = { id: 'shape-one', kernel: 'bitbybit-occt', kind: 'solid', documentVersion: 4 }
const expected: ShapeHandle = { id: 'shape-one', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-one', documentVersion: 4 }
assert.doesNotThrow(() => assertShapeHandleIntegrity({ ...expected }, expected))
assert.throws(() => assertShapeHandleIntegrity({ ...expected, documentVersion: 5 }, expected), /integrity/)
assert.throws(() => assertShapeHandleIntegrity({ ...expected, id: 'shape-two' }, expected), /integrity/)
assert.throws(() => assertShapeHandleIntegrity({ ...expected, documentId: 'doc-two' }, expected), /integrity/)
})
test('boolean geometry rejects cross-document and future-version operands', () => {
const first: ShapeHandle = { id: 'shape-one', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-one', documentVersion: 2 }
const second: ShapeHandle = { ...first, id: 'shape-two', documentVersion: 3 }
assert.doesNotThrow(() => validateBooleanUnionInput({ shapes: [first, second], documentId: 'doc-one', documentVersion: 3 }))
assert.throws(() => validateBooleanUnionInput({ shapes: [first, { ...second, documentId: 'doc-two' }], documentId: 'doc-one', documentVersion: 3 }), /another document/)
assert.throws(() => validateBooleanUnionInput({ shapes: [first, second], documentId: 'doc-one', documentVersion: 2 }), /newer/)
assert.throws(() => validateBooleanUnionInput({ shapes: [first], documentId: 'doc-one', documentVersion: 2 }), /at least 2/)
})
test('Bitbybit face meshes are normalized into a facade-owned indexed asset', () => {
const shape: ShapeHandle = { id: 'shape-test', kernel: 'bitbybit-occt', kind: 'solid', documentVersion: 7 }
const shape: ShapeHandle = { id: 'shape-test', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 7 }
const mesh = normalizeBitbybitMesh(shape, {
faceList: [{ faceIndex: 1, vertexCoord: [0, 0, 0, 2, 0, 0, 0, 3, 0], normalCoord: [0, 0, 1, 0, 0, 1, 0, 0, 1], triIndexes: [0, 1, 2], numberOfTriangles: 1 }],
edgeList: [], pointsList: [],