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
@@ -139,6 +139,9 @@ export class ExtrudeToFace extends ExtrudeBase {
139
139
  if (surfaceType === 'cylinder') {
140
140
  splitTargetFace = this.resizeCylindricalFace(targetFace);
141
141
  }
142
+ else if (surfaceType === 'cone') {
143
+ splitTargetFace = this.resizeConicalFace(targetFace);
144
+ }
142
145
  else {
143
146
  splitTargetFace = targetFace;
144
147
  }
@@ -157,6 +160,9 @@ export class ExtrudeToFace extends ExtrudeBase {
157
160
  resizeCylindricalFace(targetFace) {
158
161
  return FaceQuery.makeInfiniteCylindricalFace(targetFace, this.getEndOffset());
159
162
  }
163
+ resizeConicalFace(targetFace) {
164
+ return FaceQuery.makeInfiniteConicalFace(targetFace, this.getEndOffset());
165
+ }
160
166
  /**
161
167
  * Computes the signed distance from the sketch plane to the farthest
162
168
  * bounding-box corner of the target face, measured along the sketch
@@ -8,7 +8,7 @@ export declare class Fillet extends SceneObject {
8
8
  validate(): void;
9
9
  build(context: BuildSceneObjectContext): void;
10
10
  doBuild(sceneObjectsMap: Map<SceneObject, Shape[]>, selections: SceneObject[]): {
11
- addedShapes: Shape<import("occjs-wrapper").TopoDS_Shape>[];
11
+ addedShapes: Shape<import("ocjs-fluidcad").TopoDS_Shape>[];
12
12
  removedShapes: {
13
13
  shape: Shape;
14
14
  owner: SceneObject;
@@ -0,0 +1,41 @@
1
+ import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
2
+ import { AxisObjectBase } from "./axis-renderable-base.js";
3
+ import { IHelix } from "../core/interfaces.js";
4
+ export declare class Helix extends SceneObject implements IHelix {
5
+ source: AxisObjectBase | SceneObject;
6
+ private _pitch?;
7
+ private _turns?;
8
+ private _startOffset;
9
+ private _endOffset;
10
+ private _height?;
11
+ private _radius?;
12
+ private _endRadius?;
13
+ constructor(source: AxisObjectBase | SceneObject);
14
+ pitch(value: number): this;
15
+ turns(value: number): this;
16
+ startOffset(value: number): this;
17
+ endOffset(value: number): this;
18
+ height(value: number): this;
19
+ radius(value: number): this;
20
+ endRadius(value: number): this;
21
+ validate(): void;
22
+ build(_context?: BuildSceneObjectContext): void;
23
+ private deriveTurnsFromHeight;
24
+ private resolveSource;
25
+ private resolveFace;
26
+ private resolveEdge;
27
+ getType(): string;
28
+ getDependencies(): SceneObject[];
29
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
30
+ compareTo(other: SceneObject): boolean;
31
+ serialize(): {
32
+ source: any;
33
+ pitch: number;
34
+ turns: number;
35
+ startOffset: number;
36
+ endOffset: number;
37
+ height: number;
38
+ radius: number;
39
+ endRadius: number;
40
+ };
41
+ }
@@ -0,0 +1,337 @@
1
+ import { SceneObject } from "../common/scene-object.js";
2
+ import { BuildError } from "../common/build-error.js";
3
+ import { requireShapes } from "../common/operand-check.js";
4
+ import { AxisObjectBase } from "./axis-renderable-base.js";
5
+ import { HelixOps } from "../oc/helix-ops.js";
6
+ import { FaceQuery } from "../oc/face-query.js";
7
+ import { EdgeQuery } from "../oc/edge-query.js";
8
+ import { EdgeOps } from "../oc/edge-ops.js";
9
+ import { Convert } from "../oc/convert.js";
10
+ import { CoordinateSystem } from "../math/coordinate-system.js";
11
+ import { Vector3d } from "../math/vector3d.js";
12
+ import { Axis } from "../math/axis.js";
13
+ const DEFAULT_RADIUS = 20;
14
+ const DEFAULT_HEIGHT = 50;
15
+ const DEFAULT_TURNS = 1;
16
+ const EPS = 1e-7;
17
+ const TANGENCY_BREAK_EPSILON = 1e-6;
18
+ export class Helix extends SceneObject {
19
+ source;
20
+ _pitch;
21
+ _turns;
22
+ _startOffset = 0;
23
+ _endOffset = 0;
24
+ _height;
25
+ _radius;
26
+ _endRadius;
27
+ constructor(source) {
28
+ super();
29
+ this.source = source;
30
+ }
31
+ pitch(value) {
32
+ this._pitch = value;
33
+ return this;
34
+ }
35
+ turns(value) {
36
+ this._turns = value;
37
+ return this;
38
+ }
39
+ startOffset(value) {
40
+ this._startOffset = value;
41
+ return this;
42
+ }
43
+ endOffset(value) {
44
+ this._endOffset = value;
45
+ return this;
46
+ }
47
+ height(value) {
48
+ this._height = value;
49
+ return this;
50
+ }
51
+ radius(value) {
52
+ this._radius = value;
53
+ return this;
54
+ }
55
+ endRadius(value) {
56
+ this._endRadius = value;
57
+ return this;
58
+ }
59
+ validate() {
60
+ if (this.source instanceof AxisObjectBase) {
61
+ return;
62
+ }
63
+ requireShapes(this.source, "source", "helix");
64
+ }
65
+ build(_context) {
66
+ const resolved = this.resolveSource();
67
+ let startRadius;
68
+ let endRadius;
69
+ let cs;
70
+ let zStart;
71
+ let zEnd;
72
+ let offsetsAlreadyApplied = false;
73
+ switch (resolved.kind) {
74
+ case 'axis': {
75
+ cs = HelixOps_csFromAxis(resolved.axis);
76
+ startRadius = this._radius ?? DEFAULT_RADIUS;
77
+ endRadius = this._endRadius ?? startRadius;
78
+ const { height } = resolveAxisHeightAndPitch(this._height, this._pitch, this._turns);
79
+ zStart = 0;
80
+ zEnd = height;
81
+ break;
82
+ }
83
+ case 'cylinder-face': {
84
+ if (this._endRadius !== undefined && this._endRadius !== (this._radius ?? resolved.radius)) {
85
+ console.warn("helix: .endRadius() is ignored when source is a cylindrical face — for a tapered helix, use a conical face or axis input.");
86
+ }
87
+ cs = resolved.cs;
88
+ // Nudge inward by TANGENCY_BREAK_EPSILON when falling back to the
89
+ // face's natural radius. A helix exactly on the cylinder's surface
90
+ // produces a swept tube that's tangent to the cylinder along helical
91
+ // curves, and OCC's BOPAlgo (BRepAlgoAPI_Fuse/Cut) silently fails on
92
+ // tangent contact along curves — fuse returns the inputs as a
93
+ // compound, cut is a no-op. The 1e-6mm nudge is sub-nanometer
94
+ // (visually identical) but produces transversal intersections that
95
+ // BOPAlgo handles cleanly. Sweep also passes skipSimplify=true to
96
+ // avoid SimplifyResult/UnifySameDomain hanging on the resulting
97
+ // tangent-curve topology.
98
+ startRadius = this._radius ?? (resolved.radius - TANGENCY_BREAK_EPSILON);
99
+ endRadius = startRadius;
100
+ if (this._height !== undefined) {
101
+ zStart = 0;
102
+ zEnd = this._height;
103
+ }
104
+ else {
105
+ zStart = resolved.vMin;
106
+ zEnd = resolved.vMax;
107
+ }
108
+ break;
109
+ }
110
+ case 'cone-face': {
111
+ if (this._radius !== undefined || this._endRadius !== undefined) {
112
+ console.warn("helix: .radius()/.endRadius() are ignored when source is a conical face — radii are derived from the face geometry.");
113
+ }
114
+ cs = resolved.cs;
115
+ const cosA = Math.cos(resolved.semiAngle);
116
+ const sinA = Math.sin(resolved.semiAngle);
117
+ const zMinFace = resolved.vMin * cosA;
118
+ const zMaxFace = resolved.vMax * cosA;
119
+ const zLow = Math.min(zMinFace, zMaxFace);
120
+ const zHigh = Math.max(zMinFace, zMaxFace);
121
+ if (this._height !== undefined) {
122
+ zStart = zLow;
123
+ zEnd = zLow + this._height;
124
+ }
125
+ else {
126
+ zStart = zLow;
127
+ zEnd = zHigh;
128
+ }
129
+ // Apply offsets here so the radii follow the cone's surface at the
130
+ // extended positions (offsets extend along the cone's natural taper,
131
+ // not as a cylindrical extension).
132
+ zStart += this._startOffset;
133
+ zEnd += this._endOffset;
134
+ startRadius = resolved.refRadius + (zStart / cosA) * sinA;
135
+ endRadius = resolved.refRadius + (zEnd / cosA) * sinA;
136
+ offsetsAlreadyApplied = true;
137
+ break;
138
+ }
139
+ case 'line-edge': {
140
+ cs = HelixOps_csFromAxis(resolved.axis);
141
+ startRadius = this._radius ?? DEFAULT_RADIUS;
142
+ endRadius = this._endRadius ?? startRadius;
143
+ zStart = 0;
144
+ zEnd = this._height ?? resolved.length;
145
+ break;
146
+ }
147
+ case 'circle-edge': {
148
+ if (this._endRadius !== undefined) {
149
+ console.warn("helix: .endRadius() is ignored when source is a circular edge — both radii equal the circle radius.");
150
+ }
151
+ cs = resolved.cs;
152
+ startRadius = this._radius ?? resolved.radius;
153
+ endRadius = startRadius;
154
+ zStart = 0;
155
+ zEnd = this._height ?? DEFAULT_HEIGHT;
156
+ break;
157
+ }
158
+ }
159
+ if (!offsetsAlreadyApplied) {
160
+ zStart += this._startOffset;
161
+ zEnd += this._endOffset;
162
+ }
163
+ if (this._pitch !== undefined && Math.abs(this._pitch) < EPS) {
164
+ throw new BuildError(`helix: .pitch() must be non-zero.`);
165
+ }
166
+ if (this._turns !== undefined && this._turns <= 0) {
167
+ throw new BuildError(`helix: .turns() must be > 0, got ${this._turns}.`, `Pass a positive number to .turns().`);
168
+ }
169
+ const turns = this._turns ?? this.deriveTurnsFromHeight(zEnd - zStart);
170
+ if (!Number.isFinite(turns) || turns <= 0) {
171
+ throw new BuildError(`helix: turns must be > 0, got ${turns}.`, `Pass a positive number to .turns() or .pitch().`);
172
+ }
173
+ if (Math.abs(zEnd - zStart) < EPS) {
174
+ throw new BuildError(`helix: resulting axial height is zero (zStart=${zStart}, zEnd=${zEnd}).`, `Check .startOffset()/.endOffset()/.height() values.`);
175
+ }
176
+ if (startRadius <= 0) {
177
+ throw new BuildError(`helix: start radius must be > 0, got ${startRadius}.`);
178
+ }
179
+ if (endRadius <= 0) {
180
+ throw new BuildError(`helix: end radius would be ≤ 0 (got ${endRadius}). For a conical helix, the end radius must stay positive.`, `Reduce .endOffset() or use a smaller turns/height combination.`);
181
+ }
182
+ const edge = HelixOps.makeHelix(cs, startRadius, endRadius, zStart, zEnd, turns);
183
+ this.addShape(edge);
184
+ this.source.removeShapes(this);
185
+ }
186
+ deriveTurnsFromHeight(height) {
187
+ if (this._pitch === undefined) {
188
+ return DEFAULT_TURNS;
189
+ }
190
+ if (Math.abs(this._pitch) < EPS) {
191
+ throw new BuildError(`helix: .pitch() must be non-zero.`);
192
+ }
193
+ return Math.abs(height / this._pitch);
194
+ }
195
+ resolveSource() {
196
+ if (this.source instanceof AxisObjectBase) {
197
+ return { kind: 'axis', axis: this.source.getAxis() };
198
+ }
199
+ const shapes = this.source.getShapes({ excludeGuide: false });
200
+ if (shapes.length !== 1) {
201
+ throw new BuildError(`helix: source must contain exactly one shape (got ${shapes.length}).`, `Wrap multi-shape sources in select(...) to pick a single face or edge.`);
202
+ }
203
+ const shape = shapes[0];
204
+ if (shape.isFace()) {
205
+ return this.resolveFace(shape);
206
+ }
207
+ if (shape.isEdge()) {
208
+ return this.resolveEdge(shape);
209
+ }
210
+ throw new BuildError(`helix: source shape must be a face or edge, got '${shape.getType()}'.`);
211
+ }
212
+ resolveFace(face) {
213
+ const surfaceType = FaceQuery.getSurfaceType(face);
214
+ if (surfaceType === 'cylinder') {
215
+ const cylinder = FaceQuery.getSurfaceAdaptorCylinderRaw(face.getShape());
216
+ const ax3 = cylinder.Position();
217
+ const cs = Convert.toCoordinateSystemFromGpAx3(ax3, true);
218
+ const radius = cylinder.Radius();
219
+ cylinder.delete();
220
+ const { vMin, vMax } = FaceQuery.getSurfaceVBoundsRaw(face.getShape());
221
+ const canon = canonicalizeAxialBounds(cs, vMin, vMax);
222
+ return { kind: 'cylinder-face', cs: canon.cs, radius, vMin: canon.vMin, vMax: canon.vMax };
223
+ }
224
+ if (surfaceType === 'cone') {
225
+ const cone = FaceQuery.getSurfaceAdaptorConeRaw(face.getShape());
226
+ const ax3 = cone.Position();
227
+ const cs = Convert.toCoordinateSystemFromGpAx3(ax3, true);
228
+ const semiAngle = cone.SemiAngle();
229
+ const refRadius = cone.RefRadius();
230
+ cone.delete();
231
+ const { vMin, vMax } = FaceQuery.getSurfaceVBoundsRaw(face.getShape());
232
+ // For a cone, the V-axis lies along the slant; canonicalizing the
233
+ // CS direction here doesn't simplify the math the same way it does for
234
+ // a cylinder, so leave the cone's frame alone.
235
+ return { kind: 'cone-face', cs, semiAngle, refRadius, vMin, vMax };
236
+ }
237
+ throw new BuildError(`helix: face must be cylindrical or conical (got '${surfaceType}').`);
238
+ }
239
+ resolveEdge(edge) {
240
+ const curveType = EdgeQuery.getEdgeCurveType(edge);
241
+ if (curveType === 'line') {
242
+ const axis = EdgeOps.edgeToAxis(edge);
243
+ const params = EdgeQuery.getEdgeCurveParams(edge);
244
+ const length = Math.abs(params.last - params.first);
245
+ return { kind: 'line-edge', axis, length };
246
+ }
247
+ if (curveType === 'circle') {
248
+ const data = EdgeQuery.getCircleDataFromEdge(edge);
249
+ const axis = new Axis(data.center, data.axisDirection);
250
+ const cs = HelixOps_csFromAxis(axis);
251
+ return { kind: 'circle-edge', cs, radius: data.radius };
252
+ }
253
+ throw new BuildError(`helix: edge must be a line or circle (got '${curveType}').`);
254
+ }
255
+ getType() {
256
+ return 'helix';
257
+ }
258
+ getDependencies() {
259
+ return [this.source];
260
+ }
261
+ createCopy(remap) {
262
+ const newSource = (remap.get(this.source) ?? this.source);
263
+ const copy = new Helix(newSource);
264
+ copy._pitch = this._pitch;
265
+ copy._turns = this._turns;
266
+ copy._startOffset = this._startOffset;
267
+ copy._endOffset = this._endOffset;
268
+ copy._height = this._height;
269
+ copy._radius = this._radius;
270
+ copy._endRadius = this._endRadius;
271
+ return copy;
272
+ }
273
+ compareTo(other) {
274
+ if (!(other instanceof Helix)) {
275
+ return false;
276
+ }
277
+ if (!super.compareTo(other)) {
278
+ return false;
279
+ }
280
+ if (!this.source.compareTo(other.source)) {
281
+ return false;
282
+ }
283
+ return this._pitch === other._pitch
284
+ && this._turns === other._turns
285
+ && this._startOffset === other._startOffset
286
+ && this._endOffset === other._endOffset
287
+ && this._height === other._height
288
+ && this._radius === other._radius
289
+ && this._endRadius === other._endRadius;
290
+ }
291
+ serialize() {
292
+ return {
293
+ source: this.source.serialize(),
294
+ pitch: this._pitch,
295
+ turns: this._turns,
296
+ startOffset: this._startOffset,
297
+ endOffset: this._endOffset,
298
+ height: this._height,
299
+ radius: this._radius,
300
+ endRadius: this._endRadius,
301
+ };
302
+ }
303
+ }
304
+ function resolveAxisHeightAndPitch(height, pitch, turns) {
305
+ if (height !== undefined) {
306
+ return { height };
307
+ }
308
+ if (pitch !== undefined && turns !== undefined) {
309
+ return { height: Math.abs(pitch * turns) };
310
+ }
311
+ if (pitch !== undefined) {
312
+ return { height: Math.abs(pitch * DEFAULT_TURNS) };
313
+ }
314
+ return { height: DEFAULT_HEIGHT };
315
+ }
316
+ function HelixOps_csFromAxis(axis) {
317
+ const dir = axis.direction.normalize();
318
+ const seed = Math.abs(dir.z) < 0.9 ? Vector3d.unitZ() : Vector3d.unitX();
319
+ const xDir = seed.cross(dir).normalize();
320
+ return new CoordinateSystem(axis.origin, dir, xDir);
321
+ }
322
+ /**
323
+ * A cylindrical face's `Position()` Ax3 may have its main direction pointing
324
+ * "into" the face's V-extent rather than "out of it" (e.g. an extruded cylinder
325
+ * built from a sketch on z=0 yields mainDir = (0,0,-1) with V-bounds [-50, 0]).
326
+ * Pass through unchanged when V naturally extends in the +mainDir direction;
327
+ * otherwise flip mainDir and negate V-bounds so that V grows along the body's
328
+ * axial extent. This keeps `.startOffset()`/`.endOffset()` semantics intuitive
329
+ * (positive end-offset extends past the cylinder's "top").
330
+ */
331
+ function canonicalizeAxialBounds(cs, vMin, vMax) {
332
+ if (Math.abs(vMin) <= Math.abs(vMax)) {
333
+ return { cs, vMin, vMax };
334
+ }
335
+ const flipped = new CoordinateSystem(cs.origin, cs.mainDirection.negate(), cs.xDirection);
336
+ return { cs: flipped, vMin: -vMax, vMax: -vMin };
337
+ }
@@ -4,6 +4,7 @@ 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
6
  import { TopologyIndex } from "../oc/topology-index.js";
7
+ import { ShapeHasher } from "../oc/shape-hash.js";
7
8
  export class SelectSceneObject extends SceneObject {
8
9
  filters;
9
10
  constraintObject;
@@ -26,12 +27,20 @@ export class SelectSceneObject extends SceneObject {
26
27
  let filters = this.filters;
27
28
  let sceneObjects = context.getSceneObjects();
28
29
  let excludedObjects = [];
30
+ let narrowedToCloneGroup = false;
29
31
  if (transform) {
30
32
  filters = filters.map(f => f.transform(transform));
31
33
  if (!this.constraintObject && parent) {
32
34
  const snapshot = parent.getSnapshot();
33
35
  excludedObjects = snapshot ? Array.from(snapshot.values()).flat() : [];
34
- sceneObjects = context.getSceneObjectsFromTo(parent, this);
36
+ // Restrict to this clone instance's own siblings. Other instances of
37
+ // the same repeat share the parent container but carry a different
38
+ // clone transform, and the container itself would re-expose their
39
+ // shapes through getChildShapes.
40
+ const transformRef = this.getTransformRef();
41
+ sceneObjects = context.getSceneObjectsFromTo(parent, this)
42
+ .filter(o => o.getTransformRef() === transformRef);
43
+ narrowedToCloneGroup = true;
35
44
  }
36
45
  }
37
46
  // Objects passed explicitly via `from(...)` bypass the part scope so that
@@ -48,12 +57,23 @@ export class SelectSceneObject extends SceneObject {
48
57
  }
49
58
  }
50
59
  const allShapes = this.constraintObject ? this.constraintObject.getShapes() : this.getAllShapes(sceneObjects, excludedObjects);
60
+ let scopeHasher = null;
51
61
  if (this.type === "edge") {
52
- this.injectScopeFaces(filters, sceneObjects);
62
+ scopeHasher = this.injectScopeFaces(filters, sceneObjects);
53
63
  }
54
64
  const fromFilters = this.injectFromMembershipSets(filters);
55
65
  try {
56
- const filteredShapes = this.applyFilters(allShapes, filters);
66
+ let filteredShapes = this.applyFilters(allShapes, filters);
67
+ if (filteredShapes.length === 0 && narrowedToCloneGroup) {
68
+ // Nothing matched within the cloned group: the original selection
69
+ // resolved to geometry outside the repeated objects (e.g. a wrap
70
+ // target face on a base solid). Reuse that resolution — re-running
71
+ // the transformed filters against base geometry cannot match.
72
+ const source = this.getCloneSource();
73
+ if (source instanceof SelectSceneObject) {
74
+ filteredShapes = source.getAddedShapes();
75
+ }
76
+ }
57
77
  this.addShapes(filteredShapes);
58
78
  }
59
79
  finally {
@@ -61,6 +81,7 @@ export class SelectSceneObject extends SceneObject {
61
81
  filter.setMembershipSet(null);
62
82
  set.delete();
63
83
  }
84
+ scopeHasher?.delete();
64
85
  }
65
86
  }
66
87
  injectFromMembershipSets(filters) {
@@ -141,6 +162,7 @@ export class SelectSceneObject extends SceneObject {
141
162
  let scopeSolids = null;
142
163
  let extraFaces = null;
143
164
  let faceByHash = null;
165
+ let hasher = null;
144
166
  for (const builder of filters) {
145
167
  for (const filter of builder.getFilters()) {
146
168
  if (filter instanceof BelongsToFaceFilter || filter instanceof NotBelongsToFaceFilter) {
@@ -159,19 +181,21 @@ export class SelectSceneObject extends SceneObject {
159
181
  extraFaces = [];
160
182
  }
161
183
  faceByHash = new Map();
184
+ hasher = new ShapeHasher();
162
185
  for (const solid of scopeSolids) {
163
186
  for (const face of solid.getFaces()) {
164
- addToBucket(faceByHash, face);
187
+ addToBucket(faceByHash, face, hasher);
165
188
  }
166
189
  }
167
190
  for (const face of extraFaces) {
168
- addToBucket(faceByHash, face);
191
+ addToBucket(faceByHash, face, hasher);
169
192
  }
170
193
  }
171
- filter.setScopeIndex(scopeSolids, extraFaces, faceByHash);
194
+ filter.setScopeIndex(scopeSolids, extraFaces, faceByHash, hasher);
172
195
  }
173
196
  }
174
197
  }
198
+ return hasher;
175
199
  }
176
200
  applyFilters(shapes, filters) {
177
201
  const shapeFilter = new ShapeFilter(shapes, ...filters);
@@ -220,8 +244,8 @@ export class SelectSceneObject extends SceneObject {
220
244
  };
221
245
  }
222
246
  }
223
- function addToBucket(faceByHash, face) {
224
- const hash = face.getShape().HashCode(2147483647);
247
+ function addToBucket(faceByHash, face, hasher) {
248
+ const hash = hasher.key(face.getShape());
225
249
  let bucket = faceByHash.get(hash);
226
250
  if (!bucket) {
227
251
  bucket = [];
@@ -18,7 +18,7 @@ export declare class Extruder {
18
18
  getEndFaces(): Face[];
19
19
  getSideFaces(): Face[];
20
20
  getInternalFaces(): Face[];
21
- extrude(): Shape<import("occjs-wrapper").TopoDS_Shape>[];
21
+ extrude(): Shape<import("ocjs-fluidcad").TopoDS_Shape>[];
22
22
  private isInternalFace;
23
23
  private applyDraft;
24
24
  }
@@ -49,9 +49,14 @@ export class Extruder {
49
49
  ? p.record('Fuse profile faces', () => BooleanOps.fuseFaces(this.faces))
50
50
  : BooleanOps.fuseFaces(this.faces);
51
51
  for (const face of fusedFaces.result) {
52
+ // Canonicalize the sweep direction (so anti-parallel halves of a
53
+ // symmetric / two-distance extrude fuse with a single merged lateral
54
+ // face) only when undrafted — drafting is sensitive to the sweep
55
+ // parametrization and must keep the literal direction.
56
+ const canonicalizeSweep = !this.draft;
52
57
  let { solid, firstFace, lastFace } = p
53
- ? p.record('Make prism from face', () => ExtrudeOps.makePrismFromVec(face, vec))
54
- : ExtrudeOps.makePrismFromVec(face, vec);
58
+ ? p.record('Make prism from face', () => ExtrudeOps.makePrismFromVec(face, vec, canonicalizeSweep))
59
+ : ExtrudeOps.makePrismFromVec(face, vec, canonicalizeSweep);
55
60
  if (this.draft) {
56
61
  const draftResult = p
57
62
  ? p.record('Apply draft', () => this.applyDraft(solid, firstFace, lastFace, this.plane))
@@ -1,9 +1,11 @@
1
1
  import { TransformablePrimitive } from "../common/transformable-primitive.js";
2
+ import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
2
3
  export declare class Sphere extends TransformablePrimitive {
3
4
  radius: number;
4
5
  angle: number;
5
6
  constructor(radius: number, angle: number);
6
- build(): void;
7
+ build(context?: BuildSceneObjectContext): void;
8
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
7
9
  compareTo(other: Sphere): boolean;
8
10
  getType(): string;
9
11
  serialize(): {
@@ -8,9 +8,12 @@ export class Sphere extends TransformablePrimitive {
8
8
  this.radius = radius;
9
9
  this.angle = angle;
10
10
  }
11
- build() {
11
+ build(context) {
12
12
  const sphere = Primitives.makeSphere(this.radius, this.angle);
13
- this.addShapes([sphere]);
13
+ this.addPrimitiveShape(sphere, context);
14
+ }
15
+ createCopy(remap) {
16
+ return new Sphere(this.radius, this.angle).syncPrimitiveWith(this);
14
17
  }
15
18
  compareTo(other) {
16
19
  if (!(other instanceof Sphere)) {
@@ -141,10 +141,15 @@ export class Sweep extends ExtrudeBase {
141
141
  this.setState('side-faces', classified.sideFaces);
142
142
  this.setState('internal-faces', classified.internalFaces);
143
143
  this.setState('cap-faces', classified.capFaces);
144
- cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
144
+ cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this, skipSimplify: true });
145
145
  return;
146
146
  }
147
- this.finalizeAndFuse(solids, classified, context);
147
+ // Sweep paths can produce tangent contact between the swept tube and
148
+ // existing scene shapes (e.g., a helix sweep along a cylinder face).
149
+ // SimplifyResult's face unification can iterate forever on the resulting
150
+ // topology — skip it for sweep ops; downstream classification doesn't
151
+ // need same-domain face merging.
152
+ this.finalizeAndFuse(solids, classified, context, { skipSimplify: true });
148
153
  }
149
154
  getSpineWire(pathObj) {
150
155
  const shapes = pathObj.getShapes({ excludeMeta: false });
@@ -0,0 +1,39 @@
1
+ import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
2
+ import { ExtrudeBase } from "./extrude-base.js";
3
+ import { Extrudable } from "../helpers/types.js";
4
+ import { IWrap } from "../core/interfaces.js";
5
+ /**
6
+ * Wraps a sketch onto a curved face (cylindrical or conical) and thickens it
7
+ * along the surface normal — emboss with `.add()` (default), deboss with
8
+ * `.remove()`, standalone pad with `.new()`. The sketch is developed onto the
9
+ * surface preserving lengths (a true wrap, not a projection).
10
+ */
11
+ export declare class Wrap extends ExtrudeBase implements IWrap {
12
+ thickness: number;
13
+ face: SceneObject;
14
+ constructor(thickness: number, face: SceneObject, source?: Extrudable | SceneObject);
15
+ build(context: BuildSceneObjectContext): void;
16
+ /** Resolve the planar sketch regions to wrap (same source rules as extrude). */
17
+ private resolveSourceFaces;
18
+ private getTargetFace;
19
+ getDependencies(): SceneObject[];
20
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
21
+ compareTo(other: Wrap): boolean;
22
+ getType(): string;
23
+ getUniqueType(): string;
24
+ serialize(): {
25
+ picking: true;
26
+ pickPoints: number[][];
27
+ trigger: "region-picking";
28
+ pickPlane: {
29
+ origin: import("../math/point.js").Point;
30
+ xDirection: import("../math/vector3d.js").Vector3d;
31
+ yDirection: import("../math/vector3d.js").Vector3d;
32
+ normal: import("../math/vector3d.js").Vector3d;
33
+ };
34
+ extrudable: any;
35
+ thickness: number;
36
+ face: string;
37
+ operationMode: "new" | "remove";
38
+ };
39
+ }