feat: establish reproducible FreeCAD web compatibility baseline

This commit is contained in:
2026-08-10 16:11:38 -04:00
parent e5a5d74dbc
commit b962a5c3b5
733 changed files with 349081 additions and 647 deletions

25
native/camotics-wasm/build.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
source_root="$repo_root/CAMotics/src"
output_dir="$repo_root/public/vendor/camotics"
test -f "$source_root/camotics/sim/ConicSweep.cpp"
test -f "$source_root/camotics/sim/SpheroidSweep.cpp"
command -v em++ >/dev/null
mkdir -p "$output_dir"
em++ \
"$repo_root/native/camotics-wasm/camotics_sweep.cpp" \
"$source_root/camotics/sim/Sweep.cpp" \
"$source_root/camotics/sim/ConicSweep.cpp" \
"$source_root/camotics/sim/SpheroidSweep.cpp" \
-I"$repo_root/native/camotics-wasm/include" \
-I"$source_root" \
-std=c++17 -O3 -flto -fno-exceptions \
-sSTANDALONE_WASM=1 \
-sERROR_ON_UNDEFINED_SYMBOLS=1 \
-sEXPORTED_FUNCTIONS='["_camotics_sweep_abi_version","_camotics_conic_depth","_camotics_spheroid_depth","_camotics_conic_bbox_count"]' \
-Wl,--no-entry \
-o "$output_dir/camotics-sweep.wasm"

View File

@@ -0,0 +1,44 @@
#include <camotics/sim/ConicSweep.h>
#include <camotics/sim/SpheroidSweep.h>
#include <emscripten/emscripten.h>
#include <vector>
using CAMotics::ConicSweep;
using CAMotics::SpheroidSweep;
using cb::Rectangle3D;
using cb::Vector3D;
extern "C" {
EMSCRIPTEN_KEEPALIVE int camotics_sweep_abi_version() {return 1;}
EMSCRIPTEN_KEEPALIVE double camotics_conic_depth(
double length, double topRadius, double bottomRadius,
double ax, double ay, double az,
double bx, double by, double bz,
double px, double py, double pz) {
return ConicSweep(length, topRadius, bottomRadius).depth(
Vector3D(ax, ay, az), Vector3D(bx, by, bz), Vector3D(px, py, pz));
}
EMSCRIPTEN_KEEPALIVE double camotics_spheroid_depth(
double radius, double length,
double ax, double ay, double az,
double bx, double by, double bz,
double px, double py, double pz) {
return SpheroidSweep(radius, length).depth(
Vector3D(ax, ay, az), Vector3D(bx, by, bz), Vector3D(px, py, pz));
}
EMSCRIPTEN_KEEPALIVE int camotics_conic_bbox_count(
double length, double topRadius, double bottomRadius,
double ax, double ay, double az,
double bx, double by, double bz,
double tolerance) {
std::vector<Rectangle3D> bounds;
ConicSweep(length, topRadius, bottomRadius).getBBoxes(
Vector3D(ax, ay, az), Vector3D(bx, by, bz), bounds, tolerance);
return bounds.size();
}
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include <algorithm>
#include <cmath>
namespace cb {
class Vector3D {
double values[3];
public:
Vector3D(double value = 0) : values{value, value, value} {}
Vector3D(double x, double y, double z) : values{x, y, z} {}
double &operator[](unsigned index) {return values[index];}
double operator[](unsigned index) const {return values[index];}
double x() const {return values[0];}
double y() const {return values[1];}
double z() const {return values[2];}
Vector3D operator+(const Vector3D &other) const {
return Vector3D(x() + other.x(), y() + other.y(), z() + other.z());
}
Vector3D operator-(const Vector3D &other) const {
return Vector3D(x() - other.x(), y() - other.y(), z() - other.z());
}
Vector3D operator*(double scalar) const {
return Vector3D(x() * scalar, y() * scalar, z() * scalar);
}
Vector3D &operator*=(const Vector3D &other) {
values[0] *= other.x();
values[1] *= other.y();
values[2] *= other.z();
return *this;
}
double dot(const Vector3D &other) const {
return x() * other.x() + y() * other.y() + z() * other.z();
}
double distance(const Vector3D &other) const {
const Vector3D delta = *this - other;
return std::sqrt(delta.dot(delta));
}
};
class Rectangle3D {
Vector3D minimum;
Vector3D maximum;
public:
Rectangle3D() = default;
Rectangle3D(const Vector3D &minimum, const Vector3D &maximum) :
minimum(minimum), maximum(maximum) {}
const Vector3D &getMin() const {return minimum;}
const Vector3D &getMax() const {return maximum;}
};
}

View File

@@ -0,0 +1 @@
#pragma once

View File

@@ -0,0 +1,3 @@
#pragma once
namespace GCode {class Move;}

View File

@@ -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

View File

@@ -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

View File

@@ -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))

91
native/planegcs/README.md Normal file
View File

