P3-04: add OCCT pad pocket and revolution
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": "primitive-placement-and-boolean-integration" },
|
||||
"geometry": { "provider": "@bitbybit-dev/occt-worker", "kernel": "OCCT WASM", "version": "1.1.1", "execution": "dedicated worker", "wasmSha256": "1b6a8fc7b83d222854f73b66af8d0e45ad4fdee025904b447e885d4e18ae8656", "status": "core-feature-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",
|
||||
|
||||
@@ -1597,6 +1597,7 @@ P0 基线/治理
|
||||
| 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` 类型;句柄携带 documentId/version,底层 OCCT hash 不出运行时,跨文档、字段篡改和释放后访问被拒绝;稳定子形状命名尚未实现 |
|
||||
| P3-03 基本体和变换 | `IN PROGRESS` | 真实 OCCT Box/Cylinder/Sphere/Cone 和 FreeCAD 风格 Placement 已通过浏览器矩阵;0.05 mm 网格精度下曲面包围盒最大离散误差约 0.023 mm;自动化浏览器黄金测试尚未进入 CI |
|
||||
| P3-04 Boolean/Pad/Pocket/Revolution | `IN PROGRESS` | 文档作用域 Union/Cut/Intersection 与 PlanarProfile 驱动的 Pad/Pocket/Revolution 已通过真实 OCCT 浏览器矩阵;取消、进度、Through all/Up to face 和文档重计算接线尚未完成 |
|
||||
| P5-02 网格增量接入 | `IN PROGRESS` | Three Adapter 可用 `BufferGeometry` 接收 Facade `MeshAsset`,替换时释放旧 GPU geometry;对象缓存与文档增量同步尚未实现 |
|
||||
| P8-01 第一批自动化场景 | `IN PROGRESS` | `tests/facade.test.ts` 已覆盖初始化、schema、串行持久化队列、自动保存合并、资源引用、事件、任务生命周期、禁用命令、请求上下文、特征提交和历史;E2E/黄金几何待补齐 |
|
||||
|
||||
@@ -1664,3 +1665,18 @@ Bitbybit OCCT Worker 1.1.1 的内置 Intersection 包装在调用 `BRepAlgoAPI_C
|
||||
| 跨文档/未来版本/输入不足 | `PASS` | Facade 同步拒绝,不触发 OCCT 运算 |
|
||||
|
||||
OCCT Boolean 结果可能与源形状共享底层拓扑。1.1.1 的 `deleteShape` 会执行全量几何清理,逐个删除结果可能破坏仍存活源形状。因此当前 Facade 在句柄释放时更新内部引用计数,并在活动句柄归零后统一 `cleanAllCache()`;Worker 终止时也会释放全部 WASM 内存。P3-06 需实现依赖感知的细粒度安全回收和长会话内存基准。
|
||||
|
||||
### 16.14 P3-04 Pad/Pocket/Revolution 验证记录
|
||||
|
||||
`PlanarProfile` 由一个外环和零到多个孔环构成。Facade 接受首尾重复或不重复的闭合表示,拒绝少于三个有效点、连续重复点、共线环、非有限坐标和不共面环;进入 OCCT 前会自动把孔环绕向调整为外环的反向。自相交、孔嵌套和轮廓相交仍需 Sketcher/ShapeFix 黄金验证,当前不能静默宣称完整草图兼容。
|
||||
|
||||
| 算例 | 结果 | 包围盒 |
|
||||
|---|---|---|
|
||||
| XZ 平面 `4×2` 轮廓 Pad,长度 3 | 24 顶点、12 三角形 | X `[-2,2]`、Y `[0,3]`、Z `[-1,1]` |
|
||||
| 同轮廓 symmetricToPlane Pad,长度 4 | 24 顶点、12 三角形 | X `[-2,2]`、Y `[-2,2]`、Z `[-1,1]` |
|
||||
| `6×6` 外环、`2×2` 孔环 Pad,长度 2 | 48 顶点、32 三角形 | X/Z `[-3,3]`、Y `[0,2]`,孔壁已生成 |
|
||||
| `6×4×6` Box 上的 `2×2` 贯穿 Pocket | 48 顶点、32 三角形 | 外包围盒保持 X/Z `[-3,3]`、Y `[-2,2]` |
|
||||
| X `[1,2]`、Y `[-1,1]` 面绕 Y 轴 360° Revolution | 212 顶点、208 三角形 | X `[-2,2]`、Y `[-1,1]`、Z 约 `[-1.9854,1.9854]` |
|
||||
| 释放全部特征结果后访问 Pocket 基体 | `PASS` | 基体仍为 24 顶点、12 三角形,包围盒不变 |
|
||||
|
||||
Pad/Pocket 的方向向量在 Facade 内归一化,`length` 单独控制尺寸;`reversed` 改变方向,`symmetricToPlane` 将起始面移动到长度中点。Revolution Worker 插件支持任意轴原点和方向,弥补 Bitbybit 1.1.1 高层 `revolve` 固定绕原点轴的限制。当前 Pocket 是长度模式;`Through all`、`Up to face`、双向长度和轮廓支持面映射继续由 P4-02/P4-05 与 P3-04 后续任务实现。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
||||
import type { Inputs } from '@bitbybit-dev/occt'
|
||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, GeometryCapabilities, GeometryDocumentContext, MeshAsset, ShapeHandle } from './types'
|
||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle } from './types'
|
||||
|
||||
type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer
|
||||
type KernelMesh = Inputs.OCCT.DecomposedMeshDto
|
||||
@@ -101,6 +101,64 @@ export const validateBooleanCutInput = (input: BooleanCutInput) => {
|
||||
|
||||
export const validateBooleanIntersectionInput = (input: BooleanIntersectionInput) => validateBooleanShapes(input, input.shapes, 2)
|
||||
|
||||
const samePoint = (left: Point3, right: Point3, tolerance = 1e-9) => left.every((coordinate, axis) => Math.abs(coordinate - right[axis]) <= tolerance)
|
||||
const subtract = (left: Point3, right: Point3): Point3 => [left[0] - right[0], left[1] - right[1], left[2] - right[2]]
|
||||
const cross = (left: Point3, right: Point3): Point3 => [left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]]
|
||||
const dot = (left: Point3, right: Point3) => left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
|
||||
const magnitude = (value: Point3) => Math.hypot(value[0], value[1], value[2])
|
||||
|
||||
const normalizedRing = (ring: Point3[]) => ring.length > 1 && samePoint(ring[0], ring.at(-1) as Point3) ? ring.slice(0, -1) : [...ring]
|
||||
|
||||
const ringNormal = (ring: Point3[]): Point3 | null => {
|
||||
const origin = ring[0]
|
||||
for (let index = 1; index < ring.length - 1; index += 1) {
|
||||
const candidate = cross(subtract(ring[index], origin), subtract(ring[index + 1], origin))
|
||||
if (magnitude(candidate) > 1e-9) return candidate
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const validatePlanarProfile = (profile: PlanarProfile) => {
|
||||
const rings = [normalizedRing(profile.outer), ...(profile.holes ?? []).map(normalizedRing)]
|
||||
for (const [ringIndex, ring] of rings.entries()) {
|
||||
if (ring.length < 3) throw new RangeError(`Profile ring ${ringIndex} requires at least three distinct points.`)
|
||||
ring.forEach((point, pointIndex) => {
|
||||
validateVector(point, `profile ring ${ringIndex} point ${pointIndex}`)
|
||||
if (samePoint(point, ring[(pointIndex + 1) % ring.length])) throw new RangeError(`Profile ring ${ringIndex} contains consecutive duplicate points.`)
|
||||
})
|
||||
}
|
||||
const origin = rings[0][0]
|
||||
const normal = ringNormal(rings[0])
|
||||
if (!normal) throw new RangeError('Profile ring 0 is collinear.')
|
||||
const normalLength = magnitude(normal)
|
||||
for (const [ringIndex, ring] of rings.entries()) {
|
||||
if (!ringNormal(ring)) throw new RangeError(`Profile ring ${ringIndex} is collinear.`)
|
||||
for (const point of ring) if (Math.abs(dot(normal, subtract(point, origin))) / normalLength > 1e-7) throw new RangeError(`Profile ring ${ringIndex} is not coplanar with the outer ring.`)
|
||||
}
|
||||
}
|
||||
|
||||
const validateLinearFeature = (input: GeometryDocumentContext & LinearFeatureParameters) => {
|
||||
validateDocumentContext(input)
|
||||
validatePlanarProfile(input.profile)
|
||||
finitePositive(input.length, 'length')
|
||||
validateVector(input.direction ?? [0, 1, 0], 'direction', false)
|
||||
}
|
||||
|
||||
export const validatePadInput = (input: PadInput) => validateLinearFeature(input)
|
||||
|
||||
export const validatePocketInput = (input: PocketInput) => {
|
||||
validateLinearFeature(input)
|
||||
validateShapeContext(input, input.base)
|
||||
}
|
||||
|
||||
export const validateRevolutionInput = (input: RevolutionInput) => {
|
||||
validateDocumentContext(input)
|
||||
validatePlanarProfile(input.profile)
|
||||
validateVector(input.axisOrigin ?? [0, 0, 0], 'axisOrigin')
|
||||
validateVector(input.axisDirection ?? [0, 1, 0], 'axisDirection', false)
|
||||
validateAngle(input.angle ?? 360, 'angle')
|
||||
}
|
||||
|
||||
export const assertShapeHandleIntegrity = (actual: ShapeHandle, expected: ShapeHandle) => {
|
||||
if (actual.id !== expected.id || actual.kernel !== expected.kernel || actual.kind !== expected.kind || actual.documentId !== expected.documentId || actual.documentVersion !== expected.documentVersion) throw new Error(`Shape handle integrity check failed: ${actual.id}`)
|
||||
}
|
||||
@@ -293,6 +351,35 @@ export class BitbybitGeometryRuntime {
|
||||
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
||||
}
|
||||
|
||||
async pad(input: PadInput): Promise<ShapeHandle> {
|
||||
validatePadInput(input)
|
||||
const client = await this.readyClient()
|
||||
const kernelShape = await this.createExtrusion(client, input)
|
||||
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
||||
}
|
||||
|
||||
async pocket(input: PocketInput): Promise<ShapeHandle> {
|
||||
validatePocketInput(input)
|
||||
const base = this.resolveShape(input.base).reference
|
||||
const client = await this.readyClient()
|
||||
const tool = await this.createExtrusion(client, input)
|
||||
const kernelShape = await client.occt.booleans.difference({ shape: base, shapes: [tool], keepEdges: false })
|
||||
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
||||
}
|
||||
|
||||
async revolution(input: RevolutionInput): Promise<ShapeHandle> {
|
||||
validateRevolutionInput(input)
|
||||
const client = await this.readyClient()
|
||||
const face = await this.createProfileFace(client, input.profile)
|
||||
const kernelShape = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.feature.revolution', {
|
||||
shape: face,
|
||||
axisOrigin: input.axisOrigin ?? [0, 0, 0],
|
||||
axisDirection: input.axisDirection ?? [0, 1, 0],
|
||||
angle: input.angle ?? 360,
|
||||
}) as KernelShapeReference
|
||||
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
||||
}
|
||||
|
||||
async mesh(shape: ShapeHandle, precision = 0.05): Promise<MeshAsset> {
|
||||
finitePositive(precision, 'precision')
|
||||
const entry = this.resolveShape(shape)
|
||||
@@ -335,6 +422,27 @@ export class BitbybitGeometryRuntime {
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async createProfileFace(client: BitByBitOCCT, profile: PlanarProfile): Promise<KernelShapeReference> {
|
||||
const rings = [normalizedRing(profile.outer), ...(profile.holes ?? []).map(normalizedRing)]
|
||||
const outerNormal = ringNormal(rings[0]) as Point3
|
||||
const orientedRings = rings.map((ring, index) => index > 0 && dot(outerNormal, ringNormal(ring) as Point3) > 0 ? [...ring].reverse() : ring)
|
||||
const wires = await Promise.all(orientedRings.map((points) => client.occt.shapes.wire.createPolygonWire({ points })))
|
||||
return client.occt.shapes.face.createFaceFromWires({ shapes: wires, planar: true })
|
||||
}
|
||||
|
||||
private async createExtrusion(client: BitByBitOCCT, input: LinearFeatureParameters): Promise<KernelShapeReference> {
|
||||
const direction = input.direction ?? [0, 1, 0]
|
||||
const directionLength = magnitude(direction)
|
||||
const sign = input.reversed ? -1 : 1
|
||||
const extrusion: Point3 = direction.map((coordinate) => coordinate / directionLength * input.length * sign) as Point3
|
||||
let face = await this.createProfileFace(client, input.profile)
|
||||
if (input.symmetricToPlane) {
|
||||
const translation = extrusion.map((coordinate) => -coordinate / 2) as Point3
|
||||
face = await client.occt.transforms.translate({ shape: face, translation })
|
||||
}
|
||||
return client.occt.operations.extrude({ shape: face, direction: extrusion })
|
||||
}
|
||||
|
||||
private registerShape(reference: KernelShapeReference, documentId: string, documentVersion: number) {
|
||||
const handle: ShapeHandle = {
|
||||
id: `shape-${Date.now().toString(36)}-${(++this.sequence).toString(36)}`,
|
||||
|
||||
@@ -39,6 +39,25 @@ const createWebCadPlugins = (occt: MainModule) => ({
|
||||
}
|
||||
},
|
||||
},
|
||||
feature: {
|
||||
revolution: (inputs: { shape: TopoDS_Shape; axisOrigin: [number, number, number]; axisDirection: [number, number, number]; angle: number }) => {
|
||||
const point = new occt.gp_Pnt(inputs.axisOrigin[0], inputs.axisOrigin[1], inputs.axisOrigin[2])
|
||||
const direction = new occt.gp_Dir(inputs.axisDirection[0], inputs.axisDirection[1], inputs.axisDirection[2])
|
||||
const axis = new occt.gp_Ax1(point, direction)
|
||||
const operation = inputs.angle >= 360
|
||||
? new occt.BRepPrimAPI_MakeRevol(inputs.shape, axis)
|
||||
: new occt.BRepPrimAPI_MakeRevol(inputs.shape, axis, inputs.angle * Math.PI / 180, false)
|
||||
try {
|
||||
if (!operation.IsDone()) throw new Error('OCCT revolution failed.')
|
||||
return operation.Shape()
|
||||
} finally {
|
||||
operation.delete()
|
||||
axis.delete()
|
||||
direction.delete()
|
||||
point.delete()
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const initialize = async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export { createMockFacade } from './mockFacade'
|
||||
export { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveScheduler, SqliteProjectPersistence } from './projectStore'
|
||||
export { ThreeViewportAdapter } from './threeViewport'
|
||||
export { assertShapeHandleIntegrity, BitbybitGeometryRuntime, normalizeBitbybitMesh, validateBooleanCutInput, validateBooleanIntersectionInput, validateBooleanUnionInput, validateBoxInput, validateConeInput, validateCylinderInput, validatePlacementInput, validateSphereInput } from './geometryRuntime'
|
||||
export { assertShapeHandleIntegrity, BitbybitGeometryRuntime, normalizeBitbybitMesh, validateBooleanCutInput, validateBooleanIntersectionInput, validateBooleanUnionInput, validateBoxInput, validateConeInput, validateCylinderInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validatePocketInput, validateRevolutionInput, validateSphereInput } from './geometryRuntime'
|
||||
export { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION } from './projectSchema'
|
||||
export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentSnapshot, FacadeEvent, FacadeState, GeometryCapabilities, GeometryDocumentContext, MeshAsset, ModelTreeItem, PersistenceCapabilities, Placement, ProjectResource, ProjectSaveResult, ShapeHandle, SubshapeRef, TaskSnapshot } from './types'
|
||||
export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentSnapshot, FacadeEvent, FacadeState, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, ModelTreeItem, PadInput, PersistenceCapabilities, Placement, PlanarProfile, PocketInput, Point3, ProjectResource, ProjectSaveResult, RevolutionInput, 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), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(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), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(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,
|
||||
}
|
||||
|
||||
@@ -140,6 +140,34 @@ export type BooleanIntersectionInput = GeometryDocumentContext & {
|
||||
keepEdges?: boolean
|
||||
}
|
||||
|
||||
export type Point3 = [number, number, number]
|
||||
|
||||
export type PlanarProfile = {
|
||||
outer: Point3[]
|
||||
holes?: Point3[][]
|
||||
}
|
||||
|
||||
export type LinearFeatureParameters = {
|
||||
profile: PlanarProfile
|
||||
length: number
|
||||
direction?: Point3
|
||||
reversed?: boolean
|
||||
symmetricToPlane?: boolean
|
||||
}
|
||||
|
||||
export type PadInput = GeometryDocumentContext & LinearFeatureParameters
|
||||
|
||||
export type PocketInput = GeometryDocumentContext & LinearFeatureParameters & {
|
||||
base: ShapeHandle
|
||||
}
|
||||
|
||||
export type RevolutionInput = GeometryDocumentContext & {
|
||||
profile: PlanarProfile
|
||||
axisOrigin?: Point3
|
||||
axisDirection?: Point3
|
||||
angle?: number
|
||||
}
|
||||
|
||||
export type CommandState = {
|
||||
id: string
|
||||
status: 'hidden' | 'disabled' | 'enabled' | 'active'
|
||||
@@ -266,6 +294,9 @@ export interface BitBybitWebCadFacade {
|
||||
union(input: BooleanUnionInput): Promise<ShapeHandle>
|
||||
cut(input: BooleanCutInput): Promise<ShapeHandle>
|
||||
intersection(input: BooleanIntersectionInput): Promise<ShapeHandle>
|
||||
pad(input: PadInput): Promise<ShapeHandle>
|
||||
pocket(input: PocketInput): Promise<ShapeHandle>
|
||||
revolution(input: RevolutionInput): 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, validateBooleanUnionInput, validateBoxInput, validateConeInput, validateCylinderInput, validatePlacementInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateConeInput, validateCylinderInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import type { ShapeHandle } from '../src/facade/types'
|
||||
|
||||
test('facade exposes a stable initial document projection', () => {
|
||||
@@ -43,6 +43,15 @@ test('boolean geometry rejects cross-document and future-version operands', () =
|
||||
assert.throws(() => validateBooleanUnionInput({ shapes: [first], documentId: 'doc-one', documentVersion: 2 }), /at least 2/)
|
||||
})
|
||||
|
||||
test('feature profiles reject degenerate and non-planar geometry', () => {
|
||||
const rectangle = { outer: [[-1, 0, -1], [1, 0, -1], [1, 0, 1], [-1, 0, 1]] as [number, number, number][] }
|
||||
assert.doesNotThrow(() => validatePlanarProfile(rectangle))
|
||||
assert.doesNotThrow(() => validatePadInput({ profile: rectangle, length: 4, documentId: 'doc', documentVersion: 1 }))
|
||||
assert.throws(() => validatePlanarProfile({ outer: [[0, 0, 0], [1, 0, 0], [2, 0, 0]] }), /collinear/)
|
||||
assert.throws(() => validatePlanarProfile({ outer: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0.1, 1]] }), /coplanar/)
|
||||
assert.throws(() => validateRevolutionInput({ profile: rectangle, axisDirection: [0, 0, 0], documentId: 'doc', documentVersion: 1 }), /axisDirection/)
|
||||
})
|
||||
|
||||
test('Bitbybit face meshes are normalized into a facade-owned indexed asset', () => {
|
||||
const shape: ShapeHandle = { id: 'shape-test', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 7 }
|
||||
const mesh = normalizeBitbybitMesh(shape, {
|
||||
|
||||
Reference in New Issue
Block a user