fluidcad 0.0.32 → 0.0.34

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 (279) hide show
  1. package/README.md +3 -2
  2. package/bin/commands/init.js +55 -0
  3. package/bin/commands/mcp.js +33 -0
  4. package/bin/commands/serve.js +77 -0
  5. package/bin/fluidcad.js +15 -107
  6. package/lib/dist/common/scene-object.d.ts +4 -1
  7. package/lib/dist/common/scene-object.js +9 -2
  8. package/lib/dist/common/solid.d.ts +4 -1
  9. package/lib/dist/common/solid.js +13 -0
  10. package/lib/dist/core/2d/tarc.d.ts +20 -2
  11. package/lib/dist/core/2d/tarc.js +24 -0
  12. package/lib/dist/core/index.d.ts +2 -1
  13. package/lib/dist/core/index.js +1 -0
  14. package/lib/dist/core/interfaces.d.ts +107 -2
  15. package/lib/dist/core/load.d.ts +2 -2
  16. package/lib/dist/core/repeat.js +62 -46
  17. package/lib/dist/core/rib.d.ts +18 -0
  18. package/lib/dist/core/rib.js +37 -0
  19. package/lib/dist/features/2d/arc.d.ts +8 -2
  20. package/lib/dist/features/2d/arc.js +94 -17
  21. package/lib/dist/features/2d/back.js +3 -2
  22. package/lib/dist/features/2d/sketch.d.ts +4 -0
  23. package/lib/dist/features/2d/sketch.js +21 -0
  24. package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
  25. package/lib/dist/features/2d/tarc-constrained.js +8 -0
  26. package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
  27. package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
  28. package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
  29. package/lib/dist/features/2d/tarc-to-object.js +66 -0
  30. package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
  31. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  32. package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
  33. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  34. package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
  35. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  36. package/lib/dist/features/2d/tarc.d.ts +2 -0
  37. package/lib/dist/features/2d/tarc.js +3 -0
  38. package/lib/dist/features/extrude-base.d.ts +9 -0
  39. package/lib/dist/features/extrude-base.js +22 -0
  40. package/lib/dist/features/extrude-to-face.js +1 -5
  41. package/lib/dist/features/extrude-two-distances.js +1 -2
  42. package/lib/dist/features/extrude.js +1 -2
  43. package/lib/dist/features/load.d.ts +6 -0
  44. package/lib/dist/features/load.js +53 -1
  45. package/lib/dist/features/mirror-feature.d.ts +3 -2
  46. package/lib/dist/features/mirror-feature.js +1 -1
  47. package/lib/dist/features/repeat-circular.d.ts +3 -3
  48. package/lib/dist/features/repeat-circular.js +8 -1
  49. package/lib/dist/features/repeat-linear.d.ts +4 -2
  50. package/lib/dist/features/repeat-linear.js +10 -1
  51. package/lib/dist/features/repeat-matrix.d.ts +3 -1
  52. package/lib/dist/features/repeat-matrix.js +7 -2
  53. package/lib/dist/features/rib.d.ts +31 -0
  54. package/lib/dist/features/rib.js +321 -0
  55. package/lib/dist/features/select.d.ts +1 -0
  56. package/lib/dist/features/select.js +81 -10
  57. package/lib/dist/features/shell.d.ts +4 -1
  58. package/lib/dist/features/shell.js +14 -3
  59. package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
  60. package/lib/dist/filters/edge/belongs-to-face.js +64 -15
  61. package/lib/dist/filters/filter-builder-base.d.ts +25 -0
  62. package/lib/dist/filters/filter-builder-base.js +47 -0
  63. package/lib/dist/filters/filter.js +39 -14
  64. package/lib/dist/filters/from-object.d.ts +4 -0
  65. package/lib/dist/filters/from-object.js +10 -0
  66. package/lib/dist/helpers/clone-transform.d.ts +2 -1
  67. package/lib/dist/helpers/scene-helpers.d.ts +1 -1
  68. package/lib/dist/helpers/scene-helpers.js +146 -12
  69. package/lib/dist/index.d.ts +7 -1
  70. package/lib/dist/index.js +3 -3
  71. package/lib/dist/io/file-import.d.ts +5 -1
  72. package/lib/dist/io/file-import.js +29 -18
  73. package/lib/dist/math/lazy-matrix.d.ts +31 -0
  74. package/lib/dist/math/lazy-matrix.js +66 -0
  75. package/lib/dist/oc/color-transfer.d.ts +19 -8
  76. package/lib/dist/oc/color-transfer.js +70 -12
  77. package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
  78. package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
  79. package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
  80. package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
  81. package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
  82. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
  83. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
  84. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
  85. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
  86. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
  87. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
  88. package/lib/dist/oc/extrude-ops.d.ts +2 -1
  89. package/lib/dist/oc/extrude-ops.js +51 -2
  90. package/lib/dist/oc/mesh.d.ts +9 -4
  91. package/lib/dist/oc/mesh.js +14 -13
  92. package/lib/dist/oc/rib-ops.d.ts +35 -0
  93. package/lib/dist/oc/rib-ops.js +619 -0
  94. package/lib/dist/oc/shell-ops.d.ts +2 -1
  95. package/lib/dist/oc/shell-ops.js +5 -2
  96. package/lib/dist/oc/topology-index.d.ts +6 -0
  97. package/lib/dist/oc/topology-index.js +36 -0
  98. package/lib/dist/rendering/mesh-builder.d.ts +3 -0
  99. package/lib/dist/rendering/mesh-builder.js +8 -4
  100. package/lib/dist/rendering/render-edge.d.ts +2 -1
  101. package/lib/dist/rendering/render-edge.js +2 -2
  102. package/lib/dist/rendering/render-face.d.ts +2 -1
  103. package/lib/dist/rendering/render-face.js +2 -2
  104. package/lib/dist/rendering/render-solid.d.ts +2 -1
  105. package/lib/dist/rendering/render-solid.js +3 -5
  106. package/lib/dist/rendering/render-wire.d.ts +2 -1
  107. package/lib/dist/rendering/render-wire.js +2 -2
  108. package/lib/dist/rendering/render.d.ts +4 -0
  109. package/lib/dist/rendering/render.js +50 -2
  110. package/lib/dist/rendering/scene-compare.js +3 -0
  111. package/lib/dist/rendering/scene.d.ts +1 -0
  112. package/lib/dist/rendering/scene.js +4 -0
  113. package/lib/dist/scene-manager.d.ts +4 -2
  114. package/lib/dist/scene-manager.js +12 -4
  115. package/lib/dist/tests/features/2d/arc.test.js +64 -0
  116. package/lib/dist/tests/features/2d/back.test.js +17 -1
  117. package/lib/dist/tests/features/2d/tarc.test.js +157 -0
  118. package/lib/dist/tests/features/color-lineage.test.js +18 -0
  119. package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
  120. package/lib/dist/tests/features/filter-positional.test.js +129 -0
  121. package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
  122. package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
  123. package/lib/dist/tests/features/rib.test.d.ts +1 -0
  124. package/lib/dist/tests/features/rib.test.js +598 -0
  125. package/lib/dist/tests/features/shell.test.js +36 -0
  126. package/lib/dist/tests/global-setup.js +2 -1
  127. package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
  128. package/lib/dist/tests/helpers/extract-blocks.js +56 -0
  129. package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
  130. package/lib/dist/tests/llm-docs-examples.test.js +62 -0
  131. package/lib/dist/tests/scene-compare.test.d.ts +1 -0
  132. package/lib/dist/tests/scene-compare.test.js +77 -0
  133. package/lib/dist/tests/setup.js +2 -1
  134. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  135. package/llm-docs/.coverage-allowlist.txt +9 -0
  136. package/llm-docs/api/arc.md +48 -0
  137. package/llm-docs/api/axis.md +42 -0
  138. package/llm-docs/api/bezier.md +42 -0
  139. package/llm-docs/api/booleans.md +44 -0
  140. package/llm-docs/api/chamfer.md +40 -0
  141. package/llm-docs/api/circle.md +36 -0
  142. package/llm-docs/api/color.md +34 -0
  143. package/llm-docs/api/connect.md +41 -0
  144. package/llm-docs/api/constraint-qualifiers.md +48 -0
  145. package/llm-docs/api/copy.md +63 -0
  146. package/llm-docs/api/cursor-lines.md +50 -0
  147. package/llm-docs/api/cursor-move.md +61 -0
  148. package/llm-docs/api/cut.md +55 -0
  149. package/llm-docs/api/draft.md +36 -0
  150. package/llm-docs/api/edge-filter.md +57 -0
  151. package/llm-docs/api/ellipse.md +34 -0
  152. package/llm-docs/api/extrude.md +74 -0
  153. package/llm-docs/api/face-filter.md +61 -0
  154. package/llm-docs/api/fillet.md +51 -0
  155. package/llm-docs/api/index.json +139 -0
  156. package/llm-docs/api/line.md +42 -0
  157. package/llm-docs/api/load.md +37 -0
  158. package/llm-docs/api/local.md +38 -0
  159. package/llm-docs/api/loft.md +37 -0
  160. package/llm-docs/api/mirror.md +44 -0
  161. package/llm-docs/api/offset.md +36 -0
  162. package/llm-docs/api/part.md +40 -0
  163. package/llm-docs/api/plane.md +44 -0
  164. package/llm-docs/api/polygon.md +37 -0
  165. package/llm-docs/api/primitive-solids.md +39 -0
  166. package/llm-docs/api/project-intersect.md +48 -0
  167. package/llm-docs/api/rect.md +48 -0
  168. package/llm-docs/api/remove.md +32 -0
  169. package/llm-docs/api/repeat.md +79 -0
  170. package/llm-docs/api/revolve.md +38 -0
  171. package/llm-docs/api/rib.md +40 -0
  172. package/llm-docs/api/rotate.md +37 -0
  173. package/llm-docs/api/select.md +41 -0
  174. package/llm-docs/api/shell.md +41 -0
  175. package/llm-docs/api/sketch.md +76 -0
  176. package/llm-docs/api/slot.md +36 -0
  177. package/llm-docs/api/split-trim.md +42 -0
  178. package/llm-docs/api/sweep.md +43 -0
  179. package/llm-docs/api/tarc.md +45 -0
  180. package/llm-docs/api/tcircle.md +38 -0
  181. package/llm-docs/api/tline.md +42 -0
  182. package/llm-docs/api/translate.md +40 -0
  183. package/llm-docs/api/types/aline.md +35 -0
  184. package/llm-docs/api/types/arc-angles.md +29 -0
  185. package/llm-docs/api/types/arc-points.md +48 -0
  186. package/llm-docs/api/types/axis-like.md +38 -0
  187. package/llm-docs/api/types/axis.md +21 -0
  188. package/llm-docs/api/types/boolean-operation.md +50 -0
  189. package/llm-docs/api/types/circular-repeat-options.md +31 -0
  190. package/llm-docs/api/types/common.md +32 -0
  191. package/llm-docs/api/types/cut.md +125 -0
  192. package/llm-docs/api/types/draft.md +21 -0
  193. package/llm-docs/api/types/extrudable-geometry.md +23 -0
  194. package/llm-docs/api/types/extrude.md +194 -0
  195. package/llm-docs/api/types/geometry.md +51 -0
  196. package/llm-docs/api/types/hline.md +35 -0
  197. package/llm-docs/api/types/linear-repeat-options.md +31 -0
  198. package/llm-docs/api/types/loft.md +154 -0
  199. package/llm-docs/api/types/mirror.md +35 -0
  200. package/llm-docs/api/types/offset.md +31 -0
  201. package/llm-docs/api/types/plane-like.md +35 -0
  202. package/llm-docs/api/types/plane-transform-options.md +29 -0
  203. package/llm-docs/api/types/plane.md +21 -0
  204. package/llm-docs/api/types/point-like.md +22 -0
  205. package/llm-docs/api/types/point2dlike.md +26 -0
  206. package/llm-docs/api/types/polygon.md +46 -0
  207. package/llm-docs/api/types/rect.md +128 -0
  208. package/llm-docs/api/types/revolve.md +102 -0
  209. package/llm-docs/api/types/rib.md +133 -0
  210. package/llm-docs/api/types/scene-object.md +33 -0
  211. package/llm-docs/api/types/select.md +21 -0
  212. package/llm-docs/api/types/shell.md +54 -0
  213. package/llm-docs/api/types/slot.md +43 -0
  214. package/llm-docs/api/types/sweep.md +189 -0
  215. package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
  216. package/llm-docs/api/types/transformable.md +93 -0
  217. package/llm-docs/api/types/trim.md +27 -0
  218. package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
  219. package/llm-docs/api/types/vertex.md +17 -0
  220. package/llm-docs/api/types/vline.md +35 -0
  221. package/llm-docs/concepts/coordinate-system.md +45 -0
  222. package/llm-docs/concepts/history-and-rollback.md +40 -0
  223. package/llm-docs/concepts/last-selection.md +49 -0
  224. package/llm-docs/concepts/scene-graph.md +37 -0
  225. package/llm-docs/index.json +1750 -0
  226. package/mcp/dist/client.d.ts +64 -0
  227. package/mcp/dist/client.js +248 -0
  228. package/mcp/dist/discovery.d.ts +11 -0
  229. package/mcp/dist/discovery.js +78 -0
  230. package/mcp/dist/docs-index.d.ts +81 -0
  231. package/mcp/dist/docs-index.js +261 -0
  232. package/mcp/dist/resources.d.ts +4 -0
  233. package/mcp/dist/resources.js +115 -0
  234. package/mcp/dist/server.d.ts +12 -0
  235. package/mcp/dist/server.js +489 -0
  236. package/mcp/dist/tools/coordination.d.ts +9 -0
  237. package/mcp/dist/tools/coordination.js +46 -0
  238. package/mcp/dist/tools/docs.d.ts +66 -0
  239. package/mcp/dist/tools/docs.js +122 -0
  240. package/mcp/dist/tools/engine.d.ts +56 -0
  241. package/mcp/dist/tools/engine.js +145 -0
  242. package/mcp/dist/tools/inspection.d.ts +75 -0
  243. package/mcp/dist/tools/inspection.js +121 -0
  244. package/mcp/dist/tools/screenshot.d.ts +63 -0
  245. package/mcp/dist/tools/screenshot.js +263 -0
  246. package/mcp/dist/tools/source.d.ts +84 -0
  247. package/mcp/dist/tools/source.js +434 -0
  248. package/mcp/dist/tools/workspaces.d.ts +13 -0
  249. package/mcp/dist/tools/workspaces.js +33 -0
  250. package/mcp/dist/types.d.ts +18 -0
  251. package/mcp/dist/types.js +11 -0
  252. package/package.json +19 -5
  253. package/server/dist/code-editor.d.ts +36 -0
  254. package/server/dist/code-editor.js +8 -0
  255. package/server/dist/fluidcad-server.d.ts +50 -0
  256. package/server/dist/fluidcad-server.js +153 -1
  257. package/server/dist/global-registry.d.ts +30 -0
  258. package/server/dist/global-registry.js +126 -0
  259. package/server/dist/index.js +171 -26
  260. package/server/dist/instance-file.d.ts +31 -0
  261. package/server/dist/instance-file.js +73 -0
  262. package/server/dist/lint-fluid-js.d.ts +15 -0
  263. package/server/dist/lint-fluid-js.js +271 -0
  264. package/server/dist/routes/editor.d.ts +24 -0
  265. package/server/dist/routes/editor.js +44 -0
  266. package/server/dist/routes/export.d.ts +1 -1
  267. package/server/dist/routes/export.js +45 -8
  268. package/server/dist/routes/health.d.ts +7 -0
  269. package/server/dist/routes/health.js +14 -0
  270. package/server/dist/routes/lint.d.ts +10 -0
  271. package/server/dist/routes/lint.js +28 -0
  272. package/server/dist/routes/render.d.ts +33 -0
  273. package/server/dist/routes/render.js +34 -0
  274. package/server/dist/routes/scene.d.ts +5 -0
  275. package/server/dist/routes/scene.js +48 -0
  276. package/server/dist/routes/screenshot.js +68 -1
  277. package/server/dist/ws-protocol.d.ts +56 -2
  278. package/ui/dist/assets/{index-DMw0OYCF.js → index-BdqrMDRu.js} +30 -30
  279. package/ui/dist/index.html +1 -1