@@ -0,0 +1,91 @@
# FreeCAD planegcs WASM
This directory builds the planegcs core from the locked FreeCAD 1.1.1 commit
`0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`. The source graph contains
`Geo.cpp`, `Constraints.cpp`, `SubSystem.cpp`, `qp_eq.cpp`, and `GCS.cpp`.
The browser-only stubs provide the FreeCAD console, elapsed-time, and export
macros needed by that graph; the upstream solver sources are not modified.
The build enables a two-thread pthread pool because upstream planegcs uses
`std::async`. Browser execution therefore requires COOP/COEP isolation and
`SharedArrayBuffer`. The checked-in artifact hashes and Chrome-only dedicated
Worker evidence are validated by the repository gates.
The current Embind functions are intentionally bounded artifact and solver
lifecycle surfaces. They prove that the real FreeCAD planegcs
`System::solve()` and `applySolution()` execute in WASM, while the Facade owns
snapshot validation, capability reporting, cancellation, stale-result
handling, and crash recovery. They are not a general arbitrary-constraint
graph adapter; that broader mapping remains in SK-06/SK-07.
`solveAngle` fixes the first endpoint and combines native
`addConstraintP2PDistance` and `addConstraintP2PAngle`. Its angle input is in
radians and its length must be positive. Arbitrary endpoint combinations,
multi-line angle constraints, and complete dimensional graphs remain outside
this smoke subset.
`solveCircleRadius` and `solveCircleDiameter` fix the circle center and use
the matching native `addConstraintCircleRadius` and
`addConstraintCircleDiameter` APIs for one positive dimension.
`solveEqualLines` uses the native `addConstraintEqualLength` contract for two
lines, fixing the first line and the second start point so the second length is
deterministic while its direction remains free. `solveEqualCircles` uses the
native `addConstraintEqualRadius` contract for two circles, fixing both centers
and the first radius. Mixed geometry, arcs, ellipses, arbitrary constraint
graphs, and reference dimensions remain outside this bounded subset.
`solveTangentCircles` uses native `addConstraintTangent(Circle, Circle)` for
two externally tangent circles with fixed centers and first radius. The
adapter does not claim line-circle, arc, internal-tangent, or multi-constraint
support from this function.
`solvePointSymmetry` uses native `addConstraintP2PSymmetric(Point, Point,
Point)` for three distinct point geometries. The first point and center are
fixed, so the second point is deterministic and the returned residual is the
midpoint-to-center distance. Line-axis symmetry, endpoint references and
multi-constraint graphs remain outside this bounded subset.
`solvePointOnLine` uses native `addConstraintPointOnLine(Point, Line)` for a
single horizontal or vertical line. The point coordinate along the line is
fixed and the orthogonal coordinate is solved; oblique lines, curve targets,
endpoint references and multi-constraint graphs remain outside this subset.
`solvePointOnCircle`, `solvePointOnArc`, and `solvePointOnEllipse` use the
matching native PointOnObject relation for one fixed target curve. The point X
coordinate is fixed and Y is solved. Arc results are accepted only when the
point lies in the declared counter-clockwise sweep. Ellipse support preserves
the center, focus-derived major radius, minor radius, and rotation.
`solvePointOnCubicBspline` uses native `addConstraintPointOnBSpline` for one
non-periodic, unit-weight, clamped cubic B-spline with four strictly
X-monotonic poles. It solves the curve parameter and point Y while fixing
point X. Other degrees, pole counts, weights, knot vectors, periodic curves,
and arbitrary B-spline graphs remain outside this bounded contract.
FreeCAD `Block` is implemented at the Facade pre-solve boundary, matching the
desktop Sketch behavior: all parameters of the single referenced geometry are
frozen and no synthetic GCS constraint is claimed.
`solveCoincidentLinePoints` directly uses native
`addConstraintP2PCoincident` and accepts all four start/end combinations for
two distinct non-degenerate lines. The first line is fixed and the second line
keeps its original length. `solveCoincidentLines` remains as the compatible
first-end to second-start wrapper.
`solveSnellsLawLines` maps FreeCAD's three-Line SnellsLaw graph. The selected
ray endpoints must already coincide on the boundary, matching the Coincident
and PointOnObject prerequisites created by the desktop Sketcher command. The
native binding preserves the first ray, boundary, and second-ray length, uses
FreeCAD's stored `n2/n1` ratio split and endpoint-dependent normal flips, then
solves `addConstraintSnellsLaw`. The Facade accepts this operation only when
the three prerequisite constraints form the exact compound graph.
```bash
./npmw run build:planegcs
./npmw run check:planegcs-artifact
./npmw run test:planegcs
./npmw run test:chrome-planegcs
```
FreeCAD planegcs source files retain their upstream LGPL licensing notices.

