fluidcad 0.0.35 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -504
- package/README.md +1 -1
- package/bin/commands/login.js +33 -5
- package/bin/commands/mcp.js +3 -2
- package/bin/commands/publish.js +103 -8
- package/bin/lib/api-client.js +8 -0
- package/bin/lib/model-config.js +27 -4
- package/bin/lib/prompt.js +97 -0
- package/lib/dist/common/edge.d.ts +1 -1
- package/lib/dist/common/face.d.ts +1 -1
- package/lib/dist/common/scene-object.d.ts +6 -0
- package/lib/dist/common/scene-object.js +8 -0
- package/lib/dist/common/shape-factory.d.ts +1 -1
- package/lib/dist/common/shape-history-tracker.d.ts +1 -1
- package/lib/dist/common/shape.d.ts +1 -1
- package/lib/dist/common/solid.d.ts +1 -1
- package/lib/dist/common/transformable-primitive.d.ts +12 -1
- package/lib/dist/common/transformable-primitive.js +27 -0
- package/lib/dist/common/vertex.d.ts +1 -1
- package/lib/dist/common/wire.d.ts +1 -1
- package/lib/dist/core/2d/index.d.ts +1 -0
- package/lib/dist/core/2d/index.js +1 -0
- package/lib/dist/core/2d/text.d.ts +30 -0
- package/lib/dist/core/2d/text.js +37 -0
- package/lib/dist/core/helix.d.ts +20 -0
- package/lib/dist/core/helix.js +36 -0
- package/lib/dist/core/index.d.ts +3 -1
- package/lib/dist/core/index.js +2 -0
- package/lib/dist/core/interfaces.d.ts +180 -0
- package/lib/dist/core/wrap.d.ts +17 -0
- package/lib/dist/core/wrap.js +39 -0
- package/lib/dist/features/2d/text.d.ts +67 -0
- package/lib/dist/features/2d/text.js +320 -0
- package/lib/dist/features/cylinder.d.ts +3 -1
- package/lib/dist/features/cylinder.js +5 -2
- package/lib/dist/features/extrude-base.d.ts +1 -0
- package/lib/dist/features/extrude-to-face.d.ts +1 -0
- package/lib/dist/features/extrude-to-face.js +6 -0
- package/lib/dist/features/fillet.d.ts +1 -1
- package/lib/dist/features/helix.d.ts +41 -0
- package/lib/dist/features/helix.js +337 -0
- package/lib/dist/features/select.js +32 -8
- package/lib/dist/features/simple-extruder.d.ts +1 -1
- package/lib/dist/features/simple-extruder.js +7 -2
- package/lib/dist/features/sphere.d.ts +3 -1
- package/lib/dist/features/sphere.js +5 -2
- package/lib/dist/features/sweep.js +7 -2
- package/lib/dist/features/wrap.d.ts +39 -0
- package/lib/dist/features/wrap.js +116 -0
- package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
- package/lib/dist/filters/edge/belongs-to-face.js +14 -10
- package/lib/dist/filters/filter.d.ts +1 -1
- package/lib/dist/filters/from-object.d.ts +1 -1
- package/lib/dist/filters/tangent-expander.d.ts +1 -1
- package/lib/dist/filters/tangent-expander.js +57 -40
- package/lib/dist/helpers/scene-helpers.d.ts +2 -0
- package/lib/dist/helpers/scene-helpers.js +1 -1
- package/lib/dist/index.d.ts +2 -0
- package/lib/dist/index.js +3 -1
- package/lib/dist/io/file-import.d.ts +7 -0
- package/lib/dist/io/file-import.js +28 -1
- package/lib/dist/io/font-registry.d.ts +45 -0
- package/lib/dist/io/font-registry.js +272 -0
- package/lib/dist/math/bspline-interpolation.d.ts +29 -0
- package/lib/dist/math/bspline-interpolation.js +194 -0
- package/lib/dist/oc/boolean-ops.d.ts +3 -1
- package/lib/dist/oc/boolean-ops.js +15 -1
- package/lib/dist/oc/color-transfer.d.ts +1 -1
- package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
- package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
- package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
- package/lib/dist/oc/convert.d.ts +1 -1
- package/lib/dist/oc/draft-ops.d.ts +1 -1
- package/lib/dist/oc/edge-ops.d.ts +2 -2
- package/lib/dist/oc/edge-ops.js +13 -14
- package/lib/dist/oc/edge-props.d.ts +1 -1
- package/lib/dist/oc/edge-query.d.ts +1 -1
- package/lib/dist/oc/edge-query.js +3 -8
- package/lib/dist/oc/errors.d.ts +8 -0
- package/lib/dist/oc/errors.js +27 -0
- package/lib/dist/oc/explorer.d.ts +2 -2
- package/lib/dist/oc/extrude-ops.d.ts +28 -2
- package/lib/dist/oc/extrude-ops.js +56 -7
- package/lib/dist/oc/face-ops.d.ts +2 -1
- package/lib/dist/oc/face-ops.js +11 -0
- package/lib/dist/oc/face-props.d.ts +1 -1
- package/lib/dist/oc/face-query.d.ts +12 -1
- package/lib/dist/oc/face-query.js +39 -0
- package/lib/dist/oc/fillet-ops.d.ts +1 -1
- package/lib/dist/oc/fillet-ops.js +4 -4
- package/lib/dist/oc/geometry.d.ts +1 -1
- package/lib/dist/oc/geometry.js +12 -14
- package/lib/dist/oc/helix-ops.d.ts +37 -0
- package/lib/dist/oc/helix-ops.js +88 -0
- package/lib/dist/oc/hit-test.d.ts +1 -1
- package/lib/dist/oc/index.d.ts +4 -0
- package/lib/dist/oc/index.js +2 -0
- package/lib/dist/oc/init.d.ts +1 -1
- package/lib/dist/oc/init.js +1 -1
- package/lib/dist/oc/intersection.js +1 -1
- package/lib/dist/oc/io.d.ts +6 -6
- package/lib/dist/oc/io.js +31 -24
- package/lib/dist/oc/measure/classify.d.ts +34 -0
- package/lib/dist/oc/measure/classify.js +246 -0
- package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
- package/lib/dist/oc/measure/measure-ops.js +210 -0
- package/lib/dist/oc/measure/measure-types.d.ts +39 -0
- package/lib/dist/oc/measure/measure-types.js +1 -0
- package/lib/dist/oc/measure/sampling.d.ts +9 -0
- package/lib/dist/oc/measure/sampling.js +77 -0
- package/lib/dist/oc/measure/vec.d.ts +13 -0
- package/lib/dist/oc/measure/vec.js +23 -0
- package/lib/dist/oc/mesh.d.ts +1 -1
- package/lib/dist/oc/mesh.js +40 -28
- package/lib/dist/oc/path-sampler.d.ts +29 -0
- package/lib/dist/oc/path-sampler.js +63 -0
- package/lib/dist/oc/props.d.ts +1 -1
- package/lib/dist/oc/props.js +4 -1
- package/lib/dist/oc/shape-hash.d.ts +26 -0
- package/lib/dist/oc/shape-hash.js +32 -0
- package/lib/dist/oc/shape-ops.d.ts +5 -3
- package/lib/dist/oc/shape-ops.js +6 -5
- package/lib/dist/oc/sweep-ops.d.ts +22 -1
- package/lib/dist/oc/sweep-ops.js +206 -18
- package/lib/dist/oc/text-outline.d.ts +62 -0
- package/lib/dist/oc/text-outline.js +212 -0
- package/lib/dist/oc/topology-index.d.ts +1 -1
- package/lib/dist/oc/vertex-ops.d.ts +1 -1
- package/lib/dist/oc/wire-ops.d.ts +1 -1
- package/lib/dist/oc/wire-ops.js +1 -1
- package/lib/dist/oc/wrap-development.d.ts +105 -0
- package/lib/dist/oc/wrap-development.js +179 -0
- package/lib/dist/oc/wrap-ops.d.ts +100 -0
- package/lib/dist/oc/wrap-ops.js +406 -0
- package/lib/dist/rendering/render-solid.js +10 -2
- package/lib/dist/scene-manager.d.ts +2 -0
- package/lib/dist/scene-manager.js +29 -0
- package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
- package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
- package/lib/dist/tests/features/helix.test.d.ts +1 -0
- package/lib/dist/tests/features/helix.test.js +295 -0
- package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
- package/lib/dist/tests/features/rib.test.js +6 -1
- package/lib/dist/tests/features/sweep.test.js +125 -1
- package/lib/dist/tests/features/text.test.d.ts +1 -0
- package/lib/dist/tests/features/text.test.js +347 -0
- package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-development.test.js +130 -0
- package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
- package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
- package/lib/dist/tests/features/wrap.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap.test.js +331 -0
- package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
- package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
- package/lib/dist/tests/measure.test.d.ts +1 -0
- package/lib/dist/tests/measure.test.js +288 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/helix.md +64 -0
- package/llm-docs/api/index.json +11 -2
- package/llm-docs/api/text.md +52 -0
- package/llm-docs/api/types/helix.md +105 -0
- package/llm-docs/api/types/text.md +138 -0
- package/llm-docs/api/types/wrap.md +131 -0
- package/llm-docs/api/wrap.md +62 -0
- package/llm-docs/index.json +121 -1
- package/mcp/dist/server.js +20 -1
- package/mcp/dist/tools/inspection.d.ts +17 -0
- package/mcp/dist/tools/inspection.js +14 -0
- package/package.json +7 -3
- package/server/dist/fluidcad-server.d.ts +29 -0
- package/server/dist/fluidcad-server.js +40 -0
- package/server/dist/index.js +4 -2
- package/server/dist/model-package/pack.js +7 -6
- package/server/dist/model-package/types.d.ts +4 -3
- package/server/dist/preferences.d.ts +4 -0
- package/server/dist/preferences.js +2 -0
- package/server/dist/routes/measure.d.ts +3 -0
- package/server/dist/routes/measure.js +32 -0
- package/server/dist/routes/preferences.js +6 -0
- package/server/dist/routes/sketch-edits.js +2 -1
- package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
- package/ui/dist/assets/{index-MRqwG9Vh.js → index-no7mtr5s.js} +149 -102
- package/ui/dist/index.html +2 -2
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { getOC } from "./init.js";
|
|
2
|
+
import { Convert } from "./convert.js";
|
|
3
|
+
import { Explorer } from "./explorer.js";
|
|
4
|
+
import { FaceQuery } from "./face-query.js";
|
|
5
|
+
import { WireOps } from "./wire-ops.js";
|
|
6
|
+
import { ShapeFactory } from "../common/shape-factory.js";
|
|
7
|
+
import { ConeDevelopment, CylinderDevelopment, } from "./wrap-development.js";
|
|
8
|
+
import { interpolateBSpline2d } from "../math/bspline-interpolation.js";
|
|
9
|
+
/** Sample points per curved sketch edge before fitting its UV pcurve. */
|
|
10
|
+
const CURVED_EDGE_SAMPLES = 48;
|
|
11
|
+
/** Margin (radians) kept free so a wrapped region never closes on itself. */
|
|
12
|
+
const FULL_TURN_MARGIN = 1e-3;
|
|
13
|
+
/** Walls are ruled along the surface normal, so their normals are (near) perpendicular to it. */
|
|
14
|
+
const ALIGNED_NORMAL_THRESHOLD = 0.7;
|
|
15
|
+
/** Tracks the u-range covered by a wrapped region to reject over-full wraps. */
|
|
16
|
+
class UvSpan {
|
|
17
|
+
min = Infinity;
|
|
18
|
+
max = -Infinity;
|
|
19
|
+
add(u) {
|
|
20
|
+
if (u < this.min) {
|
|
21
|
+
this.min = u;
|
|
22
|
+
}
|
|
23
|
+
if (u > this.max) {
|
|
24
|
+
this.max = u;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
assertWithinOneTurn() {
|
|
28
|
+
if (this.max - this.min > 2 * Math.PI - FULL_TURN_MARGIN) {
|
|
29
|
+
throw new Error("wrap(): the sketch is too wide for the target surface — it would wrap around more than a full turn");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class WrapOps {
|
|
34
|
+
/**
|
|
35
|
+
* Wraps planar sketch region faces onto the surface of `targetFace` and
|
|
36
|
+
* thickens them by `thickness` measured along the surface normal. Positive
|
|
37
|
+
* thickness grows out of the material (emboss), negative grows into it
|
|
38
|
+
* (deboss tool). Returns the thickened solids with their faces classified.
|
|
39
|
+
*
|
|
40
|
+
* The pad base lies EXACTLY on the target surface. Downstream booleans
|
|
41
|
+
* resolve that coincident-face contact via their fuzzy tolerance and
|
|
42
|
+
* same-domain handling; do not be tempted to sink the base slightly past
|
|
43
|
+
* the surface to "help" them — the resulting wall∕target section curves
|
|
44
|
+
* are approximated by OCCT and oscillate visibly near the wall joints.
|
|
45
|
+
*/
|
|
46
|
+
static wrap(regionFaces, sketchPlane, targetFace, thickness) {
|
|
47
|
+
const development = WrapOps.createDevelopment(targetFace, sketchPlane);
|
|
48
|
+
const surface = WrapOps.makeSurface(development);
|
|
49
|
+
// Positive thickness must grow out of the material. The rebuilt
|
|
50
|
+
// development surface always thickens toward the development's outward
|
|
51
|
+
// normal (away from the axis), so flip the offset when the target face's
|
|
52
|
+
// material normal points toward the axis (e.g. a bore wall).
|
|
53
|
+
const signedOffset = WrapOps.isInwardFacing(targetFace, development)
|
|
54
|
+
? -thickness
|
|
55
|
+
: thickness;
|
|
56
|
+
const result = {
|
|
57
|
+
solids: [],
|
|
58
|
+
startFaces: [],
|
|
59
|
+
endFaces: [],
|
|
60
|
+
sideFaces: [],
|
|
61
|
+
internalFaces: [],
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
for (const region of regionFaces) {
|
|
65
|
+
const wrappedFace = WrapOps.wrapRegion(region, sketchPlane, development, surface);
|
|
66
|
+
const thickened = WrapOps.thicken(wrappedFace, signedOffset);
|
|
67
|
+
WrapOps.classify(thickened, wrappedFace, development, result);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
surface.delete();
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/** Builds the development mapping for the target face's underlying surface. */
|
|
76
|
+
static createDevelopment(targetFace, sketchPlane) {
|
|
77
|
+
const surfaceType = FaceQuery.getSurfaceTypeRaw(targetFace.getShape());
|
|
78
|
+
if (surfaceType === 'cylinder') {
|
|
79
|
+
const cylinder = FaceQuery.getSurfaceAdaptorCylinderRaw(targetFace.getShape());
|
|
80
|
+
const axis = cylinder.Axis();
|
|
81
|
+
const spec = {
|
|
82
|
+
origin: Convert.toPoint(axis.Location(), true),
|
|
83
|
+
axisDir: Convert.toVector3dFromGpDir(axis.Direction(), true),
|
|
84
|
+
radius: cylinder.Radius(),
|
|
85
|
+
};
|
|
86
|
+
axis.delete();
|
|
87
|
+
cylinder.delete();
|
|
88
|
+
return new CylinderDevelopment(spec, sketchPlane);
|
|
89
|
+
}
|
|
90
|
+
if (surfaceType === 'cone') {
|
|
91
|
+
const cone = FaceQuery.getSurfaceAdaptorConeRaw(targetFace.getShape());
|
|
92
|
+
const axis = cone.Axis();
|
|
93
|
+
const spec = {
|
|
94
|
+
origin: Convert.toPoint(axis.Location(), true),
|
|
95
|
+
axisDir: Convert.toVector3dFromGpDir(axis.Direction(), true),
|
|
96
|
+
refRadius: cone.RefRadius(),
|
|
97
|
+
semiAngle: cone.SemiAngle(),
|
|
98
|
+
};
|
|
99
|
+
axis.delete();
|
|
100
|
+
cone.delete();
|
|
101
|
+
return new ConeDevelopment(spec, sketchPlane);
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`wrap() requires a cylindrical or conical target face, got a ${surfaceType} face`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether the target face's material-outward normal points toward the
|
|
107
|
+
* surface axis (a bore wall) rather than away from it (an outer wall).
|
|
108
|
+
* The TopAbs orientation flag alone cannot tell: prism-swept lateral
|
|
109
|
+
* faces carry a reverse-parameterized surface (intrinsic normal inward,
|
|
110
|
+
* flag REVERSED) yet still face away from the axis.
|
|
111
|
+
*/
|
|
112
|
+
static isInwardFacing(targetFace, development) {
|
|
113
|
+
const probe = WrapOps.probeFace(targetFace);
|
|
114
|
+
return probe.normal.dot(development.surfaceNormalAt(probe.point)) < 0;
|
|
115
|
+
}
|
|
116
|
+
/** Builds the recentered Geom surface (sketch anchor at u = 0). */
|
|
117
|
+
static makeSurface(development) {
|
|
118
|
+
const oc = getOC();
|
|
119
|
+
const [origin, disposeOrigin] = Convert.toGpPnt(development.origin);
|
|
120
|
+
const [zDir, disposeZ] = Convert.toGpDir(development.axisDir);
|
|
121
|
+
const [xDir, disposeX] = Convert.toGpDir(development.xDir);
|
|
122
|
+
const frame = new oc.gp_Ax3(origin, zDir, xDir);
|
|
123
|
+
const surface = development.kind === 'cylinder'
|
|
124
|
+
? new oc.Geom_CylindricalSurface(frame, development.radius)
|
|
125
|
+
: new oc.Geom_ConicalSurface(frame, development.semiAngle, development.refRadius);
|
|
126
|
+
frame.delete();
|
|
127
|
+
disposeX();
|
|
128
|
+
disposeZ();
|
|
129
|
+
disposeOrigin();
|
|
130
|
+
return surface;
|
|
131
|
+
}
|
|
132
|
+
/** Maps one planar region face onto the surface as a face with pcurve boundaries. */
|
|
133
|
+
static wrapRegion(region, sketchPlane, development, surface) {
|
|
134
|
+
const oc = getOC();
|
|
135
|
+
const regionFace = oc.TopoDS.Face(region.getShape());
|
|
136
|
+
const outerWire = oc.BRepTools.OuterWire(regionFace);
|
|
137
|
+
const span = new UvSpan();
|
|
138
|
+
let outerUv = null;
|
|
139
|
+
const holesUv = [];
|
|
140
|
+
for (const wire of region.getWires()) {
|
|
141
|
+
const isOuter = wire.getShape().IsSame(outerWire);
|
|
142
|
+
const uvWire = WrapOps.mapWire(wire, sketchPlane, development, surface, span);
|
|
143
|
+
const oriented = WrapOps.orientUvWire(uvWire, wire, sketchPlane, development, isOuter);
|
|
144
|
+
if (isOuter) {
|
|
145
|
+
outerUv = oriented;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
holesUv.push(oriented);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!outerUv) {
|
|
152
|
+
throw new Error("wrap(): could not identify the outer boundary of a sketch region");
|
|
153
|
+
}
|
|
154
|
+
span.assertWithinOneTurn();
|
|
155
|
+
const maker = new oc.BRepBuilderAPI_MakeFace(surface, outerUv, true);
|
|
156
|
+
for (const hole of holesUv) {
|
|
157
|
+
maker.Add(hole);
|
|
158
|
+
}
|
|
159
|
+
if (!maker.IsDone()) {
|
|
160
|
+
maker.delete();
|
|
161
|
+
throw new Error("wrap(): failed to build the wrapped face on the target surface");
|
|
162
|
+
}
|
|
163
|
+
const rawFace = maker.Face();
|
|
164
|
+
maker.delete();
|
|
165
|
+
oc.BRepLib.BuildCurves3d(rawFace);
|
|
166
|
+
// Belt and suspenders for remaining surface-specific issues (the wire
|
|
167
|
+
// windings themselves are already enforced by orientUvWire — ShapeFix
|
|
168
|
+
// does NOT reliably fix multi-wire faces on periodic surfaces).
|
|
169
|
+
const fix = new oc.ShapeFix_Face(rawFace);
|
|
170
|
+
fix.Perform();
|
|
171
|
+
const fixedFace = fix.Face();
|
|
172
|
+
fix.delete();
|
|
173
|
+
return fixedFace;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Enforces the winding the face builder needs: the outer boundary
|
|
177
|
+
* counter-clockwise in UV (material to its left on the surface), holes
|
|
178
|
+
* clockwise. The mapped wire inherits the planar wire's winding through the
|
|
179
|
+
* development (possibly mirrored), so measure the source and reverse the UV
|
|
180
|
+
* wire when it lands the wrong way.
|
|
181
|
+
*/
|
|
182
|
+
static orientUvWire(uvWire, sourceWire, sketchPlane, development, isOuter) {
|
|
183
|
+
const planarCW = WireOps.isCWRaw(sourceWire.getShape(), sketchPlane.normal);
|
|
184
|
+
const uvCW = development.isOrientationPreserving() ? planarCW : !planarCW;
|
|
185
|
+
const wantCW = !isOuter;
|
|
186
|
+
if (uvCW === wantCW) {
|
|
187
|
+
return uvWire;
|
|
188
|
+
}
|
|
189
|
+
return WireOps.reverseWireRaw(uvWire);
|
|
190
|
+
}
|
|
191
|
+
/** Maps a planar wire into a wire of edges with pcurves on the surface. */
|
|
192
|
+
static mapWire(wire, sketchPlane, development, surface, span) {
|
|
193
|
+
const uvEdges = wire.getEdges().flatMap(edge => WrapOps.mapEdge(edge, sketchPlane, development, surface, span));
|
|
194
|
+
return WireOps.makeWireFromEdgesRaw(uvEdges);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Maps one sketch edge onto the surface, following the source wire's
|
|
198
|
+
* traversal direction (reversed edges are sampled back to front) so the
|
|
199
|
+
* assembled UV wire winds the same way as the planar wire it came from.
|
|
200
|
+
* Straight edges on cylinders map to exact UV lines (the development is
|
|
201
|
+
* affine there); everything else is sampled along the curve and fitted with
|
|
202
|
+
* a 2D B-spline. Closed edges (e.g. full circles) are split into two halves
|
|
203
|
+
* so the resulting wire has topologically distinct vertices.
|
|
204
|
+
*/
|
|
205
|
+
static mapEdge(edge, sketchPlane, development, surface, span) {
|
|
206
|
+
const oc = getOC();
|
|
207
|
+
const reversed = edge.getShape().Orientation() === oc.TopAbs_Orientation.TopAbs_REVERSED;
|
|
208
|
+
const adaptor = new oc.BRepAdaptor_Curve(oc.TopoDS.Edge(edge.getShape()));
|
|
209
|
+
try {
|
|
210
|
+
const first = adaptor.FirstParameter();
|
|
211
|
+
const last = adaptor.LastParameter();
|
|
212
|
+
const isLine = adaptor.GetType() === oc.GeomAbs_CurveType.GeomAbs_Line;
|
|
213
|
+
if (isLine && development.kind === 'cylinder') {
|
|
214
|
+
const start = WrapOps.mapPoint(adaptor, reversed ? last : first, sketchPlane, development, span);
|
|
215
|
+
const end = WrapOps.mapPoint(adaptor, reversed ? first : last, sketchPlane, development, span);
|
|
216
|
+
return [WrapOps.makeUvLineEdge(start, end, surface)];
|
|
217
|
+
}
|
|
218
|
+
const samples = [];
|
|
219
|
+
for (let i = 0; i <= CURVED_EDGE_SAMPLES; i++) {
|
|
220
|
+
const t = first + ((last - first) * i) / CURVED_EDGE_SAMPLES;
|
|
221
|
+
samples.push(WrapOps.mapPoint(adaptor, t, sketchPlane, development, span));
|
|
222
|
+
}
|
|
223
|
+
if (reversed) {
|
|
224
|
+
samples.reverse();
|
|
225
|
+
}
|
|
226
|
+
const start = samples[0];
|
|
227
|
+
const end = samples[samples.length - 1];
|
|
228
|
+
const isClosed = Math.hypot(end.u - start.u, end.v - start.v) < 1e-9;
|
|
229
|
+
if (isClosed) {
|
|
230
|
+
const mid = Math.floor(samples.length / 2);
|
|
231
|
+
return [
|
|
232
|
+
WrapOps.makeFittedUvEdge(samples.slice(0, mid + 1), surface),
|
|
233
|
+
WrapOps.makeFittedUvEdge(samples.slice(mid), surface),
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
return [WrapOps.makeFittedUvEdge(samples, surface)];
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
adaptor.delete();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
static mapPoint(adaptor, t, sketchPlane, development, span) {
|
|
243
|
+
const point = Convert.toPoint(adaptor.Value(t), true);
|
|
244
|
+
const uv = development.toUV(sketchPlane.worldToLocal(point));
|
|
245
|
+
span.add(uv.u);
|
|
246
|
+
return uv;
|
|
247
|
+
}
|
|
248
|
+
static makeFittedUvEdge(samples, surface) {
|
|
249
|
+
const oc = getOC();
|
|
250
|
+
const curve = WrapOps.fitUvCurve(samples);
|
|
251
|
+
const maker = new oc.BRepBuilderAPI_MakeEdge(curve, surface);
|
|
252
|
+
const result = maker.Edge();
|
|
253
|
+
maker.delete();
|
|
254
|
+
curve.delete();
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
static makeUvLineEdge(start, end, surface) {
|
|
258
|
+
const oc = getOC();
|
|
259
|
+
const du = end.u - start.u;
|
|
260
|
+
const dv = end.v - start.v;
|
|
261
|
+
const length = Math.hypot(du, dv);
|
|
262
|
+
const point = new oc.gp_Pnt2d(start.u, start.v);
|
|
263
|
+
const direction = new oc.gp_Dir2d(du, dv);
|
|
264
|
+
const line = new oc.Geom2d_Line(point, direction);
|
|
265
|
+
const maker = new oc.BRepBuilderAPI_MakeEdge(line, surface, 0, length);
|
|
266
|
+
const result = maker.Edge();
|
|
267
|
+
maker.delete();
|
|
268
|
+
line.delete();
|
|
269
|
+
direction.delete();
|
|
270
|
+
point.delete();
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Interpolates the developed UV samples of one sketch edge with a 2D
|
|
275
|
+
* B-spline that passes exactly through every sample (so adjacent wire
|
|
276
|
+
* edges meet bit-exactly — no endpoint snapping needed).
|
|
277
|
+
*
|
|
278
|
+
* The poles/knots are computed in-house: fluidcad-ocjs currently
|
|
279
|
+
* miscompiles `Geom2dAPI_PointsToBSpline` — both its array constructors
|
|
280
|
+
* (NaN poles) and its `Init` path (the "fit" of a clean semicircle bulged
|
|
281
|
+
* 70% past the samples). `Geom2d_BSplineCurve`'s array constructor is
|
|
282
|
+
* unaffected.
|
|
283
|
+
*/
|
|
284
|
+
static fitUvCurve(samples) {
|
|
285
|
+
const oc = getOC();
|
|
286
|
+
const data = interpolateBSpline2d(samples.map(s => ({ x: s.u, y: s.v })));
|
|
287
|
+
const poles = new oc.NCollection_Array1_gp_Pnt2d(1, data.poles.length);
|
|
288
|
+
for (let i = 0; i < data.poles.length; i++) {
|
|
289
|
+
const point = new oc.gp_Pnt2d(data.poles[i].x, data.poles[i].y);
|
|
290
|
+
poles.SetValue(i + 1, point);
|
|
291
|
+
point.delete();
|
|
292
|
+
}
|
|
293
|
+
const knots = new oc.NCollection_Array1_double(1, data.knots.length);
|
|
294
|
+
const multiplicities = new oc.NCollection_Array1_int(1, data.knots.length);
|
|
295
|
+
for (let i = 0; i < data.knots.length; i++) {
|
|
296
|
+
knots.SetValue(i + 1, data.knots[i]);
|
|
297
|
+
multiplicities.SetValue(i + 1, data.multiplicities[i]);
|
|
298
|
+
}
|
|
299
|
+
const curve = new oc.Geom2d_BSplineCurve(poles, knots, multiplicities, data.degree);
|
|
300
|
+
poles.delete();
|
|
301
|
+
knots.delete();
|
|
302
|
+
multiplicities.delete();
|
|
303
|
+
return curve;
|
|
304
|
+
}
|
|
305
|
+
/** Thickens the wrapped face along the surface normal into a solid. */
|
|
306
|
+
static thicken(wrappedFace, offset) {
|
|
307
|
+
const oc = getOC();
|
|
308
|
+
const maker = new oc.BRepOffsetAPI_MakeThickSolid();
|
|
309
|
+
maker.MakeThickSolidBySimple(wrappedFace, offset);
|
|
310
|
+
if (!maker.IsDone()) {
|
|
311
|
+
maker.delete();
|
|
312
|
+
throw new Error("wrap(): thickening the wrapped face failed");
|
|
313
|
+
}
|
|
314
|
+
const shape = maker.Shape();
|
|
315
|
+
maker.delete();
|
|
316
|
+
const SOLID = oc.TopAbs_ShapeEnum.TopAbs_SOLID;
|
|
317
|
+
const solids = Explorer.findShapes(shape, SOLID);
|
|
318
|
+
if (solids.length === 0) {
|
|
319
|
+
throw new Error("wrap(): thickening the wrapped face did not produce a solid");
|
|
320
|
+
}
|
|
321
|
+
return solids.map(solid => {
|
|
322
|
+
const typedSolid = oc.TopoDS.Solid(solid);
|
|
323
|
+
oc.BRepLib.OrientClosedSolid(typedSolid);
|
|
324
|
+
return ShapeFactory.fromShape(typedSolid);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Classifies the thickened solid's faces. The wrapped base face survives
|
|
329
|
+
* thickening unchanged (start); the only other face whose normal is aligned
|
|
330
|
+
* with the surface normal is the offset face (end); the remaining walls are
|
|
331
|
+
* split into internal (sharing an edge with a sketch hole) and side.
|
|
332
|
+
*/
|
|
333
|
+
static classify(solids, baseFace, development, out) {
|
|
334
|
+
const holeEdges = WrapOps.collectHoleEdges(baseFace);
|
|
335
|
+
for (const solid of solids) {
|
|
336
|
+
out.solids.push(solid);
|
|
337
|
+
for (const shape of Explorer.findFacesWrapped(solid)) {
|
|
338
|
+
const face = shape;
|
|
339
|
+
if (face.getShape().IsSame(baseFace)) {
|
|
340
|
+
out.startFaces.push(face);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const probe = WrapOps.probeFace(face);
|
|
344
|
+
const aligned = Math.abs(probe.normal.dot(development.surfaceNormalAt(probe.point))) > ALIGNED_NORMAL_THRESHOLD;
|
|
345
|
+
if (aligned) {
|
|
346
|
+
out.endFaces.push(face);
|
|
347
|
+
}
|
|
348
|
+
else if (WrapOps.sharesEdgeWith(face, holeEdges)) {
|
|
349
|
+
out.internalFaces.push(face);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
out.sideFaces.push(face);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
static collectHoleEdges(face) {
|
|
358
|
+
const oc = getOC();
|
|
359
|
+
const outerWire = oc.BRepTools.OuterWire(face);
|
|
360
|
+
const WIRE = oc.TopAbs_ShapeEnum.TopAbs_WIRE;
|
|
361
|
+
const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
|
|
362
|
+
const holeEdges = [];
|
|
363
|
+
for (const wire of Explorer.findShapes(face, WIRE)) {
|
|
364
|
+
if (wire.IsSame(outerWire)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
for (const edge of Explorer.findShapes(wire, EDGE)) {
|
|
368
|
+
holeEdges.push(oc.TopoDS.Edge(edge));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return holeEdges;
|
|
372
|
+
}
|
|
373
|
+
static sharesEdgeWith(face, edges) {
|
|
374
|
+
if (edges.length === 0) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
return face.getEdges().some(faceEdge => edges.some(edge => faceEdge.getShape().IsSame(edge)));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Samples the face at the middle of its UV bounds: the surface point and
|
|
381
|
+
* the material-outward normal (oriented by the face's TopAbs flag) at that
|
|
382
|
+
* SAME parameter. Classification compares this normal against the
|
|
383
|
+
* development normal at the sampled point — sampling both at one location
|
|
384
|
+
* keeps the comparison angle-independent on periodic surfaces, where the
|
|
385
|
+
* normal rotates with u (a normal taken anywhere else drifts by the angular
|
|
386
|
+
* distance and breaks the alignment test past ~45°).
|
|
387
|
+
*/
|
|
388
|
+
static probeFace(face) {
|
|
389
|
+
const oc = getOC();
|
|
390
|
+
const rawFace = oc.TopoDS.Face(face.getShape());
|
|
391
|
+
const bounds = oc.BRepTools.UVBounds(rawFace);
|
|
392
|
+
const u = (bounds.UMin + bounds.UMax) / 2;
|
|
393
|
+
const v = (bounds.VMin + bounds.VMax) / 2;
|
|
394
|
+
const surface = oc.BRep_Tool.Surface(rawFace);
|
|
395
|
+
const props = new oc.GeomLProp_SLProps(surface, u, v, 1, 1e-6);
|
|
396
|
+
let rawNormal = props.Normal();
|
|
397
|
+
if (rawFace.Orientation() === oc.TopAbs_Orientation.TopAbs_REVERSED) {
|
|
398
|
+
rawNormal = rawNormal.Reversed();
|
|
399
|
+
}
|
|
400
|
+
const normal = Convert.toVector3dFromGpDir(rawNormal);
|
|
401
|
+
const point = Convert.toPoint(props.Value(), true);
|
|
402
|
+
props.delete();
|
|
403
|
+
surface.delete();
|
|
404
|
+
return { point, normal };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -47,8 +47,16 @@ function getFacesMesh(shapeObj) {
|
|
|
47
47
|
for (let t = 0; t < triangleCount; t++) {
|
|
48
48
|
group.faceMapping.push(faceIdx);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Avoid spread on potentially huge arrays — JS argument count is
|
|
51
|
+
// capped (~65K) and would throw "Maximum call stack size exceeded".
|
|
52
|
+
const verts = faceResult.vertices;
|
|
53
|
+
for (let i = 0; i < verts.length; i++) {
|
|
54
|
+
group.vertices.push(verts[i]);
|
|
55
|
+
}
|
|
56
|
+
const norms = faceResult.normals;
|
|
57
|
+
for (let i = 0; i < norms.length; i++) {
|
|
58
|
+
group.normals.push(norms[i]);
|
|
59
|
+
}
|
|
52
60
|
for (const idx of faceResult.indices) {
|
|
53
61
|
group.indices.push(group.vertexOffset + idx);
|
|
54
62
|
}
|
|
@@ -7,6 +7,7 @@ import type { ShapeProperties } from "./oc/props.js";
|
|
|
7
7
|
import type { FaceProperties } from "./oc/face-props.js";
|
|
8
8
|
import type { EdgeProperties } from "./oc/edge-props.js";
|
|
9
9
|
import type { HitTestResult } from "./oc/hit-test.js";
|
|
10
|
+
import type { MeasureEntityRef, MeasureResult } from "./oc/measure/measure-types.js";
|
|
10
11
|
declare class SceneManager {
|
|
11
12
|
rootPath: string;
|
|
12
13
|
currentScene: Scene;
|
|
@@ -22,6 +23,7 @@ declare class SceneManager {
|
|
|
22
23
|
getShapeProperties(scene: Scene, shapeId: string): ShapeProperties | null;
|
|
23
24
|
getFaceProperties(scene: Scene, shapeId: string, faceIndex: number): FaceProperties | null;
|
|
24
25
|
getEdgeProperties(scene: Scene, shapeId: string, edgeIndex: number): EdgeProperties | null;
|
|
26
|
+
measure(scene: Scene, refs: MeasureEntityRef[]): MeasureResult | null;
|
|
25
27
|
exportShapes(scene: Scene, shapeIds: string[], options: ExportOptions): {
|
|
26
28
|
data: string | Uint8Array;
|
|
27
29
|
fileName: string;
|
|
@@ -9,6 +9,7 @@ import { FaceProps } from "./oc/face-props.js";
|
|
|
9
9
|
import { EdgeProps } from "./oc/edge-props.js";
|
|
10
10
|
import { Explorer } from "./oc/explorer.js";
|
|
11
11
|
import { OccHitTest } from "./oc/hit-test.js";
|
|
12
|
+
import { MeasureOps } from "./oc/measure/measure-ops.js";
|
|
12
13
|
class SceneManager {
|
|
13
14
|
rootPath;
|
|
14
15
|
currentScene = new Scene();
|
|
@@ -76,6 +77,24 @@ class SceneManager {
|
|
|
76
77
|
}
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
80
|
+
measure(scene, refs) {
|
|
81
|
+
const inputs = [];
|
|
82
|
+
for (const ref of refs) {
|
|
83
|
+
const shape = findShapeById(scene, ref.shapeId);
|
|
84
|
+
if (!shape) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const subShapes = ref.kind === 'face' ? Explorer.findFacesWrapped(shape) : Explorer.findEdgesWrapped(shape);
|
|
88
|
+
if (ref.index < 0 || ref.index >= subShapes.length) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
inputs.push({ ref, shape: subShapes[ref.index].getShape() });
|
|
92
|
+
}
|
|
93
|
+
if (inputs.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return MeasureOps.measure(inputs);
|
|
97
|
+
}
|
|
79
98
|
exportShapes(scene, shapeIds, options) {
|
|
80
99
|
const solids = [];
|
|
81
100
|
for (const obj of scene.getAllSceneObjects()) {
|
|
@@ -101,6 +120,16 @@ class SceneManager {
|
|
|
101
120
|
return null;
|
|
102
121
|
}
|
|
103
122
|
}
|
|
123
|
+
function findShapeById(scene, shapeId) {
|
|
124
|
+
for (const obj of scene.getAllSceneObjects()) {
|
|
125
|
+
for (const shape of obj.getAddedShapes()) {
|
|
126
|
+
if (shape.id === shapeId) {
|
|
127
|
+
return shape;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
104
133
|
let currentManager = null;
|
|
105
134
|
function resolveMeshConfig(options) {
|
|
106
135
|
return {
|
|
@@ -50,8 +50,8 @@ describe("cylinderCurve filter on fillet faces", () => {
|
|
|
50
50
|
const curveTypeNames = {};
|
|
51
51
|
for (const k of Object.keys(oc.GeomAbs_CurveType)) {
|
|
52
52
|
const v = oc.GeomAbs_CurveType[k];
|
|
53
|
-
if (v && typeof v
|
|
54
|
-
curveTypeNames[v
|
|
53
|
+
if (v && typeof v === "number") {
|
|
54
|
+
curveTypeNames[v] = k;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
const edgeInfo = edges.map(e => {
|
|
@@ -59,7 +59,7 @@ describe("cylinderCurve filter on fillet faces", () => {
|
|
|
59
59
|
const t = adaptor.GetType();
|
|
60
60
|
const closed = adaptor.IsClosed();
|
|
61
61
|
adaptor.delete();
|
|
62
|
-
return { type: curveTypeNames[t
|
|
62
|
+
return { type: curveTypeNames[(t)] ?? t, closed };
|
|
63
63
|
});
|
|
64
64
|
console.log("Cylinder face missed by cylinderCurve:", JSON.stringify(edgeInfo));
|
|
65
65
|
}
|
|
@@ -4,7 +4,10 @@ import sketch from "../../core/sketch.js";
|
|
|
4
4
|
import extrude from "../../core/extrude.js";
|
|
5
5
|
import select from "../../core/select.js";
|
|
6
6
|
import rotate from "../../core/rotate.js";
|
|
7
|
-
import { circle, move, rect } from "../../core/2d/index.js";
|
|
7
|
+
import { circle, move, rect, slot } from "../../core/2d/index.js";
|
|
8
|
+
import plane from "../../core/plane.js";
|
|
9
|
+
import { ShapeProps } from "../../oc/props.js";
|
|
10
|
+
import { getSceneManager } from "../../scene-manager.js";
|
|
8
11
|
import cylinder from "../../core/cylinder.js";
|
|
9
12
|
import { ShapeOps } from "../../oc/shape-ops.js";
|
|
10
13
|
import { face } from "../../filters/index.js";
|
|
@@ -206,6 +209,40 @@ describe("extrude to face", () => {
|
|
|
206
209
|
expect(shapes[0].getType()).toBe("solid");
|
|
207
210
|
});
|
|
208
211
|
});
|
|
212
|
+
describe("conical face", () => {
|
|
213
|
+
function buildDraftedScenario(endOffset) {
|
|
214
|
+
getSceneManager().startScene();
|
|
215
|
+
sketch("top", () => {
|
|
216
|
+
circle(50);
|
|
217
|
+
});
|
|
218
|
+
const base = extrude(50).draft(-8);
|
|
219
|
+
sketch(plane("front", 50), () => {
|
|
220
|
+
slot([0, 10], [0, 30], 5);
|
|
221
|
+
});
|
|
222
|
+
let e = extrude(base.sideFaces());
|
|
223
|
+
if (endOffset !== undefined) {
|
|
224
|
+
e = e.endOffset(endOffset);
|
|
225
|
+
}
|
|
226
|
+
render();
|
|
227
|
+
let volume = 0;
|
|
228
|
+
for (const s of e.getShapes()) {
|
|
229
|
+
volume += ShapeProps.getProperties(s.getShape()).volumeMm3;
|
|
230
|
+
}
|
|
231
|
+
return volume;
|
|
232
|
+
}
|
|
233
|
+
it("should extrude up to a drafted (conical) side face", () => {
|
|
234
|
+
const volume = buildDraftedScenario();
|
|
235
|
+
expect(volume).toBeGreaterThan(0);
|
|
236
|
+
});
|
|
237
|
+
it("should respect endOffset against a conical target face", () => {
|
|
238
|
+
const without = buildDraftedScenario();
|
|
239
|
+
const withOffset = buildDraftedScenario(2);
|
|
240
|
+
// endOffset(2) stops the extrusion short of the cone, so the result
|
|
241
|
+
// stays a separate, smaller solid instead of fusing with the base.
|
|
242
|
+
expect(withOffset).not.toBeCloseTo(without, 1);
|
|
243
|
+
expect(withOffset).toBeLessThan(without);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
209
246
|
describe("inclined cylindrical face", () => {
|
|
210
247
|
it("should extrude up to a rotated cylindrical face", () => {
|
|
211
248
|
const cyl = cylinder(50, 80);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|