@@ -0,0 +1,619 @@
1
+ import { Face } from "../common/face.js";
2
+ import { Wire } from "../common/wire.js";
3
+ import { Point } from "../math/point.js";
4
+ import { Vector3d } from "../math/vector3d.js";
5
+ import { Matrix4 } from "../math/matrix4.js";
6
+ import { WireOps } from "./wire-ops.js";
7
+ import { FaceOps } from "./face-ops.js";
8
+ import { EdgeOps } from "./edge-ops.js";
9
+ import { ShapeOps } from "./shape-ops.js";
10
+ import { ShapeFactory } from "../common/shape-factory.js";
11
+ import { Explorer } from "./explorer.js";
12
+ import { BooleanOps } from "./boolean-ops.js";
13
+ import { Convert } from "./convert.js";
14
+ import { getOC } from "./init.js";
15
+ function planesEqual(a, b, distTol, _angTol) {
16
+ // Two unit normals are aligned (parallel or anti-parallel) when |dot| ≈ 1.
17
+ // 1e-6 is well below any practical face precision and well above OCC's
18
+ // numerical noise.
19
+ const dot = Math.abs(a.normal.dot(b.normal));
20
+ if (1 - dot > 1e-6) {
21
+ return false;
22
+ }
23
+ const offset = a.origin.vectorTo(b.origin);
24
+ const d = Math.abs(offset.dot(b.normal));
25
+ return d <= distTol;
26
+ }
27
+ // Linearly samples points along a polyline wire by edge length. Used by
28
+ // the rib conformance to test which output solid encloses the spine
29
+ // (real rib) vs. only touches its boundary (phantom fragment).
30
+ function sampleSpinePoints(spineWire, count) {
31
+ const edges = spineWire.getEdges();
32
+ if (edges.length === 0) {
33
+ return [];
34
+ }
35
+ const verts = [edges[0].getFirstVertex().toPoint()];
36
+ for (const e of edges) {
37
+ verts.push(e.getLastVertex().toPoint());
38
+ }
39
+ // Cumulative arc length along the polyline.
40
+ const cum = [0];
41
+ for (let i = 1; i < verts.length; i++) {
42
+ cum.push(cum[i - 1] + verts[i - 1].vectorTo(verts[i]).length());
43
+ }
44
+ const total = cum[cum.length - 1];
45
+ if (total <= 0) {
46
+ return [verts[0]];
47
+ }
48
+ const out = [];
49
+ // Skip pure endpoints — they often coincide with cut boundaries and
50
+ // classify as TopAbs_ON. Sample at fractions 1/(N+1) … N/(N+1).
51
+ for (let i = 1; i <= count; i++) {
52
+ const target = (total * i) / (count + 1);
53
+ let seg = 1;
54
+ while (seg < cum.length - 1 && cum[seg] < target) {
55
+ seg++;
56
+ }
57
+ const segLen = cum[seg] - cum[seg - 1];
58
+ const t = segLen > 0 ? (target - cum[seg - 1]) / segLen : 0;
59
+ const a = verts[seg - 1];
60
+ const b = verts[seg];
61
+ out.push(new Point(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t));
62
+ }
63
+ return out;
64
+ }
65
+ export class RibOps {
66
+ static makeRibProfile(spineWire, thickness, plane) {
67
+ const halfThickness = Math.abs(thickness) / 2;
68
+ const wire1 = RibOps.offsetWireOnPlane(spineWire, plane, halfThickness);
69
+ const wire2 = RibOps.offsetWireOnPlane(spineWire, plane, -halfThickness);
70
+ return RibOps.makeOpenFaceWithCaps(wire1, wire2);
71
+ }
72
+ static makeRibProfileParallel(spineWire, thickness, plane) {
73
+ const closedWire = RibOps.makeParallelRibClosedWire(spineWire, thickness, plane);
74
+ return Face.fromTopoDSFace(FaceOps.makeFaceFromWires([closedWire.getShape()]));
75
+ }
76
+ // Closed boundary wire for the parallel-mode rib profile: spine offset
77
+ // by ±halfThickness along plane.normal with two cap edges joining the
78
+ // ends. Used as a section wire for `makeTaperedRibPrism` and as the
79
+ // outer boundary of `makeRibProfileParallel`.
80
+ static makeParallelRibClosedWire(spineWire, thickness, plane) {
81
+ const halfThickness = Math.abs(thickness) / 2;
82
+ const offset1 = plane.normal.multiply(halfThickness);
83
+ const offset2 = plane.normal.multiply(-halfThickness);
84
+ const wire1 = ShapeOps.transform(spineWire, Matrix4.fromTranslationVector(offset1));
85
+ const wire2 = ShapeOps.transform(spineWire, Matrix4.fromTranslationVector(offset2));
86
+ return RibOps.makeClosedWireWithCaps(wire1, wire2);
87
+ }
88
+ // Lofts a tapered prism between two parallel-mode rib profile wires:
89
+ // the base on the spine plane (full thickness) and the tip translated
90
+ // along `direction × extrudeLength` with thickness shrunk by
91
+ // `extrudeLength × tan(draftAngleRad)`. The base profile face is the
92
+ // returned `firstFace`, exact by construction — no draft API is
93
+ // involved, so the spine-plane face stays at the original thickness
94
+ // with zero drift. Use this in place of an extrude + post-draft when
95
+ // exact start-face preservation matters.
96
+ //
97
+ // `draftAngleRad` follows the user-facing convention: positive tapers
98
+ // the tip inward, negative widens it. For very large positive angles
99
+ // the tip would invert past the spine; the tip half-thickness is
100
+ // clamped to a sub-precision positive value and conformance trims
101
+ // anything past the cavity.
102
+ static makeTaperedRibPrism(spineWire, thickness, plane, direction, extrudeLength, draftAngleRad) {
103
+ const oc = getOC();
104
+ const halfThickness = Math.abs(thickness) / 2;
105
+ const baseClosedWire = RibOps.makeParallelRibClosedWire(spineWire, thickness, plane);
106
+ const minHalf = oc.Precision.Confusion() * 100;
107
+ let tipHalfThickness = halfThickness - extrudeLength * Math.tan(draftAngleRad);
108
+ if (tipHalfThickness <= 0) {
109
+ tipHalfThickness = minHalf;
110
+ }
111
+ const tipFullThickness = tipHalfThickness * 2;
112
+ const tipBaseWire = RibOps.makeParallelRibClosedWire(spineWire, tipFullThickness, plane);
113
+ const translation = direction.multiply(extrudeLength);
114
+ const tipClosedWire = ShapeOps.transform(tipBaseWire, Matrix4.fromTranslationVector(translation));
115
+ const loft = new oc.BRepOffsetAPI_ThruSections(true, true, oc.Precision.Confusion());
116
+ loft.AddWire(baseClosedWire.getShape());
117
+ loft.AddWire(tipClosedWire.getShape());
118
+ const progress = new oc.Message_ProgressRange();
119
+ loft.Build(progress);
120
+ progress.delete();
121
+ if (!loft.IsDone()) {
122
+ loft.delete();
123
+ throw new Error("Tapered rib loft failed");
124
+ }
125
+ const solid = loft.Shape();
126
+ const firstFace = loft.FirstShape();
127
+ const lastFace = loft.LastShape();
128
+ loft.delete();
129
+ return {
130
+ solid: ShapeFactory.fromShape(solid),
131
+ firstFace: ShapeFactory.fromShape(firstFace),
132
+ lastFace: ShapeFactory.fromShape(lastFace),
133
+ };
134
+ }
135
+ static computeSpinePerpendicularDirection(spineWire, plane) {
136
+ const start = spineWire.getFirstVertex().toPoint().toVector3d();
137
+ const end = spineWire.getLastVertex().toPoint().toVector3d();
138
+ const spineDir = end.subtract(start).normalize();
139
+ return plane.normal.cross(spineDir).normalize();
140
+ }
141
+ // Over-extends the spine endpoints along their tangents by 2× the scope bbox
142
+ // diagonal. The downstream BRepAlgoAPI_Cut against the scope is what actually
143
+ // carves the rib to the cavity; this just guarantees the pre-cut profile fully
144
+ // overlaps every cavity boundary regardless of curvature (drafted cones,
145
+ // fillets, etc.) — so the cut produces a clean blend on every face it touches.
146
+ static extendSpineWire(spineWire, scopeShapes, _plane) {
147
+ const edges = spineWire.getEdges();
148
+ if (edges.length === 0) {
149
+ return spineWire;
150
+ }
151
+ const firstVertex = spineWire.getFirstVertex().toPoint();
152
+ const lastVertex = spineWire.getLastVertex().toPoint();
153
+ const lastEdge = edges[edges.length - 1];
154
+ const endTangent = EdgeOps.getEdgeTangentAtEnd(lastEdge).normalize();
155
+ const firstEdge = edges[0];
156
+ const firstEdgeEnd = EdgeOps.getLastVertex(firstEdge).toPoint();
157
+ const startTangent = firstVertex.vectorTo(firstEdgeEnd).normalize();
158
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
159
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
160
+ for (const s of scopeShapes) {
161
+ const bb = ShapeOps.getBoundingBox(s);
162
+ if (bb.minX < minX) {
163
+ minX = bb.minX;
164
+ }
165
+ if (bb.minY < minY) {
166
+ minY = bb.minY;
167
+ }
168
+ if (bb.minZ < minZ) {
169
+ minZ = bb.minZ;
170
+ }
171
+ if (bb.maxX > maxX) {
172
+ maxX = bb.maxX;
173
+ }
174
+ if (bb.maxY > maxY) {
175
+ maxY = bb.maxY;
176
+ }
177
+ if (bb.maxZ > maxZ) {
178
+ maxZ = bb.maxZ;
179
+ }
180
+ }
181
+ const dx = maxX - minX, dy = maxY - minY, dz = maxZ - minZ;
182
+ const diag = Math.sqrt(dx * dx + dy * dy + dz * dz);
183
+ if (!isFinite(diag) || diag <= 0) {
184
+ return spineWire;
185
+ }
186
+ const ext = 2 * diag;
187
+ const startExtPoint = firstVertex.add(startTangent.multiply(-ext));
188
+ const endExtPoint = lastVertex.add(endTangent.multiply(ext));
189
+ const newEdges = [
190
+ EdgeOps.makeLineEdge(startExtPoint, firstVertex),
191
+ ...edges,
192
+ EdgeOps.makeLineEdge(lastVertex, endExtPoint),
193
+ ];
194
+ return WireOps.makeWireFromEdges(newEdges);
195
+ }
196
+ static computeExtrudeDistanceAlongDirection(direction, origin, scopeShapes) {
197
+ let maxDist = 0;
198
+ for (const shape of scopeShapes) {
199
+ const bbox = ShapeOps.getBoundingBox(shape);
200
+ const corners = [
201
+ new Point(bbox.minX, bbox.minY, bbox.minZ),
202
+ new Point(bbox.maxX, bbox.minY, bbox.minZ),
203
+ new Point(bbox.minX, bbox.maxY, bbox.minZ),
204
+ new Point(bbox.maxX, bbox.maxY, bbox.minZ),
205
+ new Point(bbox.minX, bbox.minY, bbox.maxZ),
206
+ new Point(bbox.maxX, bbox.minY, bbox.maxZ),
207
+ new Point(bbox.minX, bbox.maxY, bbox.maxZ),
208
+ new Point(bbox.maxX, bbox.maxY, bbox.maxZ),
209
+ ];
210
+ for (const corner of corners) {
211
+ const offset = origin.vectorTo(corner);
212
+ const dist = Math.abs(offset.dot(direction));
213
+ if (dist > maxDist) {
214
+ maxDist = dist;
215
+ }
216
+ }
217
+ }
218
+ return maxDist + 1e-3;
219
+ }
220
+ static computeExtrudeDistance(plane, scopeShapes) {
221
+ let maxDist = 0;
222
+ const origin = plane.origin;
223
+ const normal = plane.normal;
224
+ for (const shape of scopeShapes) {
225
+ const bbox = ShapeOps.getBoundingBox(shape);
226
+ const corners = [
227
+ new Point(bbox.minX, bbox.minY, bbox.minZ),
228
+ new Point(bbox.maxX, bbox.minY, bbox.minZ),
229
+ new Point(bbox.minX, bbox.maxY, bbox.minZ),
230
+ new Point(bbox.maxX, bbox.maxY, bbox.minZ),
231
+ new Point(bbox.minX, bbox.minY, bbox.maxZ),
232
+ new Point(bbox.maxX, bbox.minY, bbox.maxZ),
233
+ new Point(bbox.minX, bbox.maxY, bbox.maxZ),
234
+ new Point(bbox.maxX, bbox.maxY, bbox.maxZ),
235
+ ];
236
+ for (const corner of corners) {
237
+ const offset = origin.vectorTo(corner);
238
+ const dist = Math.abs(offset.dot(normal));
239
+ if (dist > maxDist) {
240
+ maxDist = dist;
241
+ }
242
+ }
243
+ }
244
+ return maxDist + 1e-3;
245
+ }
246
+ // Conforms a (possibly over-extended) prismatic rib to the cavity defined by
247
+ // `scopeShapes`. The bbox slabs clip protrusion through openings; the scope
248
+ // cut blends the rib into the cavity walls (planar, fillet, drafted cone,
249
+ // etc.). The original (un-extended) spine wire is used to keep the connected
250
+ // component(s) the user actually drew and drop outer fragments left behind
251
+ // when the prism pokes past the scope's outer walls. Face classification
252
+ // uses the scope cut's lineage (Modified) on the rib's first/last/side
253
+ // faces, fixed up by IsSame against the post-slab faces.
254
+ static conformRibToScope(ribSolid, scopeShapes, originalSpineWire, prismFirstFace, prismLastFace, extrudeDirection) {
255
+ const oc = getOC();
256
+ // Phase 1: clip protrusion through openings using axis-aligned bbox slabs.
257
+ // Sequential cuts here — multi-tool cuts with very large slabs trigger BOP
258
+ // failures in some configurations.
259
+ let trimmed = ribSolid;
260
+ const slabs = RibOps.buildBoundingBoxSlabs(scopeShapes);
261
+ for (const slab of slabs) {
262
+ trimmed = BooleanOps.cutShapes(trimmed, slab);
263
+ }
264
+ // Phase 2: cut by scope compound, with lineage so the result faces can be
265
+ // mapped back to the prism's first / last / side faces.
266
+ const scopeCompound = ShapeOps.makeCompound(scopeShapes);
267
+ const stockList = new oc.TopTools_ListOfShape();
268
+ stockList.Append(trimmed.getShape());
269
+ const toolList = new oc.TopTools_ListOfShape();
270
+ toolList.Append(scopeCompound.getShape());
271
+ const progress = new oc.Message_ProgressRange();
272
+ const cutMaker = new oc.BRepAlgoAPI_Cut();
273
+ cutMaker.SetArguments(stockList);
274
+ cutMaker.SetTools(toolList);
275
+ cutMaker.SetNonDestructive(true);
276
+ cutMaker.SetRunParallel(true);
277
+ cutMaker.Build(progress);
278
+ if (!cutMaker.IsDone() || cutMaker.HasErrors()) {
279
+ cutMaker.delete();
280
+ stockList.delete();
281
+ toolList.delete();
282
+ progress.delete();
283
+ throw new Error("Rib conformance cut failed");
284
+ }
285
+ const cutResult = cutMaker.Shape();
286
+ const rawSolids = Explorer.findShapes(cutResult, Explorer.getOcShapeType("solid"));
287
+ const allSolids = rawSolids.map(s => ShapeFactory.fromShape(s));
288
+ // Pick the solid(s) whose interior contains the original spine. The
289
+ // rib was built by extruding a profile centred on the spine, so the
290
+ // spine lies inside the rib's volume by construction. Phantom
291
+ // fragments left by the boolean cut (thin shells tracing cavity
292
+ // walls, slivers at wall corners, outer over-extension leftovers)
293
+ // touch the spine on their boundary at most — never enclose it.
294
+ //
295
+ // We sample multiple points along the spine and use OCC's
296
+ // BRepClass3d_SolidClassifier to test interior containment. A solid
297
+ // is kept iff at least one sampled point classifies as TopAbs_IN
298
+ // (= strictly inside, not on the boundary). This naturally handles
299
+ // every threshold-prone case the previous distance + volume filter
300
+ // missed:
301
+ // - L-shaped wall-trace phantoms (boundary contact, no interior
302
+ // containment) → dropped.
303
+ // - sub-mm³ corner slivers → dropped.
304
+ // - rib that legitimately splits past a cone into two halves →
305
+ // each half contains its own portion of the spine, both kept.
306
+ const tolPoint = oc.Precision.Confusion() * 10;
307
+ // The spine itself lies ON the prism's start face, so testing
308
+ // raw spine points returns TopAbs_ON for the real rib too. Nudge
309
+ // each sample point a small distance along the extrude direction
310
+ // (= into the rib body, away from the start face) so an interior
311
+ // hit means the solid encloses the spine plus a thin ribbon
312
+ // forward of it — characteristic of the real rib but not of
313
+ // wall-trace phantoms or boundary slivers.
314
+ const dn = extrudeDirection.normalize();
315
+ const nudge = 1e-2;
316
+ const samplePoints = sampleSpinePoints(originalSpineWire, 7).map(pt => pt.add(dn.multiply(nudge)));
317
+ const keptSolids = [];
318
+ for (const solid of allSolids) {
319
+ let containsSpine = false;
320
+ for (const pt of samplePoints) {
321
+ const [gpPnt, dispose] = Convert.toGpPnt(pt);
322
+ const classifier = new oc.BRepClass3d_SolidClassifier(solid.getShape(), gpPnt, tolPoint);
323
+ const state = classifier.State();
324
+ classifier.delete();
325
+ dispose();
326
+ if (state === oc.TopAbs_State.TopAbs_IN) {
327
+ containsSpine = true;
328
+ break;
329
+ }
330
+ }
331
+ if (containsSpine) {
332
+ keptSolids.push(solid);
333
+ }
334
+ }
335
+ let resultSolids = keptSolids;
336
+ if (resultSolids.length === 0 && allSolids.length > 0) {
337
+ // Fallback: if no solid contained any sampled spine point (rare —
338
+ // would mean every sample landed exactly on a face), keep the
339
+ // largest by volume. Defensive — shouldn't trip in practice.
340
+ let best = null;
341
+ let bestVol = -Infinity;
342
+ for (const s of allSolids) {
343
+ const vp = new oc.GProp_GProps();
344
+ oc.BRepGProp.VolumeProperties(s.getShape(), vp, false, false, false);
345
+ const v = vp.Mass();
346
+ vp.delete();
347
+ if (v > bestVol) {
348
+ bestVol = v;
349
+ best = s;
350
+ }
351
+ }
352
+ if (best) {
353
+ resultSolids = [best];
354
+ }
355
+ }
356
+ // Map post-slab faces back to first / last / side categories. Slab cuts
357
+ // don't track lineage, so use IsSame against the prism's first/last faces
358
+ // to recognize unmodified faces, plus surface plane comparison for faces
359
+ // that slabs split into pieces. Side faces are everything else in the
360
+ // post-slab shape.
361
+ const trimmedFaces = Explorer.findShapes(trimmed.getShape(), Explorer.getOcShapeType("face"));
362
+ const trimmedFirstFaces = [];
363
+ const trimmedLastFaces = [];
364
+ const trimmedSideFaces = [];
365
+ const firstPlane = RibOps.tryGetFacePlane(prismFirstFace.getShape());
366
+ const lastPlane = RibOps.tryGetFacePlane(prismLastFace.getShape());
367
+ const planeTol = oc.Precision.Confusion() * 10;
368
+ const angTol = oc.Precision.Angular();
369
+ for (const tf of trimmedFaces) {
370
+ if (tf.IsSame(prismFirstFace.getShape())) {
371
+ trimmedFirstFaces.push(tf);
372
+ continue;
373
+ }
374
+ if (tf.IsSame(prismLastFace.getShape())) {
375
+ trimmedLastFaces.push(tf);
376
+ continue;
377
+ }
378
+ const tfPlane = RibOps.tryGetFacePlane(tf);
379
+ if (tfPlane && firstPlane && planesEqual(tfPlane, firstPlane, planeTol, angTol)) {
380
+ trimmedFirstFaces.push(tf);
381
+ continue;
382
+ }
383
+ if (tfPlane && lastPlane && planesEqual(tfPlane, lastPlane, planeTol, angTol)) {
384
+ trimmedLastFaces.push(tf);
385
+ continue;
386
+ }
387
+ trimmedSideFaces.push(tf);
388
+ }
389
+ // Build maps of (original + modified images) for first / last / side
390
+ // through the scope cut. Anything in the kept solids' faces that doesn't
391
+ // fall in one of these is a new face from the scope cut — i.e. the
392
+ // conformal blend with a scope cavity face.
393
+ const firstMap = new oc.TopTools_MapOfShape();
394
+ for (const f of trimmedFirstFaces) {
395
+ RibOps.collectFaceImages(cutMaker, f, firstMap);
396
+ }
397
+ const lastMap = new oc.TopTools_MapOfShape();
398
+ for (const f of trimmedLastFaces) {
399
+ RibOps.collectFaceImages(cutMaker, f, lastMap);
400
+ }
401
+ const sideMap = new oc.TopTools_MapOfShape();
402
+ for (const f of trimmedSideFaces) {
403
+ RibOps.collectFaceImages(cutMaker, f, sideMap);
404
+ }
405
+ const startFaces = [];
406
+ const endFaces = [];
407
+ const sideFaces = [];
408
+ const internalFaces = [];
409
+ const seen = new oc.TopTools_MapOfShape();
410
+ for (const solid of resultSolids) {
411
+ const faces = Explorer.findShapes(solid.getShape(), Explorer.getOcShapeType("face"));
412
+ for (const f of faces) {
413
+ if (!seen.Add(f)) {
414
+ continue;
415
+ }
416
+ const wrapped = Face.fromTopoDSFace(Explorer.toFace(f));
417
+ if (firstMap.Contains(f)) {
418
+ startFaces.push(wrapped);
419
+ }
420
+ else if (lastMap.Contains(f)) {
421
+ endFaces.push(wrapped);
422
+ }
423
+ else if (sideMap.Contains(f)) {
424
+ sideFaces.push(wrapped);
425
+ }
426
+ else {
427
+ internalFaces.push(wrapped);
428
+ }
429
+ }
430
+ }
431
+ firstMap.delete();
432
+ lastMap.delete();
433
+ sideMap.delete();
434
+ seen.delete();
435
+ cutMaker.delete();
436
+ stockList.delete();
437
+ toolList.delete();
438
+ progress.delete();
439
+ // Final pass: ShapeUpgrade_UnifySameDomain merges adjacent coplanar faces
440
+ // and redundant edges left by the cut sequence (slab clips split flat
441
+ // walls into multiple sub-faces with extraneous seam edges). The lineage
442
+ // returned by cleanShapeWithLineage maps each pre-clean face to its
443
+ // post-clean image so the start / end / side / internal buckets remain
444
+ // valid for downstream face-selection queries.
445
+ const cleanedSolids = [];
446
+ const lineages = [];
447
+ for (const solid of resultSolids) {
448
+ const lineage = ShapeOps.cleanShapeWithLineage(solid);
449
+ cleanedSolids.push(lineage.shape);
450
+ lineages.push(lineage);
451
+ }
452
+ const remapBucket = (faces) => {
453
+ const out = [];
454
+ const seenOut = new oc.TopTools_MapOfShape();
455
+ for (const f of faces) {
456
+ let mapped = null;
457
+ for (const lineage of lineages) {
458
+ const r = lineage.remapFace(f);
459
+ if (r !== null) {
460
+ mapped = r;
461
+ break;
462
+ }
463
+ }
464
+ const kept = mapped ?? [f];
465
+ for (const m of kept) {
466
+ if (seenOut.Add(m.getShape())) {
467
+ out.push(m);
468
+ }
469
+ }
470
+ }
471
+ seenOut.delete();
472
+ return out;
473
+ };
474
+ const finalStart = remapBucket(startFaces);
475
+ const finalEnd = remapBucket(endFaces);
476
+ const finalSide = remapBucket(sideFaces);
477
+ const finalInternal = remapBucket(internalFaces);
478
+ for (const lineage of lineages) {
479
+ lineage.dispose();
480
+ }
481
+ return {
482
+ solids: cleanedSolids,
483
+ startFaces: finalStart,
484
+ endFaces: finalEnd,
485
+ sideFaces: finalSide,
486
+ internalFaces: finalInternal,
487
+ };
488
+ }
489
+ static collectFaceImages(cutMaker, raw, map) {
490
+ map.Add(raw);
491
+ const modList = cutMaker.Modified(raw);
492
+ while (modList.Size() > 0) {
493
+ map.Add(modList.First());
494
+ modList.RemoveFirst();
495
+ }
496
+ modList.delete();
497
+ }
498
+ static tryGetFacePlane(face) {
499
+ const oc = getOC();
500
+ try {
501
+ const adaptor = new oc.BRepAdaptor_Surface(oc.TopoDS.Face(face), true);
502
+ if (adaptor.GetType() !== oc.GeomAbs_SurfaceType.GeomAbs_Plane) {
503
+ adaptor.delete();
504
+ return null;
505
+ }
506
+ const pln = adaptor.Plane();
507
+ const loc = pln.Location();
508
+ const ax = pln.Axis().Direction();
509
+ const result = {
510
+ origin: new Point(loc.X(), loc.Y(), loc.Z()),
511
+ normal: new Vector3d(ax.X(), ax.Y(), ax.Z()).normalize(),
512
+ };
513
+ adaptor.delete();
514
+ return result;
515
+ }
516
+ catch {
517
+ return null;
518
+ }
519
+ }
520
+ static buildBoundingBoxSlabs(scopeShapes) {
521
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
522
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
523
+ for (const s of scopeShapes) {
524
+ const bbox = ShapeOps.getBoundingBox(s);
525
+ minX = Math.min(minX, bbox.minX);
526
+ minY = Math.min(minY, bbox.minY);
527
+ minZ = Math.min(minZ, bbox.minZ);
528
+ maxX = Math.max(maxX, bbox.maxX);
529
+ maxY = Math.max(maxY, bbox.maxY);
530
+ maxZ = Math.max(maxZ, bbox.maxZ);
531
+ }
532
+ const BIG = 10000;
533
+ return [
534
+ RibOps.makeAxisAlignedSlab(minX - BIG, -BIG, -BIG, minX, BIG, BIG),
535
+ RibOps.makeAxisAlignedSlab(maxX, -BIG, -BIG, maxX + BIG, BIG, BIG),
536
+ RibOps.makeAxisAlignedSlab(-BIG, minY - BIG, -BIG, BIG, minY, BIG),
537
+ RibOps.makeAxisAlignedSlab(-BIG, maxY, -BIG, BIG, maxY + BIG, BIG),
538
+ RibOps.makeAxisAlignedSlab(-BIG, -BIG, minZ - BIG, BIG, BIG, minZ),
539
+ RibOps.makeAxisAlignedSlab(-BIG, -BIG, maxZ, BIG, BIG, maxZ + BIG),
540
+ ];
541
+ }
542
+ static makeAxisAlignedSlab(x1, y1, z1, x2, y2, z2) {
543
+ const c1 = new Point(x1, y1, z1);
544
+ const c2 = new Point(x2, y1, z1);
545
+ const c3 = new Point(x2, y2, z1);
546
+ const c4 = new Point(x1, y2, z1);
547
+ const wire = WireOps.makeWireFromEdges([
548
+ EdgeOps.makeLineEdge(c1, c2),
549
+ EdgeOps.makeLineEdge(c2, c3),
550
+ EdgeOps.makeLineEdge(c3, c4),
551
+ EdgeOps.makeLineEdge(c4, c1),
552
+ ]);
553
+ const face = Face.fromTopoDSFace(FaceOps.makeFaceFromWires([wire.getShape()]));
554
+ const oc = getOC();
555
+ const dz = z2 - z1;
556
+ const [vec, dispose] = Convert.toGpVec(new Vector3d(0, 0, dz));
557
+ const prism = new oc.BRepPrimAPI_MakePrism(face.getShape(), vec, false, true);
558
+ const result = prism.Shape();
559
+ prism.delete();
560
+ dispose();
561
+ return ShapeFactory.fromShape(result);
562
+ }
563
+ static offsetWireOnPlane(wire, plane, distance) {
564
+ const oc = getOC();
565
+ if (distance < 0) {
566
+ const reversed = WireOps.reverseWire(wire);
567
+ const result = RibOps.offsetWireOnPlane(reversed, plane, -distance);
568
+ return WireOps.reverseWire(result);
569
+ }
570
+ const [pln, disposePlane] = Convert.toGpPln(plane);
571
+ const faceMaker = new oc.BRepBuilderAPI_MakeFace(pln);
572
+ if (!faceMaker.IsDone()) {
573
+ faceMaker.delete();
574
+ disposePlane();
575
+ throw new Error("Failed to create reference face for rib offset");
576
+ }
577
+ const face = faceMaker.Face();
578
+ faceMaker.delete();
579
+ disposePlane();
580
+ const maker = new oc.BRepOffsetAPI_MakeOffset();
581
+ maker.Init(face, oc.GeomAbs_JoinType.GeomAbs_Arc, true);
582
+ maker.AddWire(wire.getShape());
583
+ maker.Perform(distance, 0);
584
+ if (!maker.IsDone()) {
585
+ maker.delete();
586
+ throw new Error("Failed to offset wire for rib profile");
587
+ }
588
+ const result = maker.Shape();
589
+ maker.delete();
590
+ if (Explorer.isWire(result)) {
591
+ return Wire.fromTopoDSWire(oc.TopoDS.Wire(result));
592
+ }
593
+ const wires = Explorer.findShapes(result, oc.TopAbs_ShapeEnum.TopAbs_WIRE);
594
+ if (wires.length === 0) {
595
+ throw new Error("Rib offset produced no usable wire");
596
+ }
597
+ return Wire.fromTopoDSWire(oc.TopoDS.Wire(wires[0]));
598
+ }
599
+ static makeOpenFaceWithCaps(wire1, wire2) {
600
+ const closedWire = RibOps.makeClosedWireWithCaps(wire1, wire2);
601
+ return Face.fromTopoDSFace(FaceOps.makeFaceFromWires([closedWire.getShape()]));
602
+ }
603
+ static makeClosedWireWithCaps(wire1, wire2) {
604
+ const wire1End = wire1.getLastVertex().toPoint();
605
+ const wire2Start = wire2.getFirstVertex().toPoint();
606
+ const wire2End = wire2.getLastVertex().toPoint();
607
+ const wire1Start = wire1.getFirstVertex().toPoint();
608
+ const cap1 = EdgeOps.makeLineEdge(wire1End, wire2End);
609
+ const cap2 = EdgeOps.makeLineEdge(wire2Start, wire1Start);
610
+ const reversedWire2 = WireOps.reverseWire(wire2);
611
+ const allEdges = [
612
+ ...wire1.getEdges(),
613
+ cap1,
614
+ ...reversedWire2.getEdges(),
615
+ cap2,
616
+ ];
617
+ return WireOps.makeWireFromEdges(allEdges);
618
+ }
619
+ }
@@ -1,5 +1,6 @@
1
1
  import { Shape } from "../common/shape.js";