50
native/planegcs/build.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
FREECAD_SOURCE_DIR="${FREECAD_SOURCE_DIR:-${ROOT_DIR}/.cache/freecad/FreeCAD}"
PLANEGCS_SOURCE_DIR="${FREECAD_SOURCE_DIR}/src/Mod/Sketcher/App/planegcs"
DIST_DIR="${PLANEGCS_DIST_DIR:-${ROOT_DIR}/native/planegcs/dist}"
PUBLIC_DIR="${PLANEGCS_PUBLIC_DIR:-${ROOT_DIR}/public/native/planegcs}"
HOST_HEADERS_DIR="${ROOT_DIR}/.cache/bitbybit/planegcs-host-headers"
if [[ ! -f "${PLANEGCS_SOURCE_DIR}/GCS.cpp" ]]; then
echo "Locked FreeCAD planegcs source not found: ${PLANEGCS_SOURCE_DIR}" >&2
exit 1
fi
if ! command -v em++ >/dev/null; then
echo "Emscripten em++ is required." >&2
exit 1
fi
if [[ ! -d /usr/include/eigen3 ]] || [[ ! -d /usr/include/boost ]]; then
echo "Eigen3 and Boost headers are required." >&2
exit 1
fi
mkdir -p "${DIST_DIR}" "${PUBLIC_DIR}" "${HOST_HEADERS_DIR}"
if [[ ! -e "${HOST_HEADERS_DIR}/boost" ]]; then
ln -s /usr/include/boost "${HOST_HEADERS_DIR}/boost"
fi
em++ \
"${PLANEGCS_SOURCE_DIR}/Geo.cpp" \
"${PLANEGCS_SOURCE_DIR}/Constraints.cpp" \
"${PLANEGCS_SOURCE_DIR}/SubSystem.cpp" \
"${PLANEGCS_SOURCE_DIR}/qp_eq.cpp" \
"${PLANEGCS_SOURCE_DIR}/GCS.cpp" \
"${ROOT_DIR}/native/planegcs/planegcs_bindings.cpp" \
-I"${ROOT_DIR}/native/planegcs/stubs" \
-I"${FREECAD_SOURCE_DIR}/src" \
-I"${PLANEGCS_SOURCE_DIR}" \
-I"${HOST_HEADERS_DIR}" \
-I/usr/include/eigen3 \
-std=c++20 -O2 -DNDEBUG -DEIGEN_NO_DEBUG -fexceptions -pthread --bind \
-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web,worker,node \
-sPTHREAD_POOL_SIZE=2 \
-sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=16777216 -sMAXIMUM_MEMORY=536870912 \
-sNO_EXIT_RUNTIME=1 \
-o "${DIST_DIR}/freecad-planegcs.js"
cp "${ROOT_DIR}/native/planegcs/package.json" "${DIST_DIR}/package.json"
cp "${DIST_DIR}/freecad-planegcs.js" "${PUBLIC_DIR}/freecad-planegcs.js"
cp "${DIST_DIR}/freecad-planegcs.wasm" "${PUBLIC_DIR}/freecad-planegcs.wasm"
sha256sum "${DIST_DIR}/freecad-planegcs.js" "${DIST_DIR}/freecad-planegcs.wasm"

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
import createPlanegcs from './dist/freecad-planegcs.js'
const module = await createPlanegcs()
const result = module.solveHorizontalDistance(0, 0, 3, 4, 5)
const values = Array.from({ length: result.size() }, (_, index) => result.get(index))
result.delete()
const [status, startX, startY, endX, endY, residual] = values
if (status > 1 || Math.abs(startY - endY) > 1e-7 || Math.abs(residual) > 1e-7 || Math.abs(Math.hypot(endX - startX, endY - startY) - 5) > 1e-7) throw new Error(`planegcs smoke failed: ${JSON.stringify(values)}`)
const verticalResult = module.solveVerticalDistance(0, 0, 4, 3, 5)
const verticalValues = Array.from({ length: verticalResult.size() }, (_, index) => verticalResult.get(index))
verticalResult.delete()
const [verticalStatus, verticalStartX, verticalStartY, verticalEndX, verticalEndY, verticalResidual] = verticalValues
if (verticalStatus > 1 || Math.abs(verticalStartX - verticalEndX) > 1e-7 || Math.abs(verticalResidual) > 1e-7 || Math.abs(Math.hypot(verticalEndX - verticalStartX, verticalEndY - verticalStartY) - 5) > 1e-7) throw new Error(`planegcs vertical smoke failed: ${JSON.stringify(verticalValues)}`)
const distanceXResult = module.solveDistanceX(0, 0, 3, 4, 6)
const distanceXValues = Array.from({ length: distanceXResult.size() }, (_, index) => distanceXResult.get(index))
distanceXResult.delete()
const [distanceXStatus, distanceXStartX, distanceXStartY, distanceXEndX, distanceXEndY, distanceXResidual] = distanceXValues
if (distanceXStatus > 1 || Math.abs(distanceXEndX - distanceXStartX - 6) > 1e-7 || Math.abs(distanceXStartY - distanceXEndY) > 1e-7 || Math.abs(distanceXResidual) > 1e-7) throw new Error(`planegcs distanceX smoke failed: ${JSON.stringify(distanceXValues)}`)
const distanceYResult = module.solveDistanceY(0, 0, 3, 4, 6)
const distanceYValues = Array.from({ length: distanceYResult.size() }, (_, index) => distanceYResult.get(index))
distanceYResult.delete()
const [distanceYStatus, distanceYStartX, distanceYStartY, distanceYEndX, distanceYEndY, distanceYResidual] = distanceYValues
if (distanceYStatus > 1 || Math.abs(distanceYEndY - distanceYStartY - 6) > 1e-7 || Math.abs(distanceYStartX - distanceYEndX) > 1e-7 || Math.abs(distanceYResidual) > 1e-7) throw new Error(`planegcs distanceY smoke failed: ${JSON.stringify(distanceYValues)}`)
const angleResult = module.solveAngle(0, 0, 3, 4, 5, Math.PI / 4)
const angleValues = Array.from({ length: angleResult.size() }, (_, index) => angleResult.get(index))
angleResult.delete()
const [angleStatus, angleStartX, angleStartY, angleEndX, angleEndY, angleLengthResidual, angleResidual] = angleValues
if (angleStatus > 1 || Math.abs(Math.hypot(angleEndX - angleStartX, angleEndY - angleStartY) - 5) > 1e-7 || Math.abs(Math.atan2(angleEndY - angleStartY, angleEndX - angleStartX) - Math.PI / 4) > 1e-7 || Math.abs(angleLengthResidual) > 1e-7 || Math.abs(angleResidual) > 1e-7) throw new Error(`planegcs angle smoke failed: ${JSON.stringify(angleValues)}`)
const circleRadiusResult = module.solveCircleRadius(2, 3, 1, 4)
const circleRadiusValues = Array.from({ length: circleRadiusResult.size() }, (_, index) => circleRadiusResult.get(index))
circleRadiusResult.delete()
const [circleRadiusStatus, circleCenterX, circleCenterY, circleRadius, circleRadiusResidual] = circleRadiusValues
if (circleRadiusStatus > 1 || Math.abs(circleCenterX - 2) > 1e-7 || Math.abs(circleCenterY - 3) > 1e-7 || Math.abs(circleRadius - 4) > 1e-7 || Math.abs(circleRadiusResidual) > 1e-7) throw new Error(`planegcs circle radius smoke failed: ${JSON.stringify(circleRadiusValues)}`)
const circleDiameterResult = module.solveCircleDiameter(2, 3, 1, 8)
const circleDiameterValues = Array.from({ length: circleDiameterResult.size() }, (_, index) => circleDiameterResult.get(index))
circleDiameterResult.delete()
const [circleDiameterStatus, circleDiameterCenterX, circleDiameterCenterY, circleDiameterRadius, circleDiameterResidual] = circleDiameterValues
if (circleDiameterStatus > 1 || Math.abs(circleDiameterCenterX - 2) > 1e-7 || Math.abs(circleDiameterCenterY - 3) > 1e-7 || Math.abs(circleDiameterRadius * 2 - 8) > 1e-7 || Math.abs(circleDiameterResidual) > 1e-7) throw new Error(`planegcs circle diameter smoke failed: ${JSON.stringify(circleDiameterValues)}`)
const equalLinesResult = module.solveEqualLines(0, 0, 4, 0, 1, 2, 2, 5)
const equalLinesValues = Array.from({ length: equalLinesResult.size() }, (_, index) => equalLinesResult.get(index))
equalLinesResult.delete()
const [equalLinesStatus, equalLinesFirstStartX, equalLinesFirstStartY, equalLinesFirstEndX, equalLinesFirstEndY, equalLinesSecondStartX, equalLinesSecondStartY, equalLinesSecondEndX, equalLinesSecondEndY, equalLinesResidual] = equalLinesValues
if (equalLinesStatus > 1 || Math.abs(Math.hypot(equalLinesFirstEndX - equalLinesFirstStartX, equalLinesFirstEndY - equalLinesFirstStartY) - Math.hypot(equalLinesSecondEndX - equalLinesSecondStartX, equalLinesSecondEndY - equalLinesSecondStartY)) > 1e-7 || Math.abs(equalLinesResidual) > 1e-7) throw new Error(`planegcs equal lines smoke failed: ${JSON.stringify(equalLinesValues)}`)
const equalCirclesResult = module.solveEqualCircles(2, 3, 4, 8, 9, 1)
const equalCirclesValues = Array.from({ length: equalCirclesResult.size() }, (_, index) => equalCirclesResult.get(index))
equalCirclesResult.delete()
const [equalCirclesStatus, equalCirclesFirstCenterX, equalCirclesFirstCenterY, equalCirclesFirstRadius, equalCirclesSecondCenterX, equalCirclesSecondCenterY, equalCirclesSecondRadius, equalCirclesResidual] = equalCirclesValues
if (equalCirclesStatus > 1 || Math.abs(equalCirclesFirstCenterX - 2) > 1e-7 || Math.abs(equalCirclesFirstCenterY - 3) > 1e-7 || Math.abs(equalCirclesSecondCenterX - 8) > 1e-7 || Math.abs(equalCirclesSecondCenterY - 9) > 1e-7 || Math.abs(equalCirclesFirstRadius - 4) > 1e-7 || Math.abs(equalCirclesSecondRadius - 4) > 1e-7 || Math.abs(equalCirclesResidual) > 1e-7) throw new Error(`planegcs equal circles smoke failed: ${JSON.stringify(equalCirclesValues)}`)
const tangentCirclesResult = module.solveTangentCircles(2, 3, 4, 10, 3, 1)
const tangentCirclesValues = Array.from({ length: tangentCirclesResult.size() }, (_, index) => tangentCirclesResult.get(index))
tangentCirclesResult.delete()
const [tangentCirclesStatus, tangentCirclesFirstCenterX, tangentCirclesFirstCenterY, tangentCirclesFirstRadius, tangentCirclesSecondCenterX, tangentCirclesSecondCenterY, tangentCirclesSecondRadius, tangentCirclesResidual] = tangentCirclesValues
if (tangentCirclesStatus > 1 || Math.abs(tangentCirclesFirstCenterX - 2) > 1e-7 || Math.abs(tangentCirclesFirstCenterY - 3) > 1e-7 || Math.abs(tangentCirclesSecondCenterX - 10) > 1e-7 || Math.abs(tangentCirclesSecondCenterY - 3) > 1e-7 || Math.abs(tangentCirclesFirstRadius - 4) > 1e-7 || Math.abs(tangentCirclesSecondRadius - 4) > 1e-7 || Math.abs(tangentCirclesResidual) > 1e-7) throw new Error(`planegcs tangent circles smoke failed: ${JSON.stringify(tangentCirclesValues)}`)
const pointSymmetryResult = module.solvePointSymmetry(1, 2, 0, 0, 4, 6)
const pointSymmetryValues = Array.from({ length: pointSymmetryResult.size() }, (_, index) => pointSymmetryResult.get(index))
pointSymmetryResult.delete()
const [pointSymmetryStatus, pointSymmetryFirstX, pointSymmetryFirstY, pointSymmetrySecondX, pointSymmetrySecondY, pointSymmetryCenterX, pointSymmetryCenterY, pointSymmetryResidual] = pointSymmetryValues
if (pointSymmetryStatus > 1 || Math.abs(pointSymmetryFirstX - 1) > 1e-7 || Math.abs(pointSymmetryFirstY - 2) > 1e-7 || Math.abs(pointSymmetrySecondX - 7) > 1e-7 || Math.abs(pointSymmetrySecondY - 10) > 1e-7 || Math.abs(pointSymmetryCenterX - 4) > 1e-7 || Math.abs(pointSymmetryCenterY - 6) > 1e-7 || Math.abs(pointSymmetryResidual) > 1e-7) throw new Error(`planegcs point symmetry smoke failed: ${JSON.stringify(pointSymmetryValues)}`)
const pointOnLineResult = module.solvePointOnLine(2, 3, 0, 0, 4, 0, false)
const pointOnLineValues = Array.from({ length: pointOnLineResult.size() }, (_, index) => pointOnLineResult.get(index))
pointOnLineResult.delete()
const [pointOnLineStatus, pointOnLineX, pointOnLineY, pointOnLineStartX, pointOnLineStartY, pointOnLineEndX, pointOnLineEndY, pointOnLineResidual] = pointOnLineValues
if (pointOnLineStatus > 1 || Math.abs(pointOnLineX - 2) > 1e-7 || Math.abs(pointOnLineY) > 1e-7 || Math.abs(pointOnLineStartX) > 1e-7 || Math.abs(pointOnLineStartY) > 1e-7 || Math.abs(pointOnLineEndX - 4) > 1e-7 || Math.abs(pointOnLineEndY) > 1e-7 || Math.abs(pointOnLineResidual) > 1e-7) throw new Error(`planegcs point-on-line smoke failed: ${JSON.stringify(pointOnLineValues)}`)
const pointOnCircleResult = module.solvePointOnCircle(4, 6, 2, 3, 4)
const pointOnCircleValues = Array.from({ length: pointOnCircleResult.size() }, (_, index) => pointOnCircleResult.get(index))
pointOnCircleResult.delete()
const [pointOnCircleStatus, pointOnCircleX, pointOnCircleY, pointOnCircleCenterX, pointOnCircleCenterY, pointOnCircleRadius, pointOnCircleResidual] = pointOnCircleValues
if (pointOnCircleStatus > 1 || Math.abs(pointOnCircleX - 4) > 1e-7 || Math.abs(pointOnCircleY - 6.464101615137754) > 1e-7 || Math.abs(pointOnCircleCenterX - 2) > 1e-7 || Math.abs(pointOnCircleCenterY - 3) > 1e-7 || Math.abs(pointOnCircleRadius - 4) > 1e-7 || Math.abs(pointOnCircleResidual) > 1e-7) throw new Error(`planegcs point-on-circle smoke failed: ${JSON.stringify(pointOnCircleValues)}`)
const pointOnArcResult = module.solvePointOnArc(2, 6, 2, 3, 4, 0, Math.PI)
const pointOnArcValues = Array.from({ length: pointOnArcResult.size() }, (_, index) => pointOnArcResult.get(index))
pointOnArcResult.delete()
const [pointOnArcStatus, pointOnArcX, pointOnArcY, pointOnArcCenterX, pointOnArcCenterY, pointOnArcRadius, pointOnArcStartAngle, pointOnArcEndAngle, pointOnArcResidual] = pointOnArcValues
if (pointOnArcStatus > 1 || Math.abs(pointOnArcX - 2) > 1e-7 || Math.abs(pointOnArcY - 7) > 1e-7 || Math.abs(pointOnArcCenterX - 2) > 1e-7 || Math.abs(pointOnArcCenterY - 3) > 1e-7 || Math.abs(pointOnArcRadius - 4) > 1e-7 || Math.abs(pointOnArcStartAngle) > 1e-7 || Math.abs(pointOnArcEndAngle - Math.PI) > 1e-7 || Math.abs(pointOnArcResidual) > 1e-7) throw new Error(`planegcs point-on-arc smoke failed: ${JSON.stringify(pointOnArcValues)}`)
const pointOnEllipseResult = module.solvePointOnEllipse(2, 7, 2, 3, 6, 3, 3)
const pointOnEllipseValues = Array.from({ length: pointOnEllipseResult.size() }, (_, index) => pointOnEllipseResult.get(index))
pointOnEllipseResult.delete()
const [pointOnEllipseStatus, pointOnEllipseX, pointOnEllipseY, pointOnEllipseCenterX, pointOnEllipseCenterY, pointOnEllipseFocusX, pointOnEllipseFocusY, pointOnEllipseMinorRadius, pointOnEllipseResidual] = pointOnEllipseValues
if (pointOnEllipseStatus > 1 || Math.abs(pointOnEllipseX - 2) > 1e-7 || Math.abs(pointOnEllipseY - 6) > 1e-7 || Math.abs(pointOnEllipseCenterX - 2) > 1e-7 || Math.abs(pointOnEllipseCenterY - 3) > 1e-7 || Math.abs(pointOnEllipseFocusX - 6) > 1e-7 || Math.abs(pointOnEllipseFocusY - 3) > 1e-7 || Math.abs(pointOnEllipseMinorRadius - 3) > 1e-7 || Math.abs(pointOnEllipseResidual) > 1e-7) throw new Error(`planegcs point-on-ellipse smoke failed: ${JSON.stringify(pointOnEllipseValues)}`)
const ellipseAlignmentSolves = []
for (const alignmentType of [1, 2, 3, 4]) {
const alignmentResult = module.solveEllipseInternalAlignment(2, 3, 5, 3, 0.25, alignmentType)
const alignmentValues = Array.from({ length: alignmentResult.size() }, (_, index) => alignmentResult.get(index))
alignmentResult.delete()
const [alignmentStatus, alignmentCenterX, alignmentCenterY, alignmentFocusX, alignmentFocusY, alignmentMinorRadius, helperStartX, helperStartY, helperEndX, helperEndY, alignmentResidual] = alignmentValues
const focalDistance = 4
const major = { x: Math.cos(0.25), y: Math.sin(0.25) }
const minor = { x: -major.y, y: major.x }
const expectedStart = alignmentType === 1 ? { x: 2 + 5 * major.x, y: 3 + 5 * major.y } : alignmentType === 2 ? { x: 2 + 3 * minor.x, y: 3 + 3 * minor.y } : { x: 2 + (alignmentType === 3 ? 1 : -1) * focalDistance * major.x, y: 3 + (alignmentType === 3 ? 1 : -1) * focalDistance * major.y }
const expectedEnd = alignmentType === 1 ? { x: 2 - 5 * major.x, y: 3 - 5 * major.y } : alignmentType === 2 ? { x: 2 - 3 * minor.x, y: 3 - 3 * minor.y } : expectedStart
if (alignmentStatus > 1 || Math.abs(alignmentCenterX - 2) > 1e-7 || Math.abs(alignmentCenterY - 3) > 1e-7 || Math.abs(alignmentFocusX - (2 + focalDistance * major.x)) > 1e-7 || Math.abs(alignmentFocusY - (3 + focalDistance * major.y)) > 1e-7 || Math.abs(alignmentMinorRadius - 3) > 1e-7 || Math.abs(helperStartX - expectedStart.x) > 1e-7 || Math.abs(helperStartY - expectedStart.y) > 1e-7 || Math.abs(helperEndX - expectedEnd.x) > 1e-7 || Math.abs(helperEndY - expectedEnd.y) > 1e-7 || Math.abs(alignmentResidual) > 1e-7) throw new Error(`planegcs ellipse InternalAlignment smoke failed: ${JSON.stringify({ alignmentType, values: alignmentValues })}`)
ellipseAlignmentSolves.push({ alignmentType, solveStatus: alignmentStatus, residual: alignmentResidual })
}
const ellipseAlignmentSetResult = module.solveEllipseInternalAlignmentSet(2, 3, 5, 3, 0.25)
const ellipseAlignmentSetValues = Array.from({ length: ellipseAlignmentSetResult.size() }, (_, index) => ellipseAlignmentSetResult.get(index))
ellipseAlignmentSetResult.delete()
const [ellipseAlignmentSetStatus, ellipseAlignmentSetCenterX, ellipseAlignmentSetCenterY, ellipseAlignmentSetFocusX, ellipseAlignmentSetFocusY, ellipseAlignmentSetMinorRadius, ...ellipseAlignmentSetResiduals] = ellipseAlignmentSetValues
if (ellipseAlignmentSetStatus > 1 || Math.abs(ellipseAlignmentSetCenterX - 2) > 1e-7 || Math.abs(ellipseAlignmentSetCenterY - 3) > 1e-7 || Math.abs(ellipseAlignmentSetFocusX - (2 + 4 * Math.cos(0.25))) > 1e-7 || Math.abs(ellipseAlignmentSetFocusY - (3 + 4 * Math.sin(0.25))) > 1e-7 || Math.abs(ellipseAlignmentSetMinorRadius - 3) > 1e-7 || ellipseAlignmentSetResiduals.length !== 4 || ellipseAlignmentSetResiduals.some((value) => Math.abs(value) > 1e-7)) throw new Error(`planegcs ellipse InternalAlignment set smoke failed: ${JSON.stringify(ellipseAlignmentSetValues)}`)
const pointOnBsplineResult = module.solvePointOnCubicBspline(2, 1, 0, 0, 1, 2, 3, 2, 4, 0, 0.4)
const pointOnBsplineValues = Array.from({ length: pointOnBsplineResult.size() }, (_, index) => pointOnBsplineResult.get(index))
pointOnBsplineResult.delete()
const [pointOnBsplineStatus, pointOnBsplineX, pointOnBsplineY, pointOnBsplineParameter, pointOnBsplineResidualX, pointOnBsplineResidualY] = pointOnBsplineValues
if (pointOnBsplineStatus > 1 || Math.abs(pointOnBsplineX - 2) > 1e-7 || Math.abs(pointOnBsplineY - 1.5) > 1e-7 || Math.abs(pointOnBsplineParameter - 0.5) > 1e-7 || Math.abs(pointOnBsplineResidualX) > 1e-7 || Math.abs(pointOnBsplineResidualY) > 1e-7) throw new Error(`planegcs point-on-B-spline smoke failed: ${JSON.stringify(pointOnBsplineValues)}`)
const bsplineWeightResult = module.solveCubicBsplineWeight(0, 0, 1, 2, 3, 2, 4, 0, 1, 0.75, 1.25, 1, 1, 1.5)
const bsplineWeightValues = Array.from({ length: bsplineWeightResult.size() }, (_, index) => bsplineWeightResult.get(index))
bsplineWeightResult.delete()
const [bsplineWeightStatus, bsplineWeight0, bsplineWeight1, bsplineWeight2, bsplineWeight3, bsplineHelperCenterX, bsplineHelperCenterY, bsplineHelperRadius, bsplineAlignmentResidual, bsplineWeightResidual] = bsplineWeightValues
if (bsplineWeightStatus > 1 || Math.abs(bsplineWeight0 - 1) > 1e-7 || Math.abs(bsplineWeight1 - 1.5) > 1e-7 || Math.abs(bsplineWeight2 - 1.25) > 1e-7 || Math.abs(bsplineWeight3 - 1) > 1e-7 || Math.abs(bsplineHelperCenterX - 1) > 1e-7 || Math.abs(bsplineHelperCenterY - 2) > 1e-7 || Math.abs(bsplineHelperRadius - 1.5) > 1e-7 || Math.abs(bsplineAlignmentResidual) > 1e-7 || Math.abs(bsplineWeightResidual) > 1e-7) throw new Error(`planegcs B-spline Weight smoke failed: ${JSON.stringify(bsplineWeightValues)}`)
for (const firstEnd of [false, true]) {
for (const secondEnd of [false, true]) {
const coincidentResult = module.solveCoincidentLinePoints(0, 0, 4, 0, 0, 2, 3, 6, 5, firstEnd, secondEnd)
const coincidentValues = Array.from({ length: coincidentResult.size() }, (_, index) => coincidentResult.get(index))
coincidentResult.delete()
const [coincidentStatus, firstStartX, firstStartY, firstEndX, firstEndY, secondStartX, secondStartY, secondEndX, secondEndY, coincidentResidual] = coincidentValues
const selectedFirstX = firstEnd ? firstEndX : firstStartX
const selectedFirstY = firstEnd ? firstEndY : firstStartY
const selectedSecondX = secondEnd ? secondEndX : secondStartX
const selectedSecondY = secondEnd ? secondEndY : secondStartY
if (coincidentStatus > 1 || Math.abs(selectedFirstX - selectedSecondX) > 1e-7 || Math.abs(selectedFirstY - selectedSecondY) > 1e-7 || Math.abs(coincidentResidual) > 1e-7 || Math.abs(Math.hypot(secondEndX - secondStartX, secondEndY - secondStartY) - 5) > 1e-7) throw new Error(`planegcs selectable coincident smoke failed: ${JSON.stringify({ firstEnd, secondEnd, values: coincidentValues })}`)
}
}
const snellsLawResult = module.solveSnellsLawLines(0, 0, -4, 4, 0, 0, 4, 3, -5, 0, 5, 0, 1.5, false, false)
const snellsLawValues = Array.from({ length: snellsLawResult.size() }, (_, index) => snellsLawResult.get(index))
snellsLawResult.delete()
const [snellsLawStatus, snellsFirstStartX, snellsFirstStartY, snellsFirstEndX, snellsFirstEndY, snellsSecondStartX, snellsSecondStartY, snellsSecondEndX, snellsSecondEndY, snellsBoundaryStartX, snellsBoundaryStartY, snellsBoundaryEndX, snellsBoundaryEndY, snellsLawResidual] = snellsLawValues
if (snellsLawStatus > 1 || Math.abs(snellsFirstStartX - snellsSecondStartX) > 1e-7 || Math.abs(snellsFirstStartY - snellsSecondStartY) > 1e-7 || Math.abs(Math.hypot(snellsSecondEndX - snellsSecondStartX, snellsSecondEndY - snellsSecondStartY) - 5) > 1e-7 || Math.abs(snellsLawResidual) > 1e-7 || Math.abs(snellsBoundaryStartY) > 1e-7 || Math.abs(snellsBoundaryEndY) > 1e-7) throw new Error(`planegcs SnellsLaw smoke failed: ${JSON.stringify(snellsLawValues)}`)
const parallelResult = module.solveParallelLines(0, 0, 4, 0, 0, 2, 3, 6, 5)
const parallelValues = Array.from({ length: parallelResult.size() }, (_, index) => parallelResult.get(index))
parallelResult.delete()
const [parallelStatus, parallelFirstStartX, parallelFirstStartY, parallelFirstEndX, parallelFirstEndY, parallelSecondStartX, parallelSecondStartY, parallelSecondEndX, parallelSecondEndY, parallelResidual] = parallelValues
const parallelCross = (parallelFirstEndX - parallelFirstStartX) * (parallelSecondEndY - parallelSecondStartY) - (parallelFirstEndY - parallelFirstStartY) * (parallelSecondEndX - parallelSecondStartX)
if (parallelStatus > 1 || Math.abs(parallelCross) > 1e-7 || Math.abs(parallelResidual) > 1e-7 || Math.abs(Math.hypot(parallelSecondEndX - parallelSecondStartX, parallelSecondEndY - parallelSecondStartY) - 5) > 1e-7) throw new Error(`planegcs parallel smoke failed: ${JSON.stringify(parallelValues)}`)
const perpendicularResult = module.solvePerpendicularLines(0, 0, 4, 0, 0, 2, 3, 6, 5)
const perpendicularValues = Array.from({ length: perpendicularResult.size() }, (_, index) => perpendicularResult.get(index))
perpendicularResult.delete()
const [perpendicularStatus, perpendicularFirstStartX, perpendicularFirstStartY, perpendicularFirstEndX, perpendicularFirstEndY, perpendicularSecondStartX, perpendicularSecondStartY, perpendicularSecondEndX, perpendicularSecondEndY, perpendicularResidual] = perpendicularValues
const perpendicularDot = (perpendicularFirstEndX - perpendicularFirstStartX) * (perpendicularSecondEndX - perpendicularSecondStartX) + (perpendicularFirstEndY - perpendicularFirstStartY) * (perpendicularSecondEndY - perpendicularSecondStartY)
if (perpendicularStatus > 1 || Math.abs(perpendicularDot) > 1e-7 || Math.abs(perpendicularResidual) > 1e-7 || Math.abs(Math.hypot(perpendicularSecondEndX - perpendicularSecondStartX, perpendicularSecondEndY - perpendicularSecondStartY) - 5) > 1e-7) throw new Error(`planegcs perpendicular smoke failed: ${JSON.stringify(perpendicularValues)}`)
console.log(JSON.stringify({ status: 'planegcs-smoke-pass', solveStatus: status, line: { startX, startY, endX, endY }, residual, vertical: { solveStatus: verticalStatus, startX: verticalStartX, startY: verticalStartY, endX: verticalEndX, endY: verticalEndY, residual: verticalResidual }, distanceX: { solveStatus: distanceXStatus, startX: distanceXStartX, startY: distanceXStartY, endX: distanceXEndX, endY: distanceXEndY, residual: distanceXResidual }, distanceY: { solveStatus: distanceYStatus, startX: distanceYStartX, startY: distanceYStartY, endX: distanceYEndX, endY: distanceYEndY, residual: distanceYResidual }, angle: { solveStatus: angleStatus, startX: angleStartX, startY: angleStartY, endX: angleEndX, endY: angleEndY, lengthResidual: angleLengthResidual, residual: angleResidual }, circleRadius: { solveStatus: circleRadiusStatus, centerX: circleCenterX, centerY: circleCenterY, radius: circleRadius, residual: circleRadiusResidual }, circleDiameter: { solveStatus: circleDiameterStatus, centerX: circleDiameterCenterX, centerY: circleDiameterCenterY, radius: circleDiameterRadius, residual: circleDiameterResidual }, equalLines: { solveStatus: equalLinesStatus, firstLength: Math.hypot(equalLinesFirstEndX - equalLinesFirstStartX, equalLinesFirstEndY - equalLinesFirstStartY), secondLength: Math.hypot(equalLinesSecondEndX - equalLinesSecondStartX, equalLinesSecondEndY - equalLinesSecondStartY), residual: equalLinesResidual }, equalCircles: { solveStatus: equalCirclesStatus, firstRadius: equalCirclesFirstRadius, secondRadius: equalCirclesSecondRadius, residual: equalCirclesResidual }, tangentCircles: { solveStatus: tangentCirclesStatus, firstRadius: tangentCirclesFirstRadius, secondRadius: tangentCirclesSecondRadius, residual: tangentCirclesResidual }, snellsLaw: { solveStatus: snellsLawStatus, ratio: 1.5, junction: { x: snellsSecondStartX, y: snellsSecondStartY }, secondEnd: { x: snellsSecondEndX, y: snellsSecondEndY }, boundary: { startX: snellsBoundaryStartX, startY: snellsBoundaryStartY, endX: snellsBoundaryEndX, endY: snellsBoundaryEndY }, residual: snellsLawResidual }, ellipseInternalAlignment: ellipseAlignmentSolves, bsplineWeight: { solveStatus: bsplineWeightStatus, weights: [bsplineWeight0, bsplineWeight1, bsplineWeight2, bsplineWeight3], helper: { centerX: bsplineHelperCenterX, centerY: bsplineHelperCenterY, radius: bsplineHelperRadius }, alignmentResidual: bsplineAlignmentResidual, residual: bsplineWeightResidual }, parallel: { solveStatus: parallelStatus, startX: parallelSecondStartX, startY: parallelSecondStartY, endX: parallelSecondEndX, endY: parallelSecondEndY, residual: parallelResidual }, perpendicular: { solveStatus: perpendicularStatus, startX: perpendicularSecondStartX, startY: perpendicularSecondStartY, endX: perpendicularSecondEndX, endY: perpendicularSecondEndY, residual: perpendicularResidual } }, null, 2))

