feat: advance FreeCAD parity and native OCCT history

This commit is contained in:
2026-08-03 08:04:10 -04:00
parent 2ce261b982
commit e5a5d74dbc
32 changed files with 1883 additions and 30 deletions

View File

@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.20)
project(bitbybit_occt_history LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT OCCT_DIR)
message(FATAL_ERROR "OCCT_DIR must point to an Emscripten OCCT build containing OpenCASCADEConfig.cmake")
endif()
find_package(OpenCASCADE REQUIRED CONFIG PATHS "${OCCT_DIR}" NO_DEFAULT_PATH)
add_executable(bitbybit_occt_history occt_history.cpp)
target_include_directories(bitbybit_occt_history PRIVATE "${OCCT_DIR}/include/opencascade")
target_link_libraries(bitbybit_occt_history PRIVATE TKDESTEP TKXSBase TKDE TKBO TKBRep TKTopAlgo TKPrim TKShHealing TKGeomAlgo TKGeomBase TKG3d TKG2d TKMath TKernel)
target_compile_options(bitbybit_occt_history PRIVATE -fexceptions -O2)
target_link_options(bitbybit_occt_history PRIVATE
--bind
-fexceptions
-sMODULARIZE=1
-sEXPORT_ES6=1
-sENVIRONMENT=web,worker,node
-sALLOW_MEMORY_GROWTH=1
-sINITIAL_MEMORY=134217728
-sMAXIMUM_MEMORY=2147483648
-sNO_EXIT_RUNTIME=1
)
set_target_properties(bitbybit_occt_history PROPERTIES OUTPUT_NAME "bitbybit-occt-history")

View File

@@ -0,0 +1,34 @@
# 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
WASM instance. A handle from `@bitbybit-dev/occt` must never be passed directly
to this module because WebAssembly linear memories are independent.
Build and verify it with the project wrapper:
```bash
./npmw run build:occt-history
./npmw run test:occt-history
```
The build also publishes the JS/WASM pair to
`public/native/occt-history/`, which is the default URL used by
`NativeOcctHistoryWorkerProvider`. The generated artifacts remain ignored; a
clean checkout must run the build after restoring the pinned OCCT source and
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.
The TypeScript boundary mapper is `mapNativeOcctHistoryRecords`; the reusable
`createNativeOcctStepHistoryBridge` exports Bitbybit ShapeHandles to STEP and
converts provider source names into document object IDs before calling
`captureNativeTopologyHistory`. The direct handle API remains for calls that
stay inside this single native WASM instance.

