diff --git a/docs/freecad-full-parity-plan.zh-CN.md b/docs/freecad-full-parity-plan.zh-CN.md index 5220f28..3928f8a 100644 --- a/docs/freecad-full-parity-plan.zh-CN.md +++ b/docs/freecad-full-parity-plan.zh-CN.md @@ -313,3 +313,5 @@ TSN 已完成面级量化签名、唯一/重复候选判定和匹配 API,并 Sketcher 已有 Facade 事务模型和基础求解器适配器:Sketch 几何/约束快照进入 `DocumentObjectSnapshot`,新增几何、约束和求解会更新 `ConstraintStatus`、历史、自动保存与 SQLite schema v4。该适配器的职责是验证数据模型和提供可替换协议;它不宣称已经等价于 FreeCAD `planegcs`。SK-03 的 planegcs WASM、SK-07 的完整约束分类、SK-09 的交互回放和 SK-10 的 B-spline 仍是兼容门禁。 Bitbybit 的 OCCT `filletEdges` 与 `chamferEdges` 已进入 Geometry Facade,支持全边或显式索引、参数验证、句柄版本和 Worker 隔离。由于 OCCT 瞬时边索引不是持久命名,当前接口仍不能作为 FreeCAD PartDesign 的长期选边引用;必须等 TSN 的边级 `TopoRef`、历史映射和参数化重算完成后,才能标记为 `compatible`。 + +Geometry Facade 还接入了 Bitbybit IO 的 STEP 和 ASCII STL 导出,作为 FC-08/FC-09 的第一条交换格式垂直切片。导出数据仍只代表当前 Shape;对象树、Expression、Sketch 约束、TopoRef 和重算历史必须由 FCStd/.webcad 格式任务独立保存,不能因“能导出 STEP”而宣称参数化文件兼容。 diff --git a/docs/web-cad-implementation-plan.zh-CN.md b/docs/web-cad-implementation-plan.zh-CN.md index 85c7049..cedee17 100644 --- a/docs/web-cad-implementation-plan.zh-CN.md +++ b/docs/web-cad-implementation-plan.zh-CN.md @@ -1712,6 +1712,7 @@ DocumentSnapshot 现在区分模型树投影与 `DocumentObjectSnapshot` 真值 | 子形状签名 | `PASS` | 面签名不使用 transient `faceIndex`;重复签名标记 `ambiguous`,唯一候选可匹配 | | Sketcher 领域模型 | `PASS` | Sketch 几何/约束快照、基础求解器、DOF/冲突诊断和 Facade 事务已接入,SQLite schema v4 保存 `sketch_json` | | OCCT Fillet/Chamfer | `PASS` | Facade 调用 Bitbybit `filletEdges`/`chamferEdges`,半径/距离和边索引在边界校验;真实稳定 TopoRef 选边仍待 TSN | +| STEP/STL 几何导出 | `PASS` | Facade 通过 Bitbybit IO Worker 导出 STEP 和 ASCII STL;FCStd 参数化文档读写仍未完成 | 尚未完成的 TSN 工作:OCCT `Generated/Modified/Deleted` 历史捕获、跨布尔/特征的真实拓扑映射、面/边/顶点统一命名、附着和 `TopoRef` 文件迁移。当前签名是可审计的基础候选层,不能单独宣称 FreeCAD 稳定拓扑命名已完成。 diff --git a/src/facade/geometryRuntime.ts b/src/facade/geometryRuntime.ts index aeb1609..877a82e 100644 --- a/src/facade/geometryRuntime.ts +++ b/src/facade/geometryRuntime.ts @@ -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, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef } from './types' +import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef } from './types' import { createSubshapeRefs } from './topologyNaming' type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer @@ -380,6 +380,23 @@ export class BitbybitGeometryRuntime { return this.registerShape(kernelShape, input.documentId, input.documentVersion) } + async exportStep(shape: ShapeHandle, fileName = `${shape.id}.step`): Promise { + const entry = this.resolveShape(shape) + if (!fileName.toLowerCase().endsWith('.step') && !fileName.toLowerCase().endsWith('.stp')) throw new RangeError('STEP fileName must end with .step or .stp.') + const client = await this.readyClient() + const text = await client.occt.io.saveShapeSTEPAndReturn({ shape: entry.reference, fileName, adjustYtoZ: false, tryDownload: false }) + return { format: 'step', fileName, mediaType: 'application/step', text } + } + + async exportStl(shape: ShapeHandle, fileName = `${shape.id}.stl`, precision = 0.05): Promise { + const entry = this.resolveShape(shape) + if (!fileName.toLowerCase().endsWith('.stl')) throw new RangeError('STL fileName must end with .stl.') + finitePositive(precision, 'precision') + const client = await this.readyClient() + const text = await client.occt.io.saveShapeStlAndReturn({ shape: entry.reference, fileName, precision, adjustYtoZ: false, tryDownload: false, binary: false }) + return { format: 'stl', fileName, mediaType: 'model/stl', text } + } + async pad(input: PadInput): Promise { validatePadInput(input) const client = await this.readyClient() diff --git a/src/facade/index.ts b/src/facade/index.ts index 2eb3e60..801ca10 100644 --- a/src/facade/index.ts +++ b/src/facade/index.ts @@ -3,7 +3,7 @@ export { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveS export { ThreeViewportAdapter } from './threeViewport' 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, ChamferInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FacadeEvent, FacadeState, FilletInput, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, ModelTreeItem, ObjectPropertySnapshot, PadInput, PersistenceCapabilities, Placement, PlanarProfile, PocketInput, Point3, ProjectResource, ProjectSaveResult, PropertyValue, RecomputeResult, RevolutionInput, SetExpressionInput, SetPropertyInput, ShapeHandle, SubshapeRef, TaskSnapshot } from './types' +export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FacadeEvent, FacadeState, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, ModelTreeItem, ObjectPropertySnapshot, PadInput, PersistenceCapabilities, Placement, PlanarProfile, PocketInput, Point3, ProjectResource, ProjectSaveResult, PropertyValue, RecomputeResult, RevolutionInput, SetExpressionInput, SetPropertyInput, ShapeHandle, SubshapeRef, TaskSnapshot } from './types' export { createSubshapeRefs, matchSubshapes, signatureForFace } from './topologyNaming' export { BasicSketchSolverAdapter, cloneSketch, createSketch, solveSketch } from './sketcher' export type { SketchConstraint, SketchDiagnostic, SketchGeometry, SketchPoint, SketchPointRef, SketchSnapshot, SketchSolveOptions, SketchSolveResult, SketchSolverAdapter, SketchSolverStatus } from './sketcher' diff --git a/src/facade/mockFacade.ts b/src/facade/mockFacade.ts index d1a1fbe..3cd27c7 100644 --- a/src/facade/mockFacade.ts +++ b/src/facade/mockFacade.ts @@ -423,7 +423,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), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(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), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName), exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(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, } diff --git a/src/facade/types.ts b/src/facade/types.ts index f67169d..064802f 100644 --- a/src/facade/types.ts +++ b/src/facade/types.ts @@ -130,6 +130,13 @@ export type MeshAsset = { } } +export type GeometryFileExport = { + format: 'step' | 'stl' + fileName: string + mediaType: 'application/step' | 'model/stl' + text: string +} + export type GeometryDocumentContext = { documentId: string documentVersion: number @@ -376,6 +383,8 @@ export interface BitBybitWebCadFacade { intersection(input: BooleanIntersectionInput): Promise fillet(input: FilletInput): Promise chamfer(input: ChamferInput): Promise + exportStep(shape: ShapeHandle, fileName?: string): Promise + exportStl(shape: ShapeHandle, fileName?: string, precision?: number): Promise pad(input: PadInput): Promise pocket(input: PocketInput): Promise revolution(input: RevolutionInput): Promise