View File

@@ -0,0 +1,40 @@
#ifndef WEB_FREECAD_BASE_CONSOLE_H
#define WEB_FREECAD_BASE_CONSOLE_H
#include <chrono>
namespace Base
{
class TimeElapsed: public std::chrono::time_point<std::chrono::steady_clock>
{
public:
TimeElapsed()
: std::chrono::time_point<std::chrono::steady_clock>(std::chrono::steady_clock::now())
{}
static float diffTimeF(const TimeElapsed& start, const TimeElapsed& end = TimeElapsed())
{
return std::chrono::duration<float>(end - start).count();
}
};
class ConsoleSingleton
{
public:
template<typename... Args>
void log(const char*, Args&&...)
{}
template<typename... Args>
void warning(const char*, Args&&...)
{}
};
inline ConsoleSingleton& Console()
{
static ConsoleSingleton console;
return console;
}
} // namespace Base
#endif

View File

@@ -0,0 +1,3 @@
#ifndef WEB_FREECAD_FCCONFIG_H
#define WEB_FREECAD_FCCONFIG_H
#endif

View File

@@ -0,0 +1,6 @@
#ifndef WEB_FREECAD_FCGLOBAL_H
#define WEB_FREECAD_FCGLOBAL_H
#define FREECAD_DECL_EXPORT
#define FREECAD_DECL_IMPORT
#define BaseExport
#endif