2
2
  import { Face } from "../common/face.js";
3
+ import { ShellJoinType } from "../core/interfaces.js";
3
4
  export declare class ShellOps {
4
- static makeThickSolid(solid: Shape, faces: Face[], thickness: number): Shape;
5
+ static makeThickSolid(solid: Shape, faces: Face[], thickness: number, joinType?: ShellJoinType): Shape;
5
6
  }
@@ -3,15 +3,18 @@ import { ShapeOps } from "./shape-ops.js";
3
3
  import { ShapeFactory } from "../common/shape-factory.js";
4
4
  import { ColorTransfer } from "./color-transfer.js";
5
5
  export class ShellOps {
6
- static makeThickSolid(solid, faces, thickness) {
6
+ static makeThickSolid(solid, faces, thickness, joinType = 'arc') {
7
7
  const oc = getOC();
8
8
  const listOfFaces = new oc.TopTools_ListOfShape();
9
9
  for (const f of faces) {
10
10
  listOfFaces.Append(f.getShape());
11
11
  }
12
+ const ocJoinType = joinType === 'intersection' ? oc.GeomAbs_JoinType.GeomAbs_Intersection
13
+ : joinType === 'tangent' ? oc.GeomAbs_JoinType.GeomAbs_Tangent
14
+ : oc.GeomAbs_JoinType.GeomAbs_Arc;
12
15
  const maker = new oc.BRepOffsetAPI_MakeThickSolid();
13
16
  const progress = new oc.Message_ProgressRange();
14
- maker.MakeThickSolidByJoin(oc.TopoDS.Solid(solid.getShape()), listOfFaces, thickness, oc.Precision.Confusion(), oc.BRepOffset_Mode.BRepOffset_Skin, false, false, oc.GeomAbs_JoinType.GeomAbs_Arc, false, progress);
17
+ maker.MakeThickSolidByJoin(oc.TopoDS.Solid(solid.getShape()), listOfFaces, thickness, oc.Precision.Confusion(), oc.BRepOffset_Mode.BRepOffset_Skin, false, false, ocJoinType, false, progress);
15
18
  progress.delete();
16
19
  if (!maker.IsDone()) {
17
20
  maker.delete();
@@ -0,0 +1,6 @@
1
+ import type { TopoDS_Shape, TopTools_IndexedDataMapOfShapeListOfShape, TopTools_MapOfShape } from "occjs-wrapper";
2
+ export declare class TopologyIndex {
3
+ static buildEdgeToFaces(root: TopoDS_Shape): TopTools_IndexedDataMapOfShapeListOfShape;
4
+ static buildShapeSet(shapes: TopoDS_Shape[]): TopTools_MapOfShape;
5
+ static seekShapes(index: TopTools_IndexedDataMapOfShapeListOfShape, key: TopoDS_Shape): TopoDS_Shape[];
6
+ }