79
native/occt-history/build.sh Executable file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
OCCT_SOURCE_DIR="${OCCT_SOURCE_DIR:-/home/mes123456/working/WebCam/occt}"
OCCT_BUILD_DIR="${OCCT_BUILD_DIR:-${ROOT_DIR}/.cache/bitbybit/occt-history-build}"
DIST_DIR="${OCCT_HISTORY_DIST_DIR:-${ROOT_DIR}/native/occt-history/dist}"
PUBLIC_DIR="${OCCT_HISTORY_PUBLIC_DIR:-${ROOT_DIR}/public/native/occt-history}"
JOBS="${JOBS:-$(nproc)}"
if [[ ! -f "${OCCT_SOURCE_DIR}/CMakeLists.txt" ]]; then
echo "OCCT source checkout not found: ${OCCT_SOURCE_DIR}" >&2
exit 1
fi
if ! command -v emcmake >/dev/null || ! command -v em++ >/dev/null; then
echo "Emscripten emcmake/em++ are required." >&2
exit 1
fi
emcmake cmake -S "${OCCT_SOURCE_DIR}" -B "${OCCT_BUILD_DIR}" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MAKE_PROGRAM="$(command -v ninja)" \
-DCMAKE_CXX_FLAGS="-fexceptions" \
-DCMAKE_CXX_FLAGS_RELEASE="-O0 -DNDEBUG -fexceptions" \
-DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \
-DBUILD_LIBRARY_TYPE=Static \
-DBUILD_MODULE_FoundationClasses=ON \
-DBUILD_MODULE_ModelingData=ON \
-DBUILD_MODULE_ModelingAlgorithms=OFF \
-DBUILD_MODULE_Visualization=OFF \
-DBUILD_MODULE_ApplicationFramework=OFF \
-DBUILD_MODULE_DataExchange=ON \
-DBUILD_MODULE_Draw=OFF \
-DBUILD_ADDITIONAL_TOOLKITS="TKBO;TKDESTEP" \
-DBUILD_DOC_Overview=OFF \
-DBUILD_Inspector=OFF \
-DBUILD_SAMPLES_QT=OFF \
-DBUILD_USE_PCH=OFF \
-DUSE_FREETYPE=OFF \
-DUSE_TK=OFF \
-DUSE_TBB=OFF \
-DUSE_RAPIDJSON=OFF
cmake --build "${OCCT_BUILD_DIR}" --target \
TKernel TKMath TKG2d TKG3d TKGeomBase TKGeomAlgo TKShHealing TKPrim TKTopAlgo TKBRep TKBO \
TKDE TKXSBase TKDESTEP \
--parallel "${JOBS}"
mkdir -p "${DIST_DIR}"
em++ "${ROOT_DIR}/native/occt-history/occt_history.cpp" \
-I"${OCCT_BUILD_DIR}/include/opencascade" \
-I"${OCCT_SOURCE_DIR}/src" \
-std=c++17 -O2 -fexceptions --bind \
-Wl,--start-group \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBO.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" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBRep.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKTopAlgo.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKPrim.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKShHealing.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKGeomAlgo.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKGeomBase.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKG3d.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKG2d.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKMath.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKernel.a" \
-Wl,--end-group \
-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web,worker,node \
-sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=134217728 -sMAXIMUM_MEMORY=2147483648 \
-sNO_EXIT_RUNTIME=1 \
-o "${DIST_DIR}/bitbybit-occt-history.js"
cp "${ROOT_DIR}/native/occt-history/package.json" "${DIST_DIR}/package.json"
mkdir -p "${PUBLIC_DIR}"
cp "${DIST_DIR}/bitbybit-occt-history.js" "${PUBLIC_DIR}/bitbybit-occt-history.js"
cp "${DIST_DIR}/bitbybit-occt-history.wasm" "${PUBLIC_DIR}/bitbybit-occt-history.wasm"
sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm"

View File

