fluidcad 0.0.36 → 0.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -504
- package/README.md +1 -1
- package/lib/dist/common/edge.d.ts +1 -1
- package/lib/dist/common/face.d.ts +1 -1
- package/lib/dist/common/scene-object.d.ts +6 -0
- package/lib/dist/common/scene-object.js +8 -0
- package/lib/dist/common/shape-factory.d.ts +1 -1
- package/lib/dist/common/shape-history-tracker.d.ts +1 -1
- package/lib/dist/common/shape.d.ts +1 -1
- package/lib/dist/common/solid.d.ts +1 -1
- package/lib/dist/common/transformable-primitive.d.ts +12 -1
- package/lib/dist/common/transformable-primitive.js +27 -0
- package/lib/dist/common/vertex.d.ts +1 -1
- package/lib/dist/common/wire.d.ts +1 -1
- package/lib/dist/core/2d/index.d.ts +1 -0
- package/lib/dist/core/2d/index.js +1 -0
- package/lib/dist/core/2d/text.d.ts +30 -0
- package/lib/dist/core/2d/text.js +37 -0
- package/lib/dist/core/helix.d.ts +20 -0
- package/lib/dist/core/helix.js +36 -0
- package/lib/dist/core/index.d.ts +3 -1
- package/lib/dist/core/index.js +2 -0
- package/lib/dist/core/interfaces.d.ts +180 -0
- package/lib/dist/core/plane.d.ts +26 -6
- package/lib/dist/core/plane.js +21 -44
- package/lib/dist/core/wrap.d.ts +17 -0
- package/lib/dist/core/wrap.js +39 -0
- package/lib/dist/features/2d/offset.js +2 -2
- package/lib/dist/features/2d/text.d.ts +67 -0
- package/lib/dist/features/2d/text.js +320 -0
- package/lib/dist/features/cylinder.d.ts +3 -1
- package/lib/dist/features/cylinder.js +5 -2
- package/lib/dist/features/extrude-base.d.ts +1 -0
- package/lib/dist/features/extrude-to-face.d.ts +1 -0
- package/lib/dist/features/extrude-to-face.js +6 -0
- package/lib/dist/features/fillet.d.ts +1 -1
- package/lib/dist/features/helix.d.ts +41 -0
- package/lib/dist/features/helix.js +337 -0
- package/lib/dist/features/plane-from-object.d.ts +16 -4
- package/lib/dist/features/plane-from-object.js +101 -8
- package/lib/dist/features/select.js +32 -8
- package/lib/dist/features/simple-extruder.d.ts +1 -1
- package/lib/dist/features/simple-extruder.js +7 -2
- package/lib/dist/features/sphere.d.ts +3 -1
- package/lib/dist/features/sphere.js +5 -2
- package/lib/dist/features/sweep.js +7 -2
- package/lib/dist/features/wrap.d.ts +39 -0
- package/lib/dist/features/wrap.js +116 -0
- package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
- package/lib/dist/filters/edge/belongs-to-face.js +14 -10
- package/lib/dist/filters/filter.d.ts +1 -1
- package/lib/dist/filters/from-object.d.ts +1 -1
- package/lib/dist/filters/tangent-expander.d.ts +1 -1
- package/lib/dist/filters/tangent-expander.js +57 -40
- package/lib/dist/helpers/scene-helpers.d.ts +2 -0
- package/lib/dist/helpers/scene-helpers.js +1 -1
- package/lib/dist/index.d.ts +2 -0
- package/lib/dist/index.js +3 -1
- package/lib/dist/io/file-import.d.ts +7 -0
- package/lib/dist/io/file-import.js +28 -1
- package/lib/dist/io/font-registry.d.ts +45 -0
- package/lib/dist/io/font-registry.js +272 -0
- package/lib/dist/math/bspline-interpolation.d.ts +29 -0
- package/lib/dist/math/bspline-interpolation.js +194 -0
- package/lib/dist/oc/boolean-ops.d.ts +3 -1
- package/lib/dist/oc/boolean-ops.js +15 -1
- package/lib/dist/oc/color-transfer.d.ts +1 -1
- package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
- package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
- package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
- package/lib/dist/oc/convert.d.ts +1 -1
- package/lib/dist/oc/draft-ops.d.ts +1 -1
- package/lib/dist/oc/edge-ops.d.ts +2 -2
- package/lib/dist/oc/edge-ops.js +13 -14
- package/lib/dist/oc/edge-props.d.ts +1 -1
- package/lib/dist/oc/edge-query.d.ts +1 -1
- package/lib/dist/oc/edge-query.js +3 -8
- package/lib/dist/oc/errors.d.ts +8 -0
- package/lib/dist/oc/errors.js +27 -0
- package/lib/dist/oc/explorer.d.ts +2 -2
- package/lib/dist/oc/extrude-ops.d.ts +28 -2
- package/lib/dist/oc/extrude-ops.js +56 -7
- package/lib/dist/oc/face-ops.d.ts +2 -1
- package/lib/dist/oc/face-ops.js +11 -0
- package/lib/dist/oc/face-props.d.ts +1 -1
- package/lib/dist/oc/face-query.d.ts +12 -1
- package/lib/dist/oc/face-query.js +39 -0
- package/lib/dist/oc/fillet-ops.d.ts +1 -1
- package/lib/dist/oc/fillet-ops.js +4 -4
- package/lib/dist/oc/geometry.d.ts +1 -1
- package/lib/dist/oc/geometry.js +12 -14
- package/lib/dist/oc/helix-ops.d.ts +37 -0
- package/lib/dist/oc/helix-ops.js +88 -0
- package/lib/dist/oc/hit-test.d.ts +1 -1
- package/lib/dist/oc/index.d.ts +4 -0
- package/lib/dist/oc/index.js +2 -0
- package/lib/dist/oc/init.d.ts +1 -1
- package/lib/dist/oc/init.js +1 -1
- package/lib/dist/oc/intersection.js +1 -1
- package/lib/dist/oc/io.d.ts +6 -6
- package/lib/dist/oc/io.js +31 -24
- package/lib/dist/oc/measure/classify.d.ts +34 -0
- package/lib/dist/oc/measure/classify.js +246 -0
- package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
- package/lib/dist/oc/measure/measure-ops.js +210 -0
- package/lib/dist/oc/measure/measure-types.d.ts +39 -0
- package/lib/dist/oc/measure/measure-types.js +1 -0
- package/lib/dist/oc/measure/sampling.d.ts +9 -0
- package/lib/dist/oc/measure/sampling.js +77 -0
- package/lib/dist/oc/measure/vec.d.ts +13 -0
- package/lib/dist/oc/measure/vec.js +23 -0
- package/lib/dist/oc/mesh.d.ts +1 -1
- package/lib/dist/oc/mesh.js +40 -28
- package/lib/dist/oc/path-sampler.d.ts +29 -0
- package/lib/dist/oc/path-sampler.js +63 -0
- package/lib/dist/oc/props.d.ts +1 -1
- package/lib/dist/oc/props.js +4 -1
- package/lib/dist/oc/shape-hash.d.ts +26 -0
- package/lib/dist/oc/shape-hash.js +32 -0
- package/lib/dist/oc/shape-ops.d.ts +5 -3
- package/lib/dist/oc/shape-ops.js +6 -5
- package/lib/dist/oc/sweep-ops.d.ts +13 -1
- package/lib/dist/oc/sweep-ops.js +174 -18
- package/lib/dist/oc/text-outline.d.ts +62 -0
- package/lib/dist/oc/text-outline.js +212 -0
- package/lib/dist/oc/thin-face-maker.d.ts +0 -19
- package/lib/dist/oc/thin-face-maker.js +3 -68
- package/lib/dist/oc/topology-index.d.ts +1 -1
- package/lib/dist/oc/vertex-ops.d.ts +1 -1
- package/lib/dist/oc/wire-ops.d.ts +18 -3
- package/lib/dist/oc/wire-ops.js +56 -5
- package/lib/dist/oc/wrap-development.d.ts +105 -0
- package/lib/dist/oc/wrap-development.js +179 -0
- package/lib/dist/oc/wrap-ops.d.ts +100 -0
- package/lib/dist/oc/wrap-ops.js +406 -0
- package/lib/dist/rendering/render-solid.js +10 -2
- package/lib/dist/scene-manager.d.ts +2 -0
- package/lib/dist/scene-manager.js +29 -0
- package/lib/dist/tests/features/2d/offset.test.js +74 -1
- package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
- package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
- package/lib/dist/tests/features/helix.test.d.ts +1 -0
- package/lib/dist/tests/features/helix.test.js +295 -0
- package/lib/dist/tests/features/plane.test.js +95 -0
- package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
- package/lib/dist/tests/features/rib.test.js +6 -1
- package/lib/dist/tests/features/sweep.test.js +170 -1
- package/lib/dist/tests/features/text.test.d.ts +1 -0
- package/lib/dist/tests/features/text.test.js +347 -0
- package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-development.test.js +130 -0
- package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
- package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
- package/lib/dist/tests/features/wrap.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap.test.js +331 -0
- package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
- package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
- package/lib/dist/tests/measure.test.d.ts +1 -0
- package/lib/dist/tests/measure.test.js +288 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/helix.md +64 -0
- package/llm-docs/api/index.json +11 -2
- package/llm-docs/api/text.md +52 -0
- package/llm-docs/api/types/helix.md +105 -0
- package/llm-docs/api/types/text.md +138 -0
- package/llm-docs/api/types/wrap.md +131 -0
- package/llm-docs/api/wrap.md +62 -0
- package/llm-docs/index.json +121 -1
- package/mcp/dist/server.js +20 -1
- package/mcp/dist/tools/inspection.d.ts +17 -0
- package/mcp/dist/tools/inspection.js +14 -0
- package/package.json +7 -3
- package/server/dist/fluidcad-server.d.ts +11 -1
- package/server/dist/fluidcad-server.js +21 -1
- package/server/dist/index.js +4 -2
- package/server/dist/preferences.d.ts +4 -0
- package/server/dist/preferences.js +2 -0
- package/server/dist/routes/measure.d.ts +3 -0
- package/server/dist/routes/measure.js +32 -0
- package/server/dist/routes/params.js +1 -1
- package/server/dist/routes/preferences.js +6 -0
- package/server/dist/routes/sketch-edits.js +2 -1
- package/ui/dist/assets/{index-MRqwG9Vh.js → index-D8zV21wB.js} +149 -102
- package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
- package/ui/dist/index.html +2 -2
|
@@ -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
|
+
}
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js";
|
|
2
|
-
import { PlaneRenderableOptions } from "../core/plane.js";
|
|
2
|
+
import { EdgePlanePosition, PlaneRenderableOptions } from "../core/plane.js";
|
|
3
3
|
import { PlaneObjectBase } from "./plane-renderable-base.js";
|
|
4
4
|
import { Face } from "../common/face.js";
|
|
5
5
|
import { Point } from "../math/point.js";
|
|
6
6
|
import { Plane } from "../math/plane.js";
|
|
7
7
|
export declare class PlaneFromObject extends PlaneObjectBase {
|
|
8
8
|
sourceObject: SceneObject;
|
|
9
|
-
|
|
10
|
-
constructor(sourceObject: SceneObject,
|
|
9
|
+
optionsOrPosition?: PlaneRenderableOptions | EdgePlanePosition;
|
|
10
|
+
constructor(sourceObject: SceneObject, optionsOrPosition?: PlaneRenderableOptions | EdgePlanePosition);
|
|
11
11
|
validate(): void;
|
|
12
12
|
build(context?: BuildSceneObjectContext): void;
|
|
13
|
+
/**
|
|
14
|
+
* Builds a plane normal to `edge` at the configured position. The edge
|
|
15
|
+
* tangent at that point becomes the plane normal; the in-plane axes are
|
|
16
|
+
* an arbitrary (but deterministic) basis around it.
|
|
17
|
+
*/
|
|
18
|
+
private buildFromEdge;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the second argument for a face/plane source. A bare number is a
|
|
21
|
+
* normal-offset distance; a string position is only meaningful for edges
|
|
22
|
+
* and is rejected here.
|
|
23
|
+
*/
|
|
24
|
+
private faceOptions;
|
|
13
25
|
getFromSceneObject(sceneObject: SceneObject): {
|
|
14
26
|
plane: Plane;
|
|
15
27
|
sourceFace: Face;
|
|
@@ -24,7 +36,7 @@ export declare class PlaneFromObject extends PlaneObjectBase {
|
|
|
24
36
|
xDirection: import("../math/vector3d.js").Vector3d;
|
|
25
37
|
yDirection: import("../math/vector3d.js").Vector3d;
|
|
26
38
|
normal: import("../math/vector3d.js").Vector3d;
|
|
27
|
-
options: import("../math/plane.js").PlaneTransformOptions;
|
|
39
|
+
options: import("../math/plane.js").PlaneTransformOptions | EdgePlanePosition;
|
|
28
40
|
center: any;
|
|
29
41
|
};
|
|
30
42
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { PlaneObjectBase } from "./plane-renderable-base.js";
|
|
2
2
|
import { FaceOps } from "../oc/face-ops.js";
|
|
3
3
|
import { ShapeOps } from "../oc/shape-ops.js";
|
|
4
|
+
import { WireOps } from "../oc/wire-ops.js";
|
|
5
|
+
import { PathSampler } from "../oc/path-sampler.js";
|
|
4
6
|
import { Point } from "../math/point.js";
|
|
7
|
+
import { Plane } from "../math/plane.js";
|
|
5
8
|
import { requireShapes } from "../common/operand-check.js";
|
|
6
9
|
export class PlaneFromObject extends PlaneObjectBase {
|
|
7
10
|
sourceObject;
|
|
8
|
-
|
|
9
|
-
constructor(sourceObject,
|
|
11
|
+
optionsOrPosition;
|
|
12
|
+
constructor(sourceObject, optionsOrPosition) {
|
|
10
13
|
super();
|
|
11
14
|
this.sourceObject = sourceObject;
|
|
12
|
-
this.
|
|
15
|
+
this.optionsOrPosition = optionsOrPosition;
|
|
13
16
|
}
|
|
14
17
|
validate() {
|
|
15
18
|
// PlaneObjectBase sources expose the plane directly — no shapes required.
|
|
@@ -19,6 +22,17 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
19
22
|
requireShapes(this.sourceObject, "source", "plane");
|
|
20
23
|
}
|
|
21
24
|
build(context) {
|
|
25
|
+
// An edge source produces a plane normal to the edge at a position along
|
|
26
|
+
// it. The face-vs-edge decision is deferred to here (rather than the
|
|
27
|
+
// plane() builder) because the source shape type is only known once the
|
|
28
|
+
// selection has been resolved.
|
|
29
|
+
if (!(this.sourceObject instanceof PlaneObjectBase)) {
|
|
30
|
+
const shapes = this.sourceObject.getShapes({ excludeGuide: false });
|
|
31
|
+
if (shapes.length === 1 && shapes[0].isEdge()) {
|
|
32
|
+
this.buildFromEdge(context, shapes[0]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
22
36
|
let plane;
|
|
23
37
|
let sourceFace;
|
|
24
38
|
let center;
|
|
@@ -36,10 +50,11 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
36
50
|
const bbox = ShapeOps.getBoundingBox(sourceFace.getShape());
|
|
37
51
|
center = new Point(bbox.centerX, bbox.centerY, bbox.centerZ);
|
|
38
52
|
}
|
|
39
|
-
|
|
53
|
+
const options = this.faceOptions();
|
|
54
|
+
if (options) {
|
|
40
55
|
// Apply the same transform to the center so the preview face stays on
|
|
41
56
|
// the rotated plane instead of floating at its pre-rotation position.
|
|
42
|
-
const matrix = plane.getTransformMatrix(
|
|
57
|
+
const matrix = plane.getTransformMatrix(options);
|
|
43
58
|
plane = plane.applyMatrix(matrix);
|
|
44
59
|
if (center) {
|
|
45
60
|
center = center.transform(matrix);
|
|
@@ -60,6 +75,53 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
60
75
|
face.markAsMetaShape();
|
|
61
76
|
this.addShape(face);
|
|
62
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds a plane normal to `edge` at the configured position. The edge
|
|
80
|
+
* tangent at that point becomes the plane normal; the in-plane axes are
|
|
81
|
+
* an arbitrary (but deterministic) basis around it.
|
|
82
|
+
*/
|
|
83
|
+
buildFromEdge(context, edge) {
|
|
84
|
+
const t = normalizeEdgePosition(this.optionsOrPosition);
|
|
85
|
+
const frame = sampleEdgeFrame(edge, t);
|
|
86
|
+
// The forward tangent points *into* the edge at the start, so the plane
|
|
87
|
+
// would face inward there. Flip it at the start endpoint so it faces
|
|
88
|
+
// outward — like an extrude's start cap (the end already faces outward via
|
|
89
|
+
// the forward tangent). Interior/end positions keep the forward tangent.
|
|
90
|
+
const normal = t <= 0 ? frame.tangent.negate() : frame.tangent;
|
|
91
|
+
let plane = Plane.fromPointAndNormal(frame.point, normal);
|
|
92
|
+
let center = frame.point;
|
|
93
|
+
// Unlike the face path, an edge is only *referenced* to derive the plane —
|
|
94
|
+
// it is not consumed, so it stays available to its owning solid and to
|
|
95
|
+
// other features.
|
|
96
|
+
const transform = context?.getTransform() ?? null;
|
|
97
|
+
if (transform) {
|
|
98
|
+
plane = plane.applyMatrix(transform);
|
|
99
|
+
center = center.transform(transform);
|
|
100
|
+
}
|
|
101
|
+
this.setState('plane-center', center);
|
|
102
|
+
this.setState('plane', plane);
|
|
103
|
+
const face = FaceOps.planeToFace(plane, center);
|
|
104
|
+
face.markAsMetaShape();
|
|
105
|
+
this.addShape(face);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolves the second argument for a face/plane source. A bare number is a
|
|
109
|
+
* normal-offset distance; a string position is only meaningful for edges
|
|
110
|
+
* and is rejected here.
|
|
111
|
+
*/
|
|
112
|
+
faceOptions() {
|
|
113
|
+
const value = this.optionsOrPosition;
|
|
114
|
+
if (value == null) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value === 'number') {
|
|
118
|
+
return { offset: value };
|
|
119
|
+
}
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
throw new Error(`Plane: position '${value}' is only valid for an edge source`);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
63
125
|
getFromSceneObject(sceneObject) {
|
|
64
126
|
const shapes = sceneObject.getShapes();
|
|
65
127
|
console.log(`Plane: Retrieved ${shapes.length} shapes from selection`, shapes);
|
|
@@ -82,7 +144,7 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
82
144
|
return [];
|
|
83
145
|
}
|
|
84
146
|
createCopy(remap) {
|
|
85
|
-
return new PlaneFromObject(this, this.
|
|
147
|
+
return new PlaneFromObject(this, this.optionsOrPosition);
|
|
86
148
|
}
|
|
87
149
|
compareTo(other) {
|
|
88
150
|
if (!(other instanceof PlaneFromObject)) {
|
|
@@ -94,7 +156,7 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
94
156
|
if (!this.sourceObject.compareTo(other.sourceObject)) {
|
|
95
157
|
return false;
|
|
96
158
|
}
|
|
97
|
-
if (JSON.stringify(this.
|
|
159
|
+
if (JSON.stringify(this.optionsOrPosition) !== JSON.stringify(other.optionsOrPosition)) {
|
|
98
160
|
return false;
|
|
99
161
|
}
|
|
100
162
|
return true;
|
|
@@ -109,8 +171,39 @@ export class PlaneFromObject extends PlaneObjectBase {
|
|
|
109
171
|
xDirection: plane.xDirection,
|
|
110
172
|
yDirection: plane.yDirection,
|
|
111
173
|
normal: plane.normal,
|
|
112
|
-
options: this.
|
|
174
|
+
options: this.optionsOrPosition,
|
|
113
175
|
center: this.getState('plane-center') || plane.origin,
|
|
114
176
|
};
|
|
115
177
|
}
|
|
116
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Evaluates the point and unit (forward) tangent on `edge` at a normalized
|
|
181
|
+
* position `t` (`0` = start, `1` = end), measured by arc length.
|
|
182
|
+
*/
|
|
183
|
+
function sampleEdgeFrame(edge, t) {
|
|
184
|
+
const wire = WireOps.makeWireFromEdges([edge]);
|
|
185
|
+
const sampler = new PathSampler(wire);
|
|
186
|
+
try {
|
|
187
|
+
return sampler.evalAt(t * sampler.length);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
sampler.dispose();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function normalizeEdgePosition(position) {
|
|
194
|
+
if (position === undefined) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
if (typeof position === 'number') {
|
|
198
|
+
return position;
|
|
199
|
+
}
|
|
200
|
+
switch (position) {
|
|
201
|
+
case 'start':
|
|
202
|
+
return 0;
|
|
203
|
+
case 'middle':
|
|
204
|
+
return 0.5;
|
|
205
|
+
case 'end':
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
throw new Error("Plane: an edge plane takes a 0–1 position or 'start'/'middle'/'end', not transform options");
|
|
209
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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()
|
|
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("
|
|
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))
|