feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
25
native/camotics-wasm/build.sh
Executable file
25
native/camotics-wasm/build.sh
Executable 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"
|
||||
44
native/camotics-wasm/camotics_sweep.cpp
Normal file
44
native/camotics-wasm/camotics_sweep.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
56
native/camotics-wasm/include/cbang/geom/Rectangle.h
Normal file
56
native/camotics-wasm/include/cbang/geom/Rectangle.h
Normal 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;}
|
||||
};
|
||||
}
|
||||
1
native/camotics-wasm/include/cbang/log/Logger.h
Normal file
1
native/camotics-wasm/include/cbang/log/Logger.h
Normal file
@@ -0,0 +1 @@
|
||||
#pragma once
|
||||
3
native/camotics-wasm/include/gcode/Move.h
Normal file
3
native/camotics-wasm/include/gcode/Move.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
namespace GCode {class Move;}
|
||||
@@ -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))
|
||||
|
||||
91
native/planegcs/README.md
Normal file
91
native/planegcs/README.md
Normal 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
50
native/planegcs/build.sh
Executable 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"
|
||||
3
native/planegcs/package.json
Normal file
3
native/planegcs/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
1142
native/planegcs/planegcs_bindings.cpp
Normal file
1142
native/planegcs/planegcs_bindings.cpp
Normal file
File diff suppressed because it is too large
Load Diff
138
native/planegcs/smoke-test.mjs
Normal file
138
native/planegcs/smoke-test.mjs
Normal 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))
|
||||
40
native/planegcs/stubs/Base/Console.h
Normal file
40
native/planegcs/stubs/Base/Console.h
Normal 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
|
||||
3
native/planegcs/stubs/FCConfig.h
Normal file
3
native/planegcs/stubs/FCConfig.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#ifndef WEB_FREECAD_FCCONFIG_H
|
||||
#define WEB_FREECAD_FCCONFIG_H
|
||||
#endif
|
||||
6
native/planegcs/stubs/FCGlobal.h
Normal file
6
native/planegcs/stubs/FCGlobal.h
Normal 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
|
||||
Reference in New Issue
Block a user