@@ -0,0 +1,247 @@
#include <BRepAlgoAPI_Common.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <Standard_Version.hxx>
#include <STEPControl_Reader.hxx>
#include <STEPControl_Writer.hxx>
#include <STEPControl_StepModelType.hxx>
#include <TopAbs_ShapeEnum.hxx>
#include <TopExp_Explorer.hxx>
#include <TopoDS_Shape.hxx>
#include <emscripten/bind.h>
#include <stdexcept>
#include <sstream>
#include <string>
#include <vector>
namespace
{
struct HistoryRecord
{
std::string relation;
std::string kind;
int sourceIndex = -1;
int resultIndex = -1;
};
std::string kindName(const TopAbs_ShapeEnum theKind)
{
switch (theKind)
{
case TopAbs_VERTEX: return "vertex";
case TopAbs_EDGE: return "edge";
case TopAbs_WIRE: return "wire";
case TopAbs_FACE: return "face";
case TopAbs_SHELL: return "shell";
case TopAbs_SOLID: return "solid";
case TopAbs_COMPSOLID: return "compsolid";
case TopAbs_COMPOUND: return "compound";
default: return "shape";
}
}
std::vector<TopoDS_Shape> subShapes(const TopoDS_Shape& theShape, const TopAbs_ShapeEnum theKind)
{
std::vector<TopoDS_Shape> aShapes;
for (TopExp_Explorer anExplorer(theShape, theKind); anExplorer.More(); anExplorer.Next())
{
aShapes.push_back(anExplorer.Current());
}
return aShapes;
}
std::string writeStep(const TopoDS_Shape& theShape);
int resultIndex(const TopoDS_Shape& theShape,
const TopoDS_Shape& theResult,
const TopAbs_ShapeEnum theKind)
{
int anIndex = 0;
for (TopExp_Explorer anExplorer(theResult, theKind); anExplorer.More(); anExplorer.Next(), ++anIndex)
{
if (anExplorer.Current().IsSame(theShape))
{
return anIndex;
}
}
return -1;
}
template <typename TAlgorithm>
emscripten::val runBoolean(TAlgorithm& theAlgorithm,
const TopoDS_Shape& theObject,
const TopoDS_Shape& theTool)
{
theAlgorithm.Build();
if (!theAlgorithm.IsDone() || theAlgorithm.HasErrors())
{
throw std::runtime_error("OCCT boolean operation failed while collecting native history.");
}
const TopoDS_Shape aResult = theAlgorithm.Shape();
emscripten::val aRecords = emscripten::val::array();
const TopAbs_ShapeEnum aKinds[] = {TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE};
const TopoDS_Shape aSources[] = {theObject, theTool};
const char* aSourceNames[] = {"object", "tool"};
unsigned int aRecordIndex = 0;
for (int aSource = 0; aSource < 2; ++aSource)
{
for (const TopAbs_ShapeEnum aKind : aKinds)
{
const auto aSubShapes = subShapes(aSources[aSource], aKind);
for (std::size_t aSourceIndex = 0; aSourceIndex < aSubShapes.size(); ++aSourceIndex)
{
const TopoDS_Shape& anInput = aSubShapes[aSourceIndex];
const auto& aModified = theAlgorithm.Modified(anInput);
for (NCollection_List<TopoDS_Shape>::Iterator anIterator(aModified); anIterator.More(); anIterator.Next())
{
const int anOutputIndex = resultIndex(anIterator.Value(), aResult, aKind);
if (anOutputIndex < 0) continue;
emscripten::val aRecord = emscripten::val::object();
aRecord.set("relation", "modified");
aRecord.set("source", aSourceNames[aSource]);
aRecord.set("kind", kindName(aKind));
aRecord.set("sourceIndex", static_cast<int>(aSourceIndex));
aRecord.set("resultIndex", anOutputIndex);
aRecords.set(aRecordIndex++, aRecord);
}
const auto& aGenerated = theAlgorithm.Generated(anInput);
for (NCollection_List<TopoDS_Shape>::Iterator anIterator(aGenerated); anIterator.More(); anIterator.Next())
{
const int anOutputIndex = resultIndex(anIterator.Value(), aResult, aKind);
if (anOutputIndex < 0) continue;
emscripten::val aRecord = emscripten::val::object();
aRecord.set("relation", "generated");
aRecord.set("source", aSourceNames[aSource]);
aRecord.set("kind", kindName(aKind));
aRecord.set("sourceIndex", static_cast<int>(aSourceIndex));
aRecord.set("resultIndex", anOutputIndex);
aRecords.set(aRecordIndex++, aRecord);
}
if (theAlgorithm.IsDeleted(anInput))
{
emscripten::val aRecord = emscripten::val::object();
aRecord.set("relation", "deleted");
aRecord.set("source", aSourceNames[aSource]);
aRecord.set("kind", kindName(aKind));
aRecord.set("sourceIndex", static_cast<int>(aSourceIndex));
aRecord.set("resultIndex", -1);
aRecords.set(aRecordIndex++, aRecord);
}
}
}
}
emscripten::val aResponse = emscripten::val::object();
aResponse.set("provider", "occt-native");
aResponse.set("occtVersion", OCC_VERSION_COMPLETE);
aResponse.set("result", aResult);
aResponse.set("resultStep", writeStep(aResult));
aResponse.set("records", aRecords);
aResponse.set("hasModified", theAlgorithm.HasModified());
aResponse.set("hasGenerated", theAlgorithm.HasGenerated());
aResponse.set("hasDeleted", theAlgorithm.HasDeleted());
return aResponse;
}
TopoDS_Shape makeBox(const double theX, const double theY, const double theZ)
{
if (theX <= 0.0 || theY <= 0.0 || theZ <= 0.0)
{
throw std::invalid_argument("Box dimensions must be positive.");
}
return BRepPrimAPI_MakeBox(theX, theY, theZ).Shape();
}
std::string occtVersion()
{
return OCC_VERSION_COMPLETE;
}
emscripten::val booleanHistory(const TopoDS_Shape& theObject,
const TopoDS_Shape& theTool,
const std::string& theOperation)
{
if (theOperation == "fuse")
{
BRepAlgoAPI_Fuse anAlgorithm(theObject, theTool);
return runBoolean(anAlgorithm, theObject, theTool);
}
if (theOperation == "cut")
{
BRepAlgoAPI_Cut anAlgorithm(theObject, theTool);
return runBoolean(anAlgorithm, theObject, theTool);
}
if (theOperation == "common")
{
BRepAlgoAPI_Common anAlgorithm(theObject, theTool);
return runBoolean(anAlgorithm, theObject, theTool);
}
throw std::invalid_argument("Unsupported Boolean operation. Expected fuse, cut or common.");
}
TopoDS_Shape readStep(const std::string& theText)
{
if (theText.empty())
{
throw std::invalid_argument("STEP text must not be empty.");
}
std::istringstream aStream(theText);
STEPControl_Reader aReader;
const IFSelect_ReturnStatus aStatus = aReader.ReadStream("bitbybit.step", aStream);
if (aStatus != IFSelect_RetDone || aReader.NbRootsForTransfer() <= 0 || !aReader.TransferRoots())
{
throw std::runtime_error("OCCT could not read and transfer the supplied STEP text.");
}
const TopoDS_Shape aShape = aReader.OneShape();
if (aShape.IsNull())
{
throw std::runtime_error("OCCT STEP transfer produced a null shape.");
}
return aShape;
}
std::string writeStep(const TopoDS_Shape& theShape)
{
if (theShape.IsNull())
{
throw std::invalid_argument("Cannot serialize a null OCCT shape.");
}
STEPControl_Writer aWriter;
if (aWriter.Transfer(theShape, STEPControl_AsIs) != IFSelect_RetDone)
{
throw std::runtime_error("OCCT could not transfer the shape to STEP.");
}
std::ostringstream aStream;
if (aWriter.WriteStream(aStream) != IFSelect_RetDone)
{
throw std::runtime_error("OCCT could not write the shape to STEP.");
}
return aStream.str();
}
emscripten::val booleanHistoryFromStep(const std::string& theObjectStep,
const std::string& theToolStep,
const std::string& theOperation)
{
return booleanHistory(readStep(theObjectStep), readStep(theToolStep), theOperation);
}
}
EMSCRIPTEN_BINDINGS(bitbybit_occt_history)
{
emscripten::class_<TopoDS_Shape>("TopoDS_Shape")
.constructor<>()
.function("isNull", &TopoDS_Shape::IsNull)
.function("shapeType", &TopoDS_Shape::ShapeType)
.function("isSame", &TopoDS_Shape::IsSame);
emscripten::function("occtVersion", &occtVersion);
emscripten::function("makeBox", &makeBox);
emscripten::function("booleanHistory", &booleanHistory);
emscripten::function("shapeToStep", &writeStep);
emscripten::function("booleanHistoryFromStep", &booleanHistoryFromStep);
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,22 @@
import assert from 'node:assert/strict'
import createOcctHistory from './dist/bitbybit-occt-history.js'
const occt = await createOcctHistory()
assert.match(occt.occtVersion(), /^8\./)
const object = occt.makeBox(10, 10, 10)
const tool = occt.makeBox(5, 5, 5)
const response = occt.booleanHistory(object, tool, 'cut')
const objectStep = occt.shapeToStep(object)
const toolStep = occt.shapeToStep(tool)
assert.match(objectStep, /^ISO-10303-21;/)
assert.match(toolStep, /^ISO-10303-21;/)
const serializedResponse = occt.booleanHistoryFromStep(objectStep, toolStep, 'cut')
assert.equal(response.provider, 'occt-native')
assert.equal(response.records.every((record) => ['generated', 'modified', 'deleted'].includes(record.relation)), true)
assert.equal(typeof response.hasModified, 'boolean')
assert.equal(typeof response.hasGenerated, 'boolean')
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))