feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Native OCCT history provider
|
||||
|
||||
This provider is compiled from the cached OCCT checkout selected by
|
||||
`OCCT_SOURCE_DIR` and exposes the native Boolean history API through Emscripten
|
||||
Embind. The provider keeps `TopoDS_Shape` handles and history queries in one
|
||||
`OCCT_SOURCE_DIR` and exposes native Boolean and PartDesign history APIs through
|
||||
Emscripten Embind. The provider keeps `TopoDS_Shape` handles and history queries in one
|
||||
WASM instance. A handle from `@bitbybit-dev/occt` must never be passed directly
|
||||
to this module because WebAssembly linear memories are independent.
|
||||
|
||||
@@ -21,11 +21,67 @@ Emscripten toolchain.
|
||||
|
||||
The exported `booleanHistory(object, tool, operation)` function supports
|
||||
`fuse`, `cut`, and `common`. It returns the native result shape, per-input
|
||||
`modified`/`generated`/`deleted` records, and capability flags. The provider also
|
||||
exports `shapeToStep(shape)` and `booleanHistoryFromStep(objectStep, toolStep,
|
||||
operation)`. The latter is the cross-WASM transport: Bitbybit exports STEP text,
|
||||
the native provider reads it with `STEPControl_Reader`, returns the Boolean
|
||||
result again as `resultStep`, and no linear-memory pointer crosses the boundary.
|
||||
`modified`/`generated`/`deleted` records, a validity/topology/quality summary,
|
||||
and capability flags. `prismHistory(profile, dx, dy, dz)` adds the first
|
||||
PartDesign Pad contract: profile vertices can generate edges and profile edges
|
||||
can generate faces, while the profile face is preserved as a result face.
|
||||
`booleanProbe` and `booleanResult` are intentionally smaller operations for
|
||||
Worker stress/ownership gates; callers must delete every returned Embind shape
|
||||
handle. Primitive constructors cover positioned boxes, cylinders, spheres,
|
||||
cones and a test rectangle Face. The provider also exports `shapeToStep(shape)`,
|
||||
`booleanHistoryFromStep(objectStep, toolStep, operation)` and
|
||||
`prismHistoryFromStep(profileStep, dx, dy, dz)`. The STEP functions are the
|
||||
cross-WASM transport: Bitbybit exports STEP text, the native provider reads it
|
||||
with `STEPControl_Reader`, returns the result again as `resultStep`, and no
|
||||
linear-memory pointer crosses the boundary. Pad history currently requires a
|
||||
valid planar Face profile; solids are rejected by OCCT rather than silently
|
||||
treated as sketches. `loftHistoryFromStep(firstSectionStep, secondSectionStep,
|
||||
ruled)` adds a verified two-section solid loft using `BRepOffsetAPI_ThruSections`.
|
||||
It records generated edges/faces for both source profiles; more than two
|
||||
sections, closed lofts, and PartDesign base fusion/cut stay on the signature
|
||||
history path until their multi-source contract is implemented.
|
||||
`pipeHistoryFromStep(profileStep, spineStep)` adds a verified single-edge
|
||||
open-spine solid sweep using `BRepOffsetAPI_MakePipe`. It records profile
|
||||
vertex/edge generation, profile end caps, and spine edge/endpoint provenance;
|
||||
multi-edge spines, Frenet/transition modes, and hollow profiles remain explicit
|
||||
unsupported cases.
|
||||
`filletHistoryFromStep(baseStep, radius)` enables the first
|
||||
native dress-up slice: OCCT `BRepFilletAPI_MakeFillet` applies a fixed radius to
|
||||
all source edges and returns per-edge/face `Modified`, `Generated`, and `Deleted`
|
||||
relations with result-kind mapping. The native build enables `TKFillet` and its
|
||||
`TKBool` dependency.
|
||||
`chamferHistoryFromStep(baseStep, distance)` uses the same native history
|
||||
collector with `BRepFilletAPI_MakeChamfer` for a symmetric, fixed-distance,
|
||||
all-edge chamfer.
|
||||
`holeHistoryFromStep(baseStep, radius, depth, px, py, pz, dx, dy, dz)` composes
|
||||
an OCCT cylinder and `BRepAlgoAPI_Cut` for a straight cylindrical hole with
|
||||
explicit position and direction, returning the Boolean source history. Thread,
|
||||
counterbore, countersink, and face-attached hole semantics remain outside this
|
||||
minimal native contract.
|
||||
`draftHistoryFromStep(baseStep, faceIndex, angle, direction, neutralPlane,
|
||||
reversed)` uses `BRepOffsetAPI_DraftAngle` from `TKOffset` for one explicitly
|
||||
selected source face. It returns native per-subshape history and rejects an
|
||||
invalid face index, zero direction, zero angle, or invalid neutral plane before
|
||||
publishing a result. Multi-face propagation and STEP files whose imported face
|
||||
orientation OCCT cannot draft remain explicit unsupported/error cases.
|
||||
`thicknessHistoryFromStep(baseStep, faceIndex, offset, intersectionJoin)` uses
|
||||
`BRepOffsetAPI_MakeThickSolid` for one explicitly removed source face and
|
||||
returns native offset history. Zero offsets and invalid face indexes are
|
||||
rejected before execution.
|
||||
`linearPatternHistoryFromStep(baseStep, dx, dy, dz)` creates one transformed
|
||||
copy and fuses it with the source, preserving both Boolean input histories.
|
||||
The contract intentionally covers exactly two whole-shape instances.
|
||||
`polarPatternHistoryFromStep(baseStep, axis, angle)` creates one rotated copy
|
||||
and fuses it with the source. It likewise covers exactly two whole-shape
|
||||
instances around an explicit finite axis.
|
||||
`mirroredHistoryFromStep(baseStep, planeOrigin, planeNormal)` creates one plane
|
||||
reflected copy and fuses it with the source. The native contract is limited to
|
||||
one whole-shape base, one standard document plane, and a fused result.
|
||||
The `multiTransformHistoryFromStep(baseStep, steps)` binding accepts two to six
|
||||
ordered two-instance linear, polar, or mirrored whole-shape steps. Each created
|
||||
instance retains its transformed Base subshape mapping; one multi-argument Fuse
|
||||
then aggregates every instance history back to the original Base indexes. The
|
||||
single-step protocol continues to reuse the individual verified bindings.
|
||||
|
||||
The TypeScript boundary mapper is `mapNativeOcctHistoryRecords`; the reusable
|
||||
`createNativeOcctStepHistoryBridge` exports Bitbybit ShapeHandles to STEP and
|
||||
|
||||
@@ -26,12 +26,12 @@ emcmake cmake -S "${OCCT_SOURCE_DIR}" -B "${OCCT_BUILD_DIR}" -G Ninja \
|
||||
-DBUILD_LIBRARY_TYPE=Static \
|
||||
-DBUILD_MODULE_FoundationClasses=ON \
|
||||
-DBUILD_MODULE_ModelingData=ON \
|
||||
-DBUILD_MODULE_ModelingAlgorithms=OFF \
|
||||
-DBUILD_MODULE_ModelingAlgorithms=ON \
|
||||
-DBUILD_MODULE_Visualization=OFF \
|
||||
-DBUILD_MODULE_ApplicationFramework=OFF \
|
||||
-DBUILD_MODULE_DataExchange=ON \
|
||||
-DBUILD_MODULE_Draw=OFF \
|
||||
-DBUILD_ADDITIONAL_TOOLKITS="TKBO;TKDESTEP" \
|
||||
-DBUILD_ADDITIONAL_TOOLKITS="TKBO;TKFillet;TKOffset;TKDESTEP" \
|
||||
-DBUILD_DOC_Overview=OFF \
|
||||
-DBUILD_Inspector=OFF \
|
||||
-DBUILD_SAMPLES_QT=OFF \
|
||||
@@ -42,7 +42,7 @@ emcmake cmake -S "${OCCT_SOURCE_DIR}" -B "${OCCT_BUILD_DIR}" -G Ninja \
|
||||
-DUSE_RAPIDJSON=OFF
|
||||
|
||||
cmake --build "${OCCT_BUILD_DIR}" --target \
|
||||
TKernel TKMath TKG2d TKG3d TKGeomBase TKGeomAlgo TKShHealing TKPrim TKTopAlgo TKBRep TKBO \
|
||||
TKernel TKMath TKG2d TKG3d TKGeomBase TKGeomAlgo TKShHealing TKPrim TKTopAlgo TKBRep TKBool TKBO TKFillet TKOffset \
|
||||
TKDE TKXSBase TKDESTEP \
|
||||
--parallel "${JOBS}"
|
||||
mkdir -p "${DIST_DIR}"
|
||||
@@ -53,6 +53,9 @@ em++ "${ROOT_DIR}/native/occt-history/occt_history.cpp" \
|
||||
-std=c++17 -O2 -fexceptions --bind \
|
||||
-Wl,--start-group \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBO.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKFillet.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKOffset.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBool.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKDESTEP.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKXSBase.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKDE.a" \
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,4 +19,121 @@ assert.equal(typeof response.hasDeleted, 'boolean')
|
||||
assert.equal(serializedResponse.provider, 'occt-native')
|
||||
assert.equal(serializedResponse.records.length > 0, true)
|
||||
assert.match(serializedResponse.resultStep, /^ISO-10303-21;/)
|
||||
console.log(JSON.stringify({ occtVersion: response.occtVersion, recordCount: response.records.length, hasModified: response.hasModified, hasGenerated: response.hasGenerated, hasDeleted: response.hasDeleted }, null, 2))
|
||||
const pocketBase = occt.makeBox(10, 10, 10)
|
||||
const pocketProfile = occt.makeRectangleFace(2, 3)
|
||||
const pocket = occt.pocketHistory(pocketBase, pocketProfile, 0, 0, 5)
|
||||
const pocketFromStep = occt.pocketHistoryFromStep(occt.shapeToStep(pocketBase), occt.shapeToStep(pocketProfile), 0, 0, 5)
|
||||
assert.equal(pocket.provider, 'occt-native')
|
||||
assert.equal(pocket.summary.isValid, true)
|
||||
assert.equal(pocket.summary.solids, 1)
|
||||
assert.ok(Math.abs(pocket.summary.volume - 970) <= 1e-7)
|
||||
assert.equal(pocket.records.length > 0, true)
|
||||
assert.equal(pocketFromStep.summary.isValid, true)
|
||||
assert.ok(Math.abs(pocketFromStep.summary.volume - 970) <= 1e-7)
|
||||
const revolution = occt.revolutionHistory(pocketProfile, -1, 0, 0, 0, 1, 0, 360)
|
||||
const revolutionFromStep = occt.revolutionHistoryFromStep(occt.shapeToStep(pocketProfile), -1, 0, 0, 0, 1, 0, 360)
|
||||
assert.equal(revolution.provider, 'occt-native')
|
||||
assert.equal(revolution.summary.isValid, true)
|
||||
assert.equal(revolution.summary.solids, 1)
|
||||
assert.equal(revolution.records.length > 0, true)
|
||||
assert.equal(revolutionFromStep.summary.isValid, true)
|
||||
assert.equal(revolutionFromStep.summary.solids, 1)
|
||||
const grooveBase = occt.makeBoxPlaced(10, 10, 8, -5, 0, -4)
|
||||
const grooveProfile = occt.makeRectangleFacePlaced(1, 3, 0.5, 2, 0)
|
||||
const groove = occt.grooveHistory(grooveBase, grooveProfile, 0, 0, 0, 0, 1, 0, 360)
|
||||
const grooveFromStep = occt.grooveHistoryFromStep(occt.shapeToStep(grooveBase), occt.shapeToStep(grooveProfile), 0, 0, 0, 0, 1, 0, 360)
|
||||
assert.equal(groove.provider, 'occt-native')
|
||||
assert.equal(groove.summary.isValid, true)
|
||||
assert.equal(groove.summary.solids, 1)
|
||||
assert.ok(Math.abs(groove.summary.volume - (800 - 6 * Math.PI)) <= 1e-7)
|
||||
assert.equal(groove.records.length > 0, true)
|
||||
assert.equal(grooveFromStep.summary.isValid, true)
|
||||
assert.ok(Math.abs(grooveFromStep.summary.volume - groove.summary.volume) <= 1e-7)
|
||||
const loftFirst = occt.makeRectangleFacePlaced(2, 2, 0, 0, 0)
|
||||
const loftSecond = occt.makeRectangleFacePlaced(2, 2, 0, 0, 5)
|
||||
const loftFromStep = occt.loftHistoryFromStep(occt.shapeToStep(loftFirst), occt.shapeToStep(loftSecond), false)
|
||||
assert.equal(loftFromStep.provider, 'occt-native')
|
||||
assert.equal(loftFromStep.summary.isValid, true)
|
||||
assert.equal(loftFromStep.summary.solids, 1)
|
||||
assert.ok(Math.abs(loftFromStep.summary.volume - 20) <= 1e-7)
|
||||
assert.equal(loftFromStep.records.length > 0, true)
|
||||
assert.equal(loftFromStep.records.some((record) => record.source === 'object'), true)
|
||||
assert.equal(loftFromStep.records.some((record) => record.source === 'tool'), true)
|
||||
const pipeProfile = occt.makeRectangleFace(2, 2)
|
||||
const pipeSpine = occt.makeLineWire(0, 0, 0, 0, 0, 5)
|
||||
const pipeFromStep = occt.pipeHistoryFromStep(occt.shapeToStep(pipeProfile), occt.shapeToStep(pipeSpine))
|
||||
assert.equal(pipeFromStep.provider, 'occt-native')
|
||||
assert.equal(pipeFromStep.summary.isValid, true)
|
||||
assert.equal(pipeFromStep.summary.solids, 1)
|
||||
assert.ok(Math.abs(pipeFromStep.summary.volume - 20) <= 1e-7)
|
||||
assert.equal(pipeFromStep.records.length > 0, true)
|
||||
assert.equal(pipeFromStep.records.some((record) => record.source === 'object'), true)
|
||||
assert.equal(pipeFromStep.records.some((record) => record.source === 'tool'), true)
|
||||
const filletBase = occt.makeBox(6, 6, 6)
|
||||
const filletFromStep = occt.filletHistoryFromStep(occt.shapeToStep(filletBase), 0.4)
|
||||
assert.equal(filletFromStep.provider, 'occt-native')
|
||||
assert.equal(filletFromStep.summary.isValid, true)
|
||||
assert.equal(filletFromStep.summary.solids, 1)
|
||||
assert.equal(filletFromStep.records.length > 0, true)
|
||||
assert.equal(filletFromStep.hasModified, true)
|
||||
assert.equal(filletFromStep.hasGenerated, true)
|
||||
assert.equal(filletFromStep.hasDeleted, true)
|
||||
const chamferFromStep = occt.chamferHistoryFromStep(occt.shapeToStep(filletBase), 0.4)
|
||||
assert.equal(chamferFromStep.provider, 'occt-native')
|
||||
assert.equal(chamferFromStep.summary.isValid, true)
|
||||
assert.equal(chamferFromStep.summary.solids, 1)
|
||||
assert.equal(chamferFromStep.records.length > 0, true)
|
||||
assert.equal(chamferFromStep.hasModified, true)
|
||||
assert.equal(chamferFromStep.hasGenerated, true)
|
||||
assert.equal(chamferFromStep.hasDeleted, true)
|
||||
const draftBase = occt.makeBox(10, 10, 10)
|
||||
const draftFromStep = occt.draftHistoryFromStep(occt.shapeToStep(draftBase), 0, 5, 0, 0, 1, 0, 0, 0, 0, 0, 1, false)
|
||||
assert.equal(draftFromStep.provider, 'occt-native')
|
||||
assert.equal(draftFromStep.summary.isValid, true)
|
||||
assert.equal(draftFromStep.summary.solids, 1)
|
||||
assert.equal(draftFromStep.records.length > 0, true)
|
||||
assert.equal(draftFromStep.hasModified, true)
|
||||
const thicknessBase = occt.makeBox(6, 6, 6)
|
||||
const thicknessFromStep = occt.thicknessHistoryFromStep(occt.shapeToStep(thicknessBase), 1, -0.4, false)
|
||||
assert.equal(thicknessFromStep.provider, 'occt-native')
|
||||
assert.equal(thicknessFromStep.summary.isValid, true)
|
||||
assert.equal(thicknessFromStep.summary.solids, 1)
|
||||
assert.equal(thicknessFromStep.records.length > 0, true)
|
||||
assert.equal(thicknessFromStep.hasModified, true)
|
||||
const linearPatternFromStep = occt.linearPatternHistoryFromStep(occt.shapeToStep(occt.makeBox(2, 2, 2)), 2, 0, 0)
|
||||
assert.equal(linearPatternFromStep.provider, 'occt-native')
|
||||
assert.equal(linearPatternFromStep.summary.isValid, true)
|
||||
assert.equal(linearPatternFromStep.summary.solids, 1)
|
||||
assert.ok(Math.abs(linearPatternFromStep.summary.volume - 16) <= 1e-7)
|
||||
assert.equal(linearPatternFromStep.records.length > 0, true)
|
||||
const polarPatternBase = occt.makeBoxPlaced(2, 1, 1, -1, -0.5, -0.5)
|
||||
const polarPatternFromStep = occt.polarPatternHistoryFromStep(occt.shapeToStep(polarPatternBase), 0, 0, 0, 0, 0, 1, 90)
|
||||
assert.equal(polarPatternFromStep.provider, 'occt-native')
|
||||
assert.equal(polarPatternFromStep.summary.isValid, true)
|
||||
assert.equal(polarPatternFromStep.summary.solids, 1)
|
||||
assert.ok(Math.abs(polarPatternFromStep.summary.volume - 3) <= 1e-7)
|
||||
assert.equal(polarPatternFromStep.records.length > 0, true)
|
||||
const mirroredBase = occt.makeBoxPlaced(2, 1, 1, -0.5, -0.5, -0.5)
|
||||
const mirroredFromStep = occt.mirroredHistoryFromStep(occt.shapeToStep(mirroredBase), 0, 0, 0, 1, 0, 0)
|
||||
assert.equal(mirroredFromStep.provider, 'occt-native')
|
||||
assert.equal(mirroredFromStep.summary.isValid, true)
|
||||
assert.equal(mirroredFromStep.summary.solids, 1)
|
||||
assert.ok(Math.abs(mirroredFromStep.summary.volume - 3) <= 1e-7)
|
||||
assert.equal(mirroredFromStep.records.length > 0, true)
|
||||
const orderedMultiTransform = occt.multiTransformHistoryFromStep(occt.shapeToStep(mirroredBase), [
|
||||
{ type: 'linear', direction: [1, 0, 0] },
|
||||
{ type: 'mirrored', axisOrigin: [0, 0, 0], direction: [1, 0, 0] },
|
||||
])
|
||||
assert.equal(orderedMultiTransform.provider, 'occt-native')
|
||||
assert.equal(orderedMultiTransform.summary.isValid, true)
|
||||
assert.equal(orderedMultiTransform.summary.solids, 1)
|
||||
assert.ok(Math.abs(orderedMultiTransform.summary.volume - 5) <= 1e-7)
|
||||
assert.equal(orderedMultiTransform.records.length > 0, true)
|
||||
const holeFromStep = occt.holeHistoryFromStep(occt.shapeToStep(occt.makeBox(10, 10, 10)), 1, 10, 0, 0, -5, 0, 0, 1)
|
||||
assert.equal(holeFromStep.provider, 'occt-native')
|
||||
assert.equal(holeFromStep.summary.isValid, true)
|
||||
assert.equal(holeFromStep.summary.solids, 1)
|
||||
assert.equal(holeFromStep.records.length > 0, true)
|
||||
assert.equal(holeFromStep.hasModified, true)
|
||||
assert.equal(holeFromStep.hasDeleted, true)
|
||||
console.log(JSON.stringify({ occtVersion: response.occtVersion, recordCount: response.records.length, hasModified: response.hasModified, hasGenerated: response.hasGenerated, hasDeleted: response.hasDeleted, pocket: { recordCount: pocket.records.length, volume: pocket.summary.volume, fromStepVolume: pocketFromStep.summary.volume, hasModified: pocket.hasModified, hasGenerated: pocket.hasGenerated, hasDeleted: pocket.hasDeleted }, revolution: { recordCount: revolution.records.length, volume: revolution.summary.volume, fromStepVolume: revolutionFromStep.summary.volume, hasModified: revolution.hasModified, hasGenerated: revolution.hasGenerated, hasDeleted: revolution.hasDeleted }, groove: { recordCount: groove.records.length, volume: groove.summary.volume, fromStepVolume: grooveFromStep.summary.volume, hasModified: groove.hasModified, hasGenerated: groove.hasGenerated, hasDeleted: groove.hasDeleted }, loft: { recordCount: loftFromStep.records.length, volume: loftFromStep.summary.volume, hasGenerated: loftFromStep.hasGenerated }, pipe: { recordCount: pipeFromStep.records.length, volume: pipeFromStep.summary.volume, hasModified: pipeFromStep.hasModified, hasGenerated: pipeFromStep.hasGenerated }, fillet: { recordCount: filletFromStep.records.length, volume: filletFromStep.summary.volume, hasModified: filletFromStep.hasModified, hasGenerated: filletFromStep.hasGenerated, hasDeleted: filletFromStep.hasDeleted }, chamfer: { recordCount: chamferFromStep.records.length, volume: chamferFromStep.summary.volume, hasModified: chamferFromStep.hasModified, hasGenerated: chamferFromStep.hasGenerated, hasDeleted: chamferFromStep.hasDeleted }, draft: { recordCount: draftFromStep.records.length, volume: draftFromStep.summary.volume, hasModified: draftFromStep.hasModified, hasGenerated: draftFromStep.hasGenerated, hasDeleted: draftFromStep.hasDeleted }, thickness: { recordCount: thicknessFromStep.records.length, volume: thicknessFromStep.summary.volume, hasModified: thicknessFromStep.hasModified, hasGenerated: thicknessFromStep.hasGenerated, hasDeleted: thicknessFromStep.hasDeleted }, mirrored: { recordCount: mirroredFromStep.records.length, volume: mirroredFromStep.summary.volume, hasModified: mirroredFromStep.hasModified, hasGenerated: mirroredFromStep.hasGenerated, hasDeleted: mirroredFromStep.hasDeleted }, orderedMultiTransform: { recordCount: orderedMultiTransform.records.length, volume: orderedMultiTransform.summary.volume, hasModified: orderedMultiTransform.hasModified, hasGenerated: orderedMultiTransform.hasGenerated, hasDeleted: orderedMultiTransform.hasDeleted }, hole: { recordCount: holeFromStep.records.length, volume: holeFromStep.summary.volume, hasModified: holeFromStep.hasModified, hasGenerated: holeFromStep.hasGenerated, hasDeleted: holeFromStep.hasDeleted } }, null, 2))
|
||||
|
||||
Reference in New Issue
Block a user