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.
Files changed (186) hide show
  1. package/LICENSE.txt +21 -504
  2. package/README.md +1 -1
  3. package/bin/commands/login.js +33 -5
  4. package/bin/commands/mcp.js +3 -2
  5. package/bin/commands/publish.js +103 -8
  6. package/bin/lib/api-client.js +8 -0
  7. package/bin/lib/model-config.js +27 -4
  8. package/bin/lib/prompt.js +97 -0
  9. package/lib/dist/common/edge.d.ts +1 -1
  10. package/lib/dist/common/face.d.ts +1 -1
  11. package/lib/dist/common/scene-object.d.ts +6 -0
  12. package/lib/dist/common/scene-object.js +8 -0
  13. package/lib/dist/common/shape-factory.d.ts +1 -1
  14. package/lib/dist/common/shape-history-tracker.d.ts +1 -1
  15. package/lib/dist/common/shape.d.ts +1 -1
  16. package/lib/dist/common/solid.d.ts +1 -1
  17. package/lib/dist/common/transformable-primitive.d.ts +12 -1
  18. package/lib/dist/common/transformable-primitive.js +27 -0
  19. package/lib/dist/common/vertex.d.ts +1 -1
  20. package/lib/dist/common/wire.d.ts +1 -1
  21. package/lib/dist/core/2d/index.d.ts +1 -0
  22. package/lib/dist/core/2d/index.js +1 -0
  23. package/lib/dist/core/2d/text.d.ts +30 -0
  24. package/lib/dist/core/2d/text.js +37 -0
  25. package/lib/dist/core/helix.d.ts +20 -0
  26. package/lib/dist/core/helix.js +36 -0
  27. package/lib/dist/core/index.d.ts +3 -1
  28. package/lib/dist/core/index.js +2 -0
  29. package/lib/dist/core/interfaces.d.ts +180 -0
  30. package/lib/dist/core/wrap.d.ts +17 -0
  31. package/lib/dist/core/wrap.js +39 -0
  32. package/lib/dist/features/2d/text.d.ts +67 -0
  33. package/lib/dist/features/2d/text.js +320 -0
  34. package/lib/dist/features/cylinder.d.ts +3 -1
  35. package/lib/dist/features/cylinder.js +5 -2
  36. package/lib/dist/features/extrude-base.d.ts +1 -0
  37. package/lib/dist/features/extrude-to-face.d.ts +1 -0
  38. package/lib/dist/features/extrude-to-face.js +6 -0
  39. package/lib/dist/features/fillet.d.ts +1 -1
  40. package/lib/dist/features/helix.d.ts +41 -0
  41. package/lib/dist/features/helix.js +337 -0
  42. package/lib/dist/features/select.js +32 -8
  43. package/lib/dist/features/simple-extruder.d.ts +1 -1
  44. package/lib/dist/features/simple-extruder.js +7 -2
  45. package/lib/dist/features/sphere.d.ts +3 -1
  46. package/lib/dist/features/sphere.js +5 -2
  47. package/lib/dist/features/sweep.js +7 -2
  48. package/lib/dist/features/wrap.d.ts +39 -0
  49. package/lib/dist/features/wrap.js +116 -0
  50. package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
  51. package/lib/dist/filters/edge/belongs-to-face.js +14 -10
  52. package/lib/dist/filters/filter.d.ts +1 -1
  53. package/lib/dist/filters/from-object.d.ts +1 -1
  54. package/lib/dist/filters/tangent-expander.d.ts +1 -1
  55. package/lib/dist/filters/tangent-expander.js +57 -40
  56. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  57. package/lib/dist/helpers/scene-helpers.js +1 -1
  58. package/lib/dist/index.d.ts +2 -0
  59. package/lib/dist/index.js +3 -1
  60. package/lib/dist/io/file-import.d.ts +7 -0
  61. package/lib/dist/io/file-import.js +28 -1
  62. package/lib/dist/io/font-registry.d.ts +45 -0
  63. package/lib/dist/io/font-registry.js +272 -0
  64. package/lib/dist/math/bspline-interpolation.d.ts +29 -0
  65. package/lib/dist/math/bspline-interpolation.js +194 -0
  66. package/lib/dist/oc/boolean-ops.d.ts +3 -1
  67. package/lib/dist/oc/boolean-ops.js +15 -1
  68. package/lib/dist/oc/color-transfer.d.ts +1 -1
  69. package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
  70. package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
  71. package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
  72. package/lib/dist/oc/convert.d.ts +1 -1
  73. package/lib/dist/oc/draft-ops.d.ts +1 -1
  74. package/lib/dist/oc/edge-ops.d.ts +2 -2
  75. package/lib/dist/oc/edge-ops.js +13 -14
  76. package/lib/dist/oc/edge-props.d.ts +1 -1
  77. package/lib/dist/oc/edge-query.d.ts +1 -1
  78. package/lib/dist/oc/edge-query.js +3 -8
  79. package/lib/dist/oc/errors.d.ts +8 -0
  80. package/lib/dist/oc/errors.js +27 -0
  81. package/lib/dist/oc/explorer.d.ts +2 -2
  82. package/lib/dist/oc/extrude-ops.d.ts +28 -2
  83. package/lib/dist/oc/extrude-ops.js +56 -7
  84. package/lib/dist/oc/face-ops.d.ts +2 -1
  85. package/lib/dist/oc/face-ops.js +11 -0
  86. package/lib/dist/oc/face-props.d.ts +1 -1
  87. package/lib/dist/oc/face-query.d.ts +12 -1
  88. package/lib/dist/oc/face-query.js +39 -0
  89. package/lib/dist/oc/fillet-ops.d.ts +1 -1
  90. package/lib/dist/oc/fillet-ops.js +4 -4
  91. package/lib/dist/oc/geometry.d.ts +1 -1
  92. package/lib/dist/oc/geometry.js +12 -14
  93. package/lib/dist/oc/helix-ops.d.ts +37 -0
  94. package/lib/dist/oc/helix-ops.js +88 -0
  95. package/lib/dist/oc/hit-test.d.ts +1 -1
  96. package/lib/dist/oc/index.d.ts +4 -0
  97. package/lib/dist/oc/index.js +2 -0
  98. package/lib/dist/oc/init.d.ts +1 -1
  99. package/lib/dist/oc/init.js +1 -1
  100. package/lib/dist/oc/intersection.js +1 -1
  101. package/lib/dist/oc/io.d.ts +6 -6
  102. package/lib/dist/oc/io.js +31 -24
  103. package/lib/dist/oc/measure/classify.d.ts +34 -0
  104. package/lib/dist/oc/measure/classify.js +246 -0
  105. package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
  106. package/lib/dist/oc/measure/measure-ops.js +210 -0
  107. package/lib/dist/oc/measure/measure-types.d.ts +39 -0
  108. package/lib/dist/oc/measure/measure-types.js +1 -0
  109. package/lib/dist/oc/measure/sampling.d.ts +9 -0
  110. package/lib/dist/oc/measure/sampling.js +77 -0
  111. package/lib/dist/oc/measure/vec.d.ts +13 -0
  112. package/lib/dist/oc/measure/vec.js +23 -0
  113. package/lib/dist/oc/mesh.d.ts +1 -1
  114. package/lib/dist/oc/mesh.js +40 -28
  115. package/lib/dist/oc/path-sampler.d.ts +29 -0
  116. package/lib/dist/oc/path-sampler.js +63 -0
  117. package/lib/dist/oc/props.d.ts +1 -1
  118. package/lib/dist/oc/props.js +4 -1
  119. package/lib/dist/oc/shape-hash.d.ts +26 -0
  120. package/lib/dist/oc/shape-hash.js +32 -0
  121. package/lib/dist/oc/shape-ops.d.ts +5 -3
  122. package/lib/dist/oc/shape-ops.js +6 -5
  123. package/lib/dist/oc/sweep-ops.d.ts +22 -1
  124. package/lib/dist/oc/sweep-ops.js +206 -18
  125. package/lib/dist/oc/text-outline.d.ts +62 -0
  126. package/lib/dist/oc/text-outline.js +212 -0
  127. package/lib/dist/oc/topology-index.d.ts +1 -1
  128. package/lib/dist/oc/vertex-ops.d.ts +1 -1
  129. package/lib/dist/oc/wire-ops.d.ts +1 -1
  130. package/lib/dist/oc/wire-ops.js +1 -1
  131. package/lib/dist/oc/wrap-development.d.ts +105 -0
  132. package/lib/dist/oc/wrap-development.js +179 -0
  133. package/lib/dist/oc/wrap-ops.d.ts +100 -0
  134. package/lib/dist/oc/wrap-ops.js +406 -0
  135. package/lib/dist/rendering/render-solid.js +10 -2
  136. package/lib/dist/scene-manager.d.ts +2 -0
  137. package/lib/dist/scene-manager.js +29 -0
  138. package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
  139. package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
  140. package/lib/dist/tests/features/helix.test.d.ts +1 -0
  141. package/lib/dist/tests/features/helix.test.js +295 -0
  142. package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
  143. package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
  144. package/lib/dist/tests/features/rib.test.js +6 -1
  145. package/lib/dist/tests/features/sweep.test.js +125 -1
  146. package/lib/dist/tests/features/text.test.d.ts +1 -0
  147. package/lib/dist/tests/features/text.test.js +347 -0
  148. package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
  149. package/lib/dist/tests/features/wrap-development.test.js +130 -0
  150. package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
  151. package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
  152. package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
  153. package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
  154. package/lib/dist/tests/features/wrap.test.d.ts +1 -0
  155. package/lib/dist/tests/features/wrap.test.js +331 -0
  156. package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
  157. package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
  158. package/lib/dist/tests/measure.test.d.ts +1 -0
  159. package/lib/dist/tests/measure.test.js +288 -0
  160. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  161. package/llm-docs/api/helix.md +64 -0
  162. package/llm-docs/api/index.json +11 -2
  163. package/llm-docs/api/text.md +52 -0
  164. package/llm-docs/api/types/helix.md +105 -0
  165. package/llm-docs/api/types/text.md +138 -0
  166. package/llm-docs/api/types/wrap.md +131 -0
  167. package/llm-docs/api/wrap.md +62 -0
  168. package/llm-docs/index.json +121 -1
  169. package/mcp/dist/server.js +20 -1
  170. package/mcp/dist/tools/inspection.d.ts +17 -0
  171. package/mcp/dist/tools/inspection.js +14 -0
  172. package/package.json +7 -3
  173. package/server/dist/fluidcad-server.d.ts +29 -0
  174. package/server/dist/fluidcad-server.js +40 -0
  175. package/server/dist/index.js +4 -2
  176. package/server/dist/model-package/pack.js +7 -6
  177. package/server/dist/model-package/types.d.ts +4 -3
  178. package/server/dist/preferences.d.ts +4 -0
  179. package/server/dist/preferences.js +2 -0
  180. package/server/dist/routes/measure.d.ts +3 -0
  181. package/server/dist/routes/measure.js +32 -0
  182. package/server/dist/routes/preferences.js +6 -0
  183. package/server/dist/routes/sketch-edits.js +2 -1
  184. package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
  185. package/ui/dist/assets/{index-MRqwG9Vh.js → index-no7mtr5s.js} +149 -102
  186. 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
- group.vertices.push(...faceResult.vertices);
51
- group.normals.push(...faceResult.normals);
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.value === "number") {
54
- curveTypeNames[v.value] = k;
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.value] ?? t.value, closed };
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 {};