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
@@ -1,9 +1,11 @@
1
1
  import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
2
2
  import { Matrix4 } from "../math/matrix4.js";
3
+ import { LazyMatrix } from "../math/lazy-matrix.js";
3
4
  export declare class RepeatMatrix extends SceneObject {
4
5
  private _matrix;
5
6
  targetObjects: SceneObject[];
6
- constructor(_matrix: Matrix4, targetObjects: SceneObject[]);
7
+ private sources;
8
+ constructor(_matrix: LazyMatrix, targetObjects: SceneObject[], sources?: SceneObject[]);
7
9
  getTransformMatrix(): Matrix4 | null;
8
10
  isContainer(): boolean;
9
11
  build(context: BuildSceneObjectContext): void;
@@ -2,19 +2,24 @@ import { SceneObject } from "../common/scene-object.js";
2
2
  export class RepeatMatrix extends SceneObject {
3
3
  _matrix;
4
4
  targetObjects;
5
- constructor(_matrix, targetObjects) {
5
+ sources;
6
+ constructor(_matrix, targetObjects, sources = []) {
6
7
  super();
7
8
  this._matrix = _matrix;
8
9
  this.targetObjects = targetObjects;
10
+ this.sources = sources;
9
11
  this.setAlwaysVisible();
10
12
  }
11
13
  getTransformMatrix() {
12
- return this._matrix;
14
+ return this._matrix.resolve();
13
15
  }
14
16
  isContainer() {
15
17
  return true;
16
18
  }
17
19
  build(context) {
20
+ for (const source of this.sources) {
21
+ source.removeShapes(this);
22
+ }
18
23
  this.saveShapesSnapshot(context);
19
24
  }
20
25
  compareTo(other) {
@@ -0,0 +1,31 @@
1
+ import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
2
+ import { Extrudable } from "../helpers/types.js";
3
+ import { ExtrudeBase } from "./extrude-base.js";
4
+ import { IRib } from "../core/interfaces.js";
5
+ export declare class Rib extends ExtrudeBase implements IRib {
6
+ private _thickness;
7
+ private _spine;
8
+ private _parallel;
9
+ private _extend;
10
+ constructor(thickness: number, spine: SceneObject, extrudable?: Extrudable);
11
+ get thickness(): number;
12
+ get spine(): SceneObject;
13
+ parallel(): this;
14
+ extend(): this;
15
+ validate(): void;
16
+ build(context: BuildSceneObjectContext): void;
17
+ private getSpineWire;
18
+ getDependencies(): SceneObject[];
19
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
20
+ compareTo(other: Rib): boolean;
21
+ getType(): string;
22
+ serialize(): {
23
+ thickness: number;
24
+ spine: any;
25
+ extrudable: any;
26
+ operationMode: "new" | "remove";
27
+ parallel: true;
28
+ extend: true;
29
+ draft: number | [number, number];
30
+ };
31
+ }
@@ -0,0 +1,321 @@
1
+ import { Face } from "../common/face.js";
2
+ import { ExtrudeBase } from "./extrude-base.js";
3
+ import { ExtrudeOps } from "../oc/extrude-ops.js";
4
+ import { Explorer } from "../oc/explorer.js";
5
+ import { FaceQuery } from "../oc/face-query.js";
6
+ import { RibOps } from "../oc/rib-ops.js";
7
+ import { WireOps } from "../oc/wire-ops.js";
8
+ import { requireShapes } from "../common/operand-check.js";
9
+ export class Rib extends ExtrudeBase {
10
+ _thickness;
11
+ _spine;
12
+ _parallel = false;
13
+ _extend = false;
14
+ constructor(thickness, spine, extrudable) {
15
+ super(extrudable);
16
+ this._thickness = thickness;
17
+ this._spine = spine;
18
+ }
19
+ get thickness() {
20
+ return this._thickness;
21
+ }
22
+ get spine() {
23
+ return this._spine;
24
+ }
25
+ parallel() {
26
+ this._parallel = true;
27
+ return this;
28
+ }
29
+ extend() {
30
+ this._extend = true;
31
+ return this;
32
+ }
33
+ validate() {
34
+ requireShapes(this._spine, "spine", "rib");
35
+ }
36
+ build(context) {
37
+ const p = context.getProfiler();
38
+ const plane = this.extrudable.getPlane();
39
+ const originalSpineWire = p.record('Get spine wire', () => this.getSpineWire(this._spine));
40
+ let spineWire = originalSpineWire;
41
+ const scopeObjects = this.resolveFusionScope(context.getSceneObjects());
42
+ const scopeShapes = scopeObjects.flatMap(o => o.getShapes({}, 'solid'));
43
+ if (scopeShapes.length === 0) {
44
+ throw new Error("Rib requires target solids in the scene or via .scope()");
45
+ }
46
+ if (this._extend) {
47
+ spineWire = p.record('Extend spine', () => RibOps.extendSpineWire(spineWire, scopeShapes, plane));
48
+ }
49
+ // Sign convention:
50
+ // thickness > 0 → extrude OPPOSITE to the sketch normal (typical
51
+ // "into the cavity" direction when the sketch sits
52
+ // on top of a parent face).
53
+ // thickness < 0 → extrude IN the sketch normal direction.
54
+ // Same applies for parallel mode (the perpendicular-in-plane direction
55
+ // gets the same sign flip).
56
+ const dirSign = -Math.sign(this._thickness);
57
+ let direction;
58
+ let distance;
59
+ if (this._parallel) {
60
+ const perpDir = RibOps.computeSpinePerpendicularDirection(spineWire, plane);
61
+ direction = perpDir.multiply(dirSign);
62
+ distance = p.record('Compute extrude distance', () => RibOps.computeExtrudeDistanceAlongDirection(direction, plane.origin, scopeShapes));
63
+ }
64
+ else {
65
+ direction = plane.normal.multiply(dirSign);
66
+ distance = p.record('Compute extrude distance', () => RibOps.computeExtrudeDistance(plane, scopeShapes));
67
+ }
68
+ // Parallel + draft uses a tapered loft (firstFace lies on the spine
69
+ // plane by construction, exact thickness preserved). Everything else
70
+ // builds a plain prism from the rib profile face.
71
+ const useTaperedLoft = this._parallel && this.getDraft() !== null;
72
+ const profileFace = useTaperedLoft
73
+ ? null
74
+ : p.record('Make rib profile', () => this._parallel
75
+ ? RibOps.makeRibProfileParallel(spineWire, this._thickness, plane)
76
+ : RibOps.makeRibProfile(spineWire, this._thickness, plane));
77
+ const vec = direction.multiply(distance);
78
+ const { solid, firstFace, lastFace } = p.record('Extrude rib', () => {
79
+ if (useTaperedLoft) {
80
+ const draft = this.getDraft();
81
+ const angleRad = (draft[0] * Math.PI) / 180;
82
+ return RibOps.makeTaperedRibPrism(spineWire, this._thickness, plane, direction, distance, angleRad);
83
+ }
84
+ return ExtrudeOps.makePrismFromVec(profileFace, vec);
85
+ });
86
+ const ribSolid = solid;
87
+ const ribFirstFace = firstFace;
88
+ const ribLastFace = lastFace;
89
+ this.extrudable.removeShapes(this);
90
+ if (this._spine !== this.extrudable) {
91
+ this._spine.removeShapes(this);
92
+ }
93
+ const conformed = p.record('Conform rib', () => RibOps.conformRibToScope(ribSolid, scopeShapes, originalSpineWire, ribFirstFace, ribLastFace, direction));
94
+ let classified = {
95
+ startFaces: conformed.startFaces,
96
+ endFaces: conformed.endFaces,
97
+ sideFaces: conformed.sideFaces,
98
+ internalFaces: conformed.internalFaces,
99
+ capFaces: [],
100
+ };
101
+ let conformedSolids = conformed.solids;
102
+ // Draft is applied AFTER conformance so the prism walls are already
103
+ // bounded by the cavity. Drafting the over-extended pre-conform
104
+ // prism caused OCC to fail (walls would cross within the over-
105
+ // extension); the conformed rib is finite, so OCC handles strong
106
+ // drafts cleanly.
107
+ //
108
+ // Parallel mode skips this entirely — its prism was built tapered
109
+ // by `RibOps.makeTaperedRibPrism`, so the conformed result is
110
+ // already drafted with an exact (no-drift) start-face thickness.
111
+ // Normal mode uses OCC's draft with the sketch plane as the neutral
112
+ // plane (which already coincides with the prism base, so no shift
113
+ // is required).
114
+ if (!this._parallel && this.getDraft() && conformedSolids.length === 1 && classified.startFaces.length > 0) {
115
+ const draft = this.getDraft();
116
+ let angle = draft[0];
117
+ const draftPlane = plane;
118
+ // Mirrors the dirSign reversal above: in normal mode the OCC
119
+ // pull direction is the unsigned plane.normal, which now matches
120
+ // the extrude direction only when thickness < 0 (since extrude
121
+ // direction = plane.normal * -sign(thickness)). Negate the angle
122
+ // to flip OCC's outward bias to the user-facing "+ draft narrows
123
+ // the tip" convention.
124
+ if (this._thickness < 0) {
125
+ angle = -angle;
126
+ }
127
+ const rad = (deg) => deg * Math.PI / 180;
128
+ const draftResult = p.record('Apply draft', () => {
129
+ // Use the first start face as the OCC "firstFace" param (pivot
130
+ // anchor). When the conformance trimmed the original end face
131
+ // into the cavity (so endFaces is empty), the tip is already
132
+ // captured as an internal face — we pass startFaces[0] as the
133
+ // "lastFace" placeholder so its IsSame check is a no-op (it'll
134
+ // also match firstFace and so be excluded once). The actual tip
135
+ // surface stays excluded via the internalFaces argument.
136
+ const startRep = classified.startFaces[0];
137
+ const endRep = classified.endFaces[0] ?? startRep;
138
+ // Faces of the rib that sit flush with a scope face (the rib's
139
+ // mounting face — typically a cap that meets the cavity wall)
140
+ // must not be tilted. Drafting them either tears the rib away
141
+ // from the parent (negative draft) or makes OCC fail outright
142
+ // (positive draft) because there's no material outside the wall
143
+ // for the tilt to extend into.
144
+ const wallTouchingFaces = findScopeCoincidentFaces(classified.sideFaces, scopeShapes);
145
+ // The rib's "cap" faces (perpendicular to the spine direction)
146
+ // close the slab at the spine endpoints. Drafting them tilts
147
+ // them inward and shrinks the rib's length along the spine,
148
+ // which the user doesn't expect — only the long wall faces
149
+ // (perpendicular to spine direction) should taper.
150
+ const spineStart = originalSpineWire.getFirstVertex().toPoint();
151
+ const spineEnd = originalSpineWire.getLastVertex().toPoint();
152
+ const spineDir = spineStart.vectorTo(spineEnd).normalize();
153
+ const capFaces = findFacesAlignedWith(classified.sideFaces, spineDir);
154
+ const excludes = [
155
+ ...classified.startFaces.slice(1),
156
+ ...classified.endFaces.slice(classified.endFaces[0] === endRep ? 1 : 0),
157
+ ...classified.internalFaces,
158
+ ...wallTouchingFaces,
159
+ ...capFaces,
160
+ ];
161
+ return ExtrudeOps.applyDraftOnSideFaces(conformedSolids[0], startRep, endRep, draftPlane, rad(angle), excludes);
162
+ });
163
+ const remap = (faces) => {
164
+ const out = [];
165
+ for (const f of faces) {
166
+ for (const r of draftResult.remapFace(f)) {
167
+ out.push(r);
168
+ }
169
+ }
170
+ return out;
171
+ };
172
+ classified = {
173
+ startFaces: remap(classified.startFaces),
174
+ endFaces: remap(classified.endFaces),
175
+ sideFaces: remap(classified.sideFaces),
176
+ internalFaces: remap(classified.internalFaces),
177
+ capFaces: [],
178
+ };
179
+ conformedSolids = [draftResult.solid];
180
+ }
181
+ if (this._operationMode === 'new') {
182
+ this.setState('start-faces', classified.startFaces);
183
+ this.setState('end-faces', classified.endFaces);
184
+ this.setState('side-faces', classified.sideFaces);
185
+ this.setState('internal-faces', classified.internalFaces);
186
+ this.setState('cap-faces', classified.capFaces);
187
+ this.addShapes(conformedSolids);
188
+ this.recordShapeFacesAndEdgesAsAdditions(conformedSolids);
189
+ this.classifyExtrudeEdges();
190
+ return;
191
+ }
192
+ this.finalizeAndFuse(conformedSolids, classified, context);
193
+ }
194
+ getSpineWire(pathObj) {
195
+ const shapes = pathObj.getShapes({ excludeMeta: false });
196
+ const edges = shapes.flatMap(s => s.getSubShapes('edge'));
197
+ return WireOps.makeWireFromEdges(edges);
198
+ }
199
+ getDependencies() {
200
+ const deps = [];
201
+ if (this.extrudable) {
202
+ deps.push(this.extrudable);
203
+ }
204
+ if (this._spine) {
205
+ deps.push(this._spine);
206
+ }
207
+ return deps;
208
+ }
209
+ createCopy(remap) {
210
+ const extrudable = this.extrudable
211
+ ? (remap.get(this.extrudable) || this.extrudable)
212
+ : undefined;
213
+ const spine = remap.get(this._spine) || this._spine;
214
+ const copy = new Rib(this._thickness, spine, extrudable).syncWith(this);
215
+ copy._parallel = this._parallel;
216
+ copy._extend = this._extend;
217
+ return copy;
218
+ }
219
+ compareTo(other) {
220
+ if (!(other instanceof Rib)) {
221
+ return false;
222
+ }
223
+ if (!super.compareTo(other)) {
224
+ return false;
225
+ }
226
+ if (this._thickness !== other._thickness) {
227
+ return false;
228
+ }
229
+ if (!this._spine.compareTo(other._spine)) {
230
+ return false;
231
+ }
232
+ if (this.extrudable && other.extrudable && !this.extrudable.compareTo(other.extrudable)) {
233
+ return false;
234
+ }
235
+ if (this._parallel !== other._parallel) {
236
+ return false;
237
+ }
238
+ if (this._extend !== other._extend) {
239
+ return false;
240
+ }
241
+ return true;
242
+ }
243
+ getType() {
244
+ return "rib";
245
+ }
246
+ serialize() {
247
+ return {
248
+ thickness: this._thickness,
249
+ spine: this._spine.serialize(),
250
+ extrudable: this.extrudable?.serialize(),
251
+ operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
252
+ parallel: this._parallel || undefined,
253
+ extend: this._extend || undefined,
254
+ draft: this._draft,
255
+ };
256
+ }
257
+ }
258
+ // Faces of the rib whose surface sits flush with a face of any scope
259
+ // shape (planar coincidence). These are the rib's mounting faces — they
260
+ // must not be tilted by draft; tilting them either tears the rib away
261
+ // from the parent (negative draft) or makes OCC's draft fail (positive
262
+ // draft), since there's no material outside the parent wall for the
263
+ // tilt to extend into.
264
+ // Rib faces whose surface normal is parallel to `direction` — for the
265
+ // rib's draft step this picks out the "cap" faces at the spine endpoints
266
+ // (perpendicular to the spine direction). They cap the slab length and
267
+ // shouldn't tilt under draft; only the wall faces, whose normals are
268
+ // perpendicular to spineDir, take the taper.
269
+ function findFacesAlignedWith(faces, direction) {
270
+ const out = [];
271
+ for (const f of faces) {
272
+ if (FaceQuery.getSurfaceType(f) !== "plane") {
273
+ continue;
274
+ }
275
+ const pl = FaceQuery.getSurfacePlane(f);
276
+ if (1 - Math.abs(pl.normal.dot(direction)) < 1e-4) {
277
+ out.push(f);
278
+ }
279
+ }
280
+ return out;
281
+ }
282
+ function findScopeCoincidentFaces(ribSideFaces, scopeShapes) {
283
+ const out = [];
284
+ const scopePlanarFaces = [];
285
+ for (const scope of scopeShapes) {
286
+ const rawFaces = Explorer.findShapes(scope.getShape(), Explorer.getOcShapeType("face"));
287
+ for (const rf of rawFaces) {
288
+ const wrapped = Face.fromTopoDSFace(Explorer.toFace(rf));
289
+ if (FaceQuery.getSurfaceType(wrapped) !== "plane") {
290
+ continue;
291
+ }
292
+ const pl = FaceQuery.getSurfacePlane(wrapped);
293
+ scopePlanarFaces.push({ face: wrapped, origin: pl.origin, normal: pl.normal });
294
+ }
295
+ }
296
+ const tol = 1e-4;
297
+ for (const rf of ribSideFaces) {
298
+ if (FaceQuery.getSurfaceType(rf) !== "plane") {
299
+ continue;
300
+ }
301
+ const rPl = FaceQuery.getSurfacePlane(rf);
302
+ for (const { origin: sOrigin, normal: sNormal } of scopePlanarFaces) {
303
+ // Parallel test: |normal · normal'| ≈ 1
304
+ if (1 - Math.abs(rPl.normal.dot(sNormal)) > 1e-6) {
305
+ continue;
306
+ }
307
+ // Coincidence test: perpendicular distance from one plane's origin
308
+ // to the other plane is below tolerance. (FaceQuery.getSignedPlaneDistance
309
+ // routes through gp_Pln.Distance, which returns 0 for parallel-but-
310
+ // separated planes — useless for coincidence — so we compute it
311
+ // directly here.)
312
+ const offset = sOrigin.vectorTo(rPl.origin);
313
+ const d = Math.abs(offset.dot(sNormal));
314
+ if (d <= tol) {
315
+ out.push(rf);
316
+ break;
317
+ }
318
+ }
319
+ }
320
+ return out;
321
+ }
@@ -11,6 +11,7 @@ export declare class SelectSceneObject extends SceneObject implements ISelect {
11
11
  private shapes;
12
12
  constructor(filters: FilterBuilderBase<Shape>[], constraintObject?: SceneObject);
13
13
  build(context: BuildSceneObjectContext): void;
14
+ private injectFromMembershipSets;
14
15
  private collectFromSceneObjects;
15
16
  private getAllShapes;
16
17
  getDependencies(): SceneObject[];
@@ -3,6 +3,7 @@ import { ShapeFilter } from "../filters/filter.js";
3
3
  import { SceneObject } from "../common/scene-object.js";
4
4
  import { BelongsToFaceFilter, NotBelongsToFaceFilter } from "../filters/edge/belongs-to-face.js";
5
5
  import { FromSceneObjectFilter } from "../filters/from-object.js";
6
+ import { TopologyIndex } from "../oc/topology-index.js";
6
7
  export class SelectSceneObject extends SceneObject {
7
8
  filters;
8
9
  constraintObject;
@@ -50,8 +51,39 @@ export class SelectSceneObject extends SceneObject {
50
51
  if (this.type === "edge") {
51
52
  this.injectScopeFaces(filters, sceneObjects);
52
53
  }
53
- const filteredShapes = this.applyFilters(allShapes, filters);
54
- this.addShapes(filteredShapes);
54
+ const fromFilters = this.injectFromMembershipSets(filters);
55
+ try {
56
+ const filteredShapes = this.applyFilters(allShapes, filters);
57
+ this.addShapes(filteredShapes);
58
+ }
59
+ finally {
60
+ for (const { filter, set } of fromFilters) {
61
+ filter.setMembershipSet(null);
62
+ set.delete();
63
+ }
64
+ }
65
+ }
66
+ injectFromMembershipSets(filters) {
67
+ const allocated = [];
68
+ for (const builder of filters) {
69
+ for (const filter of builder.getFilters()) {
70
+ if (filter instanceof FromSceneObjectFilter) {
71
+ const shapeType = filter.getShapeType();
72
+ const rawShapes = [];
73
+ for (const obj of filter.getSceneObjects()) {
74
+ for (const owner of obj.getShapes()) {
75
+ for (const sub of owner.getSubShapes(shapeType)) {
76
+ rawShapes.push(sub.getShape());
77
+ }
78
+ }
79
+ }
80
+ const set = TopologyIndex.buildShapeSet(rawShapes);
81
+ filter.setMembershipSet(set);
82
+ allocated.push({ filter, set });
83
+ }
84
+ }
85
+ }
86
+ return allocated;
55
87
  }
56
88
  collectFromSceneObjects(filters) {
57
89
  const objects = [];
@@ -70,8 +102,17 @@ export class SelectSceneObject extends SceneObject {
70
102
  }
71
103
  getAllShapes(scope, exludedShapes) {
72
104
  const scopeShapes = scope.flatMap(obj => obj.getShapes({}, 'solid').map(s => s.getSubShapes(this.type)).flat());
73
- exludedShapes = exludedShapes.flatMap(s => s.getSubShapes(this.type));
74
- return scopeShapes.filter(shape => !exludedShapes.some(exShape => exShape.isSame(shape)));
105
+ const flatExcluded = exludedShapes.flatMap(s => s.getSubShapes(this.type));
106
+ if (flatExcluded.length === 0) {
107
+ return scopeShapes;
108
+ }
109
+ const excludedSet = TopologyIndex.buildShapeSet(flatExcluded.map(s => s.getShape()));
110
+ try {
111
+ return scopeShapes.filter(shape => !excludedSet.Contains(shape.getShape()));
112
+ }
113
+ finally {
114
+ excludedSet.delete();
115
+ }
75
116
  }
76
117
  getDependencies() {
77
118
  const deps = [];
@@ -97,16 +138,37 @@ export class SelectSceneObject extends SceneObject {
97
138
  return new SelectSceneObject(mirroredFilters, this.constraintObject);
98
139
  }
99
140
  injectScopeFaces(filters, sceneObjects) {
100
- let scopeFaces = null;
141
+ let scopeSolids = null;
142
+ let extraFaces = null;
143
+ let faceByHash = null;
101
144
  for (const builder of filters) {
102
145
  for (const filter of builder.getFilters()) {
103
146
  if (filter instanceof BelongsToFaceFilter || filter instanceof NotBelongsToFaceFilter) {
104
- if (!scopeFaces) {
105
- scopeFaces = this.constraintObject
106
- ? this.constraintObject.getShapes().flatMap(s => s.getSubShapes("face"))
107
- : sceneObjects.flatMap(obj => obj.getShapes({}, 'solid').flatMap(s => s.getSubShapes("face")));
147
+ if (!scopeSolids) {
148
+ if (this.constraintObject) {
149
+ const constraintShapes = this.constraintObject.getShapes();
150
+ scopeSolids = constraintShapes.filter(s => s.isSolid());
151
+ // Faces directly in the constraint (not part of a solid) need the
152
+ // legacy linear-scan path since they don't have a cached index.
153
+ extraFaces = constraintShapes
154
+ .filter(s => !s.isSolid())
155
+ .flatMap(s => s.getSubShapes("face"));
156
+ }
157
+ else {
158
+ scopeSolids = sceneObjects.flatMap(obj => obj.getShapes({}, 'solid'));
159
+ extraFaces = [];
160
+ }
161
+ faceByHash = new Map();
162
+ for (const solid of scopeSolids) {
163
+ for (const face of solid.getFaces()) {
164
+ addToBucket(faceByHash, face);
165
+ }
166
+ }
167
+ for (const face of extraFaces) {
168
+ addToBucket(faceByHash, face);
169
+ }
108
170
  }
109
- filter.setScopeFaces(scopeFaces);
171
+ filter.setScopeIndex(scopeSolids, extraFaces, faceByHash);
110
172
  }
111
173
  }
112
174
  }
@@ -158,3 +220,12 @@ export class SelectSceneObject extends SceneObject {
158
220
  };
159
221
  }
160
222
  }
223
+ function addToBucket(faceByHash, face) {
224
+ const hash = face.getShape().HashCode(2147483647);
225
+ let bucket = faceByHash.get(hash);
226
+ if (!bucket) {
227
+ bucket = [];
228
+ faceByHash.set(hash, bucket);
229
+ }
230
+ bucket.push(face);
231
+ }
@@ -2,12 +2,14 @@ import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js"
2
2
  import { SelectSceneObject } from "./select.js";
3
3
  import { FaceFilterBuilder } from "../filters/face/face-filter.js";
4
4
  import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
5
- import { IShell } from "../core/interfaces.js";
5
+ import { IShell, ShellJoinType } from "../core/interfaces.js";
6
6
  export declare class Shell extends SceneObject implements IShell {
7
7
  private thickness;
8
8
  private _faceSelections;
9
+ private _joinType;
9
10
  constructor(thickness: number, faceSelections?: SelectSceneObject[]);
10
11
  get faceSelections(): SelectSceneObject[];
12
+ join(type: ShellJoinType): this;
11
13
  validate(): void;
12
14
  build(context: BuildSceneObjectContext): void;
13
15
  internalFaces(...args: (number | FaceFilterBuilder)[]): SceneObject;
@@ -21,5 +23,6 @@ export declare class Shell extends SceneObject implements IShell {
21
23
  getType(): string;
22
24
  serialize(): {
23
25
  thickness: number;
26
+ joinType: "intersection" | "tangent";
24
27
  };
25
28
  }
@@ -11,6 +11,7 @@ import { requireShapes } from "../common/operand-check.js";
11
11
  export class Shell extends SceneObject {
12
12
  thickness;
13
13
  _faceSelections = [];
14
+ _joinType = 'arc';
14
15
  constructor(thickness, faceSelections) {
15
16
  super();
16
17
  this.thickness = thickness;
@@ -19,6 +20,10 @@ export class Shell extends SceneObject {
19
20
  get faceSelections() {
20
21
  return this._faceSelections;
21
22
  }
23
+ join(type) {
24
+ this._joinType = type;
25
+ return this;
26
+ }
22
27
  validate() {
23
28
  for (let i = 0; i < this._faceSelections.length; i++) {
24
29
  requireShapes(this._faceSelections[i], `face selection ${i + 1}`, "shell");
@@ -52,7 +57,7 @@ export class Shell extends SceneObject {
52
57
  continue;
53
58
  }
54
59
  try {
55
- const newShape = ShellOps.makeThickSolid(shape, targetFaces, this.thickness);
60
+ const newShape = ShellOps.makeThickSolid(shape, targetFaces, this.thickness, this._joinType);
56
61
  newShapes.push(newShape);
57
62
  const originalObj = shapeObjMap.get(shape);
58
63
  originalObj.removeShape(shape, this);
@@ -192,6 +197,9 @@ export class Shell extends SceneObject {
192
197
  if (this.thickness !== other.thickness) {
193
198
  return false;
194
199
  }
200
+ if (this._joinType !== other._joinType) {
201
+ return false;
202
+ }
195
203
  if (this._faceSelections.length !== other._faceSelections.length) {
196
204
  return false;
197
205
  }
@@ -207,14 +215,17 @@ export class Shell extends SceneObject {
207
215
  }
208
216
  createCopy(remap) {
209
217
  const faceSelections = this._faceSelections.map(sel => (remap.get(sel) || sel));
210
- return new Shell(this.thickness, faceSelections.length > 0 ? faceSelections : undefined);
218
+ const copy = new Shell(this.thickness, faceSelections.length > 0 ? faceSelections : undefined);
219
+ copy._joinType = this._joinType;
220
+ return copy;
211
221
  }
212
222
  getType() {
213
223
  return 'shell';
214
224
  }
215
225
  serialize() {
216
226
  return {
217
- thickness: this.thickness
227
+ thickness: this.thickness,
228
+ joinType: this._joinType !== 'arc' ? this._joinType : undefined
218
229
  };
219
230
  }
220
231
  }
@@ -1,22 +1,25 @@
1
1
  import { Matrix4 } from "../../math/matrix4.js";
2
2
  import { Edge, Face } from "../../common/shapes.js";
3
+ import { Solid } from "../../common/solid.js";
3
4
  import { FilterBase } from "../filter-base.js";
4
5
  import { FilterBuilderBase } from "../filter-builder-base.js";
5
- export declare class BelongsToFaceFilter extends FilterBase<Edge> {
6
- private faceFilterBuilders;
7
- private scopeFaces;
6
+ declare abstract class BelongsToFaceFilterBase extends FilterBase<Edge> {
7
+ protected faceFilterBuilders: FilterBuilderBase<Face>[];
8
+ protected scopeSolids: Solid[];
9
+ protected scopeFaces: Face[];
10
+ protected faceByHash: Map<number, Face[]>;
8
11
  constructor(faceFilterBuilders: FilterBuilderBase<Face>[]);
9
- setScopeFaces(faces: Face[]): void;
12
+ setScopeIndex(solids: Solid[], extraFaces: Face[], faceByHash: Map<number, Face[]>): void;
13
+ protected findContainingFaces(edge: Edge): Face[];
14
+ }
15
+ export declare class BelongsToFaceFilter extends BelongsToFaceFilterBase {
10
16
  match(shape: Edge): boolean;
11
17
  compareTo(other: BelongsToFaceFilter): boolean;
12
18
  transform(matrix: Matrix4): BelongsToFaceFilter;
13
19
  }
14
- export declare class NotBelongsToFaceFilter extends FilterBase<Edge> {
15
- private faceFilterBuilders;
16
- private scopeFaces;
17
- constructor(faceFilterBuilders: FilterBuilderBase<Face>[]);
18
- setScopeFaces(faces: Face[]): void;
20
+ export declare class NotBelongsToFaceFilter extends BelongsToFaceFilterBase {
19
21
  match(shape: Edge): boolean;
20
22
  compareTo(other: NotBelongsToFaceFilter): boolean;
21
23
  transform(matrix: Matrix4): NotBelongsToFaceFilter;
22
24
  }
25
+ export {};