P3-03: add OCCT primitives and placement
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"baselineId": "bitbybit-web-cad-runtime-0.1",
|
||||
"entrypoint": "BitBybitWebCadFacade",
|
||||
"frontendFramework": "React 18 + Vite 6 + TypeScript 5",
|
||||
"geometry": { "provider": "@bitbybit-dev/occt-worker", "kernel": "OCCT WASM", "version": "1.1.1", "execution": "dedicated worker", "wasmSha256": "1b6a8fc7b83d222854f73b66af8d0e45ad4fdee025904b447e885d4e18ae8656", "status": "basic-runtime-integration" },
|
||||
"geometry": { "provider": "@bitbybit-dev/occt-worker", "kernel": "OCCT WASM", "version": "1.1.1", "execution": "dedicated worker", "wasmSha256": "1b6a8fc7b83d222854f73b66af8d0e45ad4fdee025904b447e885d4e18ae8656", "status": "primitive-and-placement-integration" },
|
||||
"viewport": { "provider": "Three.js internal adapter", "package": "three", "version": "0.185.1", "baseline": "WebGL2", "status": "adapter-skeleton" },
|
||||
"persistence": { "database": "@sqlite.org/sqlite-wasm 3.53.0-build1", "filesystem": "OPFS VFS with in-memory fallback", "writer": "single worker", "crossOriginIsolation": "required for OPFS worker", "status": "worker-skeleton" },
|
||||
"targetBrowsers": "Capability matrix to be locked in P0-02",
|
||||
|
||||
@@ -1596,7 +1596,7 @@ P0 基线/治理
|
||||
| P5-01 Three.js 视口适配器 | `IN PROGRESS` | `src/facade/threeViewport.ts` 使用 `three@0.185.1`,已挂载 WebGL2 场景和资源释放 |
|
||||
| P3-01 Bitbybit OCCT WASM 运行时 | `IN PROGRESS` | 精确锁定 `@bitbybit-dev/occt-worker@1.1.1`,专用 Worker 可加载 34,524,750 字节 OCCT WASM;尚未建立 FreeCAD/OCCT 自构建脚本,不能标记完成 |
|
||||
| P3-02 几何句柄与网格协议 | `IN PROGRESS` | Facade 已定义并实现受控 `ShapeHandle`、`MeshAsset`、`SubshapeRef` 类型;底层 OCCT hash 不出运行时,句柄篡改/释放后访问被拒绝;稳定子形状命名尚未实现 |
|
||||
| P3-03 基本体和变换 | `IN PROGRESS` | 真实 OCCT Box 创建与三角化通过;Cylinder/Sphere/Cone、Placement、单位误差预算和黄金模型尚未实现 |
|
||||
| P3-03 基本体和变换 | `IN PROGRESS` | 真实 OCCT Box/Cylinder/Sphere/Cone 和 FreeCAD 风格 Placement 已通过浏览器矩阵;0.05 mm 网格精度下曲面包围盒最大离散误差约 0.023 mm;自动化浏览器黄金测试尚未进入 CI |
|
||||
| P5-02 网格增量接入 | `IN PROGRESS` | Three Adapter 可用 `BufferGeometry` 接收 Facade `MeshAsset`,替换时释放旧 GPU geometry;对象缓存与文档增量同步尚未实现 |
|
||||
| P8-01 第一批自动化场景 | `IN PROGRESS` | `tests/facade.test.ts` 已覆盖初始化、schema、串行持久化队列、自动保存合并、资源引用、事件、任务生命周期、禁用命令、请求上下文、特征提交和历史;E2E/黄金几何待补齐 |
|
||||
|
||||
@@ -1633,4 +1633,18 @@ P0 基线/治理
|
||||
| Three 网格消费 | `PASS` | `MeshAsset` 转为 Three `BufferGeometry`,旧 geometry 在替换/销毁时释放;选择变化不再重建场景 |
|
||||
| 生产构建 | `PASS` | Vite 输出独立 `geometryWorker` 与 OCCT WASM;WASM 约 34.5 MB,gzip 约 8.6 MB |
|
||||
|
||||
限制和下一任务:P3-02 还需设计稳定 `SubshapeRef` 的持久命名算法;P3-03 继续加入 Cylinder、Sphere、Cone 和 Placement,并用黄金包围盒/体积核验;P5-02 需要按文档对象 ID 建立多对象网格缓存,而不是仅加载一个启动预览实体。
|
||||
限制和下一任务:P3-02 还需设计稳定 `SubshapeRef` 的持久命名算法;P3-03 需要把已通过的浏览器黄金包围盒矩阵纳入 CI,并补充体积核验;P5-02 需要按文档对象 ID 建立多对象网格缓存,而不是仅加载一个启动预览实体。
|
||||
|
||||
### 16.12 P3-03 基本体与 Placement 验证记录
|
||||
|
||||
所有输入先在 Facade 边界验证有限数、正尺寸、角度范围、非零方向向量和文档版本;错误输入不会触发 Worker 初始化。Placement 使用平移和轴角旋转,固定 `scaleFactor=1`,不把缩放混入 FreeCAD Placement 语义。变换产生新句柄,源形状保持不变。
|
||||
|
||||
| 算例 | 结果 | 包围盒/误差 |
|
||||
|---|---|---|
|
||||
| Cylinder `r=2,h=4,Y轴,中心原点` | 106 顶点、100 三角形 | X `[-1.9854,1.9854]`、Y `[-2,2]`、Z `[-2,2]`;最大离散误差约 0.0146 mm |
|
||||
| Sphere `r=1.5,center=[1,2,3]` | 168 顶点、306 三角形 | Z `[1.5,4.5]`;X/Y 极值最大离散误差约 0.0228 mm |
|
||||
| Cone `r1=2,r2=1,h=3,Y轴` | 138 顶点、160 三角形 | Y `[0,3]`、Z `[-2,2]`;X 极值误差约 0.0146 mm |
|
||||
| Box Placement `translation=[5,-1,2]` | 24 顶点、12 三角形 | 变换后 X `[4,6]`、Y `[-3,1]`、Z `[0.5,3.5]`,与解析值一致 |
|
||||
| 相同 Box 双句柄 | `PASS` | 释放第一个句柄后第二个仍可网格化;最终引用释放才删除 OCCT 缓存形状 |
|
||||
|
||||
浏览器验证使用 `crossOriginIsolated=true` 的 Chrome、专用 OCCT Worker 和 `precision=0.05`。上述曲面误差均小于网格精度;这是三角网格显示误差,不代表 B-Rep 几何尺寸误差。
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
||||
import type { Inputs } from '@bitbybit-dev/occt'
|
||||
import type { CreateBoxInput, GeometryCapabilities, MeshAsset, ShapeHandle } from './types'
|
||||
import type { ApplyPlacementInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, GeometryCapabilities, MeshAsset, ShapeHandle } from './types'
|
||||
|
||||
type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer
|
||||
type KernelMesh = Inputs.OCCT.DecomposedMeshDto
|
||||
type ShapeEntry = { handle: ShapeHandle; reference: KernelShapeReference }
|
||||
type KernelReferenceEntry = { count: number; reference: KernelShapeReference }
|
||||
|
||||
const unavailableCapabilities = (): GeometryCapabilities => ({
|
||||
provider: 'Bitbybit OCCT',
|
||||
@@ -19,13 +20,63 @@ const finitePositive = (value: number, name: string) => {
|
||||
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a finite number greater than zero.`)
|
||||
}
|
||||
|
||||
const finiteNonNegative = (value: number, name: string) => {
|
||||
if (!Number.isFinite(value) || value < 0) throw new RangeError(`${name} must be a finite non-negative number.`)
|
||||
}
|
||||
|
||||
const validateDocumentVersion = (value: number) => {
|
||||
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError('documentVersion must be a non-negative safe integer.')
|
||||
}
|
||||
|
||||
const validateVector = (value: [number, number, number], name: string, allowZero = true) => {
|
||||
if (value.length !== 3 || value.some((coordinate) => !Number.isFinite(coordinate))) throw new RangeError(`${name} must contain three finite coordinates.`)
|
||||
if (!allowZero && value.every((coordinate) => coordinate === 0)) throw new RangeError(`${name} must not be the zero vector.`)
|
||||
}
|
||||
|
||||
const validateAngle = (value: number, name: string, allowZero = false) => {
|
||||
if (!Number.isFinite(value) || value > 360 || value < (allowZero ? 0 : Number.EPSILON)) throw new RangeError(`${name} must be between ${allowZero ? '0' : '0 (exclusive)'} and 360 degrees.`)
|
||||
}
|
||||
|
||||
export const validateBoxInput = (input: CreateBoxInput) => {
|
||||
finitePositive(input.width, 'width')
|
||||
finitePositive(input.length, 'length')
|
||||
finitePositive(input.height, 'height')
|
||||
if (!Number.isSafeInteger(input.documentVersion) || input.documentVersion < 0) throw new RangeError('documentVersion must be a non-negative safe integer.')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
const center = input.center ?? [0, 0, 0]
|
||||
if (center.length !== 3 || center.some((coordinate) => !Number.isFinite(coordinate))) throw new RangeError('center must contain three finite coordinates.')
|
||||
validateVector(center, 'center')
|
||||
}
|
||||
|
||||
export const validateCylinderInput = (input: CreateCylinderInput) => {
|
||||
finitePositive(input.radius, 'radius')
|
||||
finitePositive(input.height, 'height')
|
||||
validateVector(input.center ?? [0, 0, 0], 'center')
|
||||
validateVector(input.direction ?? [0, 1, 0], 'direction', false)
|
||||
validateAngle(input.angle ?? 360, 'angle')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
export const validateSphereInput = (input: CreateSphereInput) => {
|
||||
finitePositive(input.radius, 'radius')
|
||||
validateVector(input.center ?? [0, 0, 0], 'center')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
export const validateConeInput = (input: CreateConeInput) => {
|
||||
finiteNonNegative(input.radius1, 'radius1')
|
||||
finiteNonNegative(input.radius2, 'radius2')
|
||||
if (input.radius1 === 0 && input.radius2 === 0) throw new RangeError('At least one cone radius must be greater than zero.')
|
||||
finitePositive(input.height, 'height')
|
||||
validateVector(input.center ?? [0, 0, 0], 'center')
|
||||
validateVector(input.direction ?? [0, 1, 0], 'direction', false)
|
||||
validateAngle(input.angle ?? 360, 'angle')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
export const validatePlacementInput = (input: ApplyPlacementInput) => {
|
||||
validateVector(input.placement.translation, 'translation')
|
||||
validateVector(input.placement.rotationAxis, 'rotationAxis', false)
|
||||
validateAngle(input.placement.rotationAngle, 'rotationAngle', true)
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
export const assertShapeHandleIntegrity = (actual: ShapeHandle, expected: ShapeHandle) => {
|
||||
@@ -78,6 +129,7 @@ export class BitbybitGeometryRuntime {
|
||||
private cancelInitialization: (() => void) | null = null
|
||||
private sequence = 0
|
||||
private readonly shapes = new Map<string, ShapeEntry>()
|
||||
private readonly kernelReferences = new Map<number, KernelReferenceEntry>()
|
||||
|
||||
capabilities(): GeometryCapabilities { return { ...this.capabilitiesState } }
|
||||
|
||||
@@ -113,6 +165,7 @@ export class BitbybitGeometryRuntime {
|
||||
subscription.unsubscribe()
|
||||
worker.terminate()
|
||||
this.shapes.clear()
|
||||
this.kernelReferences.clear()
|
||||
this.worker = null
|
||||
this.client = null
|
||||
this.initialization = null
|
||||
@@ -141,20 +194,62 @@ export class BitbybitGeometryRuntime {
|
||||
center: input.center ?? [0, 0, 0],
|
||||
originOnCenter: input.originOnCenter ?? true,
|
||||
})
|
||||
const handle: ShapeHandle = {
|
||||
id: `shape-${Date.now().toString(36)}-${(++this.sequence).toString(36)}`,
|
||||
kernel: 'bitbybit-occt',
|
||||
kind: 'solid',
|
||||
documentVersion: input.documentVersion,
|
||||
return this.registerShape(kernelShape, input.documentVersion)
|
||||
}
|
||||
this.shapes.set(handle.id, { handle, reference: kernelShape })
|
||||
return handle
|
||||
|
||||
async createCylinder(input: CreateCylinderInput): Promise<ShapeHandle> {
|
||||
validateCylinderInput(input)
|
||||
const client = await this.readyClient()
|
||||
const kernelShape = await client.occt.shapes.solid.createCylinder({
|
||||
radius: input.radius,
|
||||
height: input.height,
|
||||
center: input.center ?? [0, 0, 0],
|
||||
direction: input.direction ?? [0, 1, 0],
|
||||
angle: input.angle ?? 360,
|
||||
originOnCenter: input.originOnCenter ?? false,
|
||||
})
|
||||
return this.registerShape(kernelShape, input.documentVersion)
|
||||
}
|
||||
|
||||
async createSphere(input: CreateSphereInput): Promise<ShapeHandle> {
|
||||
validateSphereInput(input)
|
||||
const client = await this.readyClient()
|
||||
const kernelShape = await client.occt.shapes.solid.createSphere({ radius: input.radius, center: input.center ?? [0, 0, 0] })
|
||||
return this.registerShape(kernelShape, input.documentVersion)
|
||||
}
|
||||
|
||||
async createCone(input: CreateConeInput): Promise<ShapeHandle> {
|
||||
validateConeInput(input)
|
||||
const client = await this.readyClient()
|
||||
const kernelShape = await client.occt.shapes.solid.createCone({
|
||||
radius1: input.radius1,
|
||||
radius2: input.radius2,
|
||||
height: input.height,
|
||||
angle: input.angle ?? 360,
|
||||
center: input.center ?? [0, 0, 0],
|
||||
direction: input.direction ?? [0, 1, 0],
|
||||
})
|
||||
return this.registerShape(kernelShape, input.documentVersion)
|
||||
}
|
||||
|
||||
async applyPlacement(input: ApplyPlacementInput): Promise<ShapeHandle> {
|
||||
validatePlacementInput(input)
|
||||
const source = this.resolveShape(input.shape)
|
||||
const client = await this.readyClient()
|
||||
const kernelShape = await client.occt.transforms.transform({
|
||||
shape: source.reference,
|
||||
translation: input.placement.translation,
|
||||
rotationAxis: input.placement.rotationAxis,
|
||||
rotationAngle: input.placement.rotationAngle,
|
||||
scaleFactor: 1,
|
||||
})
|
||||
return this.registerShape(kernelShape, input.documentVersion)
|
||||
}
|
||||
|
||||
async mesh(shape: ShapeHandle, precision = 0.05): Promise<MeshAsset> {
|
||||
finitePositive(precision, 'precision')
|
||||
const client = await this.readyClient()
|
||||
const entry = this.resolveShape(shape)
|
||||
const client = await this.readyClient()
|
||||
const mesh = await client.occt.shapeToMesh({ shape: entry.reference, precision, adjustYtoZ: false })
|
||||
return normalizeBitbybitMesh(entry.handle, mesh)
|
||||
}
|
||||
@@ -164,6 +259,12 @@ export class BitbybitGeometryRuntime {
|
||||
if (!entry) return
|
||||
this.assertHandle(shape, entry.handle)
|
||||
this.shapes.delete(shape.id)
|
||||
const kernelReference = this.kernelReferences.get(entry.reference.hash)
|
||||
if (kernelReference && kernelReference.count > 1) {
|
||||
kernelReference.count -= 1
|
||||
return
|
||||
}
|
||||
this.kernelReferences.delete(entry.reference.hash)
|
||||
const client = this.client
|
||||
if (client && this.capabilitiesState.status === 'ready') await client.occt.deleteShape({ shape: entry.reference })
|
||||
}
|
||||
@@ -171,6 +272,7 @@ export class BitbybitGeometryRuntime {
|
||||
dispose() {
|
||||
this.cancelInitialization?.()
|
||||
this.shapes.clear()
|
||||
this.kernelReferences.clear()
|
||||
this.client?.occtWorkerManager.cleanPromisesMade()
|
||||
this.worker?.terminate()
|
||||
this.client = null
|
||||
@@ -186,6 +288,20 @@ export class BitbybitGeometryRuntime {
|
||||
return this.client
|
||||
}
|
||||
|
||||
private registerShape(reference: KernelShapeReference, documentVersion: number) {
|
||||
const handle: ShapeHandle = {
|
||||
id: `shape-${Date.now().toString(36)}-${(++this.sequence).toString(36)}`,
|
||||
kernel: 'bitbybit-occt',
|
||||
kind: 'solid',
|
||||
documentVersion,
|
||||
}
|
||||
const kernelReference = this.kernelReferences.get(reference.hash)
|
||||
if (kernelReference) kernelReference.count += 1
|
||||
else this.kernelReferences.set(reference.hash, { count: 1, reference })
|
||||
this.shapes.set(handle.id, { handle, reference })
|
||||
return handle
|
||||
}
|
||||
|
||||
private resolveShape(shape: ShapeHandle) {
|
||||
if (shape.kernel !== 'bitbybit-occt') throw new Error(`Unsupported geometry kernel: ${shape.kernel}`)
|
||||
const entry = this.shapes.get(shape.id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export { createMockFacade } from './mockFacade'
|
||||
export { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveScheduler, SqliteProjectPersistence } from './projectStore'
|
||||
export { ThreeViewportAdapter } from './threeViewport'
|
||||
export { assertShapeHandleIntegrity, BitbybitGeometryRuntime, normalizeBitbybitMesh, validateBoxInput } from './geometryRuntime'
|
||||
export { assertShapeHandleIntegrity, BitbybitGeometryRuntime, normalizeBitbybitMesh, validateBoxInput, validateConeInput, validateCylinderInput, validatePlacementInput, validateSphereInput } from './geometryRuntime'
|
||||
export { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION } from './projectSchema'
|
||||
export type { BitBybitViewportAdapter, BitBybitWebCadFacade, CommandState, CreateBoxInput, DocumentSnapshot, FacadeEvent, FacadeState, GeometryCapabilities, MeshAsset, ModelTreeItem, PersistenceCapabilities, ProjectResource, ProjectSaveResult, ShapeHandle, SubshapeRef, TaskSnapshot } from './types'
|
||||
export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentSnapshot, FacadeEvent, FacadeState, GeometryCapabilities, MeshAsset, ModelTreeItem, PersistenceCapabilities, Placement, ProjectResource, ProjectSaveResult, ShapeHandle, SubshapeRef, TaskSnapshot } from './types'
|
||||
|
||||
@@ -132,7 +132,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },
|
||||
task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
|
||||
project: { capabilities: () => projectPersistence.capabilities(), save: (document = getState().document) => projectPersistence.save(document), load: (documentId) => projectPersistence.load(documentId), resource: projectPersistence.resource },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), release: (shape) => geometryRuntime.release(shape), dispose: () => geometryRuntime.dispose() },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), release: (shape) => geometryRuntime.release(shape), dispose: () => geometryRuntime.dispose() },
|
||||
viewport: { createAdapter: () => new ThreeViewportAdapter() },
|
||||
getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
|
||||
}
|
||||
|
||||
@@ -85,6 +85,44 @@ export type CreateBoxInput = {
|
||||
documentVersion: number
|
||||
}
|
||||
|
||||
export type CreateCylinderInput = {
|
||||
radius: number
|
||||
height: number
|
||||
center?: [number, number, number]
|
||||
direction?: [number, number, number]
|
||||
angle?: number
|
||||
originOnCenter?: boolean
|
||||
documentVersion: number
|
||||
}
|
||||
|
||||
export type CreateSphereInput = {
|
||||
radius: number
|
||||
center?: [number, number, number]
|
||||
documentVersion: number
|
||||
}
|
||||
|
||||
export type CreateConeInput = {
|
||||
radius1: number
|
||||
radius2: number
|
||||
height: number
|
||||
center?: [number, number, number]
|
||||
direction?: [number, number, number]
|
||||
angle?: number
|
||||
documentVersion: number
|
||||
}
|
||||
|
||||
export type Placement = {
|
||||
translation: [number, number, number]
|
||||
rotationAxis: [number, number, number]
|
||||
rotationAngle: number
|
||||
}
|
||||
|
||||
export type ApplyPlacementInput = {
|
||||
shape: ShapeHandle
|
||||
placement: Placement
|
||||
documentVersion: number
|
||||
}
|
||||
|
||||
export type CommandState = {
|
||||
id: string
|
||||
status: 'hidden' | 'disabled' | 'enabled' | 'active'
|
||||
@@ -204,6 +242,10 @@ export interface BitBybitWebCadFacade {
|
||||
capabilities(): GeometryCapabilities
|
||||
initialize(): Promise<GeometryCapabilities>
|
||||
createBox(input: CreateBoxInput): Promise<ShapeHandle>
|
||||
createCylinder(input: CreateCylinderInput): Promise<ShapeHandle>
|
||||
createSphere(input: CreateSphereInput): Promise<ShapeHandle>
|
||||
createCone(input: CreateConeInput): Promise<ShapeHandle>
|
||||
applyPlacement(input: ApplyPlacementInput): Promise<ShapeHandle>
|
||||
mesh(shape: ShapeHandle, precision?: number): Promise<MeshAsset>
|
||||
release(shape: ShapeHandle): Promise<void>
|
||||
dispose(): void
|
||||
|
||||
@@ -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 } from '../src/facade/geometryRuntime'
|
||||
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBoxInput, validateConeInput, validateCylinderInput, validatePlacementInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import type { ShapeHandle } from '../src/facade/types'
|
||||
|
||||
test('facade exposes a stable initial document projection', () => {
|
||||
@@ -20,6 +20,10 @@ 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/)
|
||||
})
|
||||
|
||||
test('geometry handles reject forged document ownership', () => {
|
||||
|
||||
Reference in New Issue
Block a user