@metamesh/modeling 0.0.0-stage → 0.7.0
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/CHANGELOG.md +111 -0
- package/LICENSE +200 -0
- package/README.md +376 -2
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +71 -0
- package/dist/api/geometry.d.ts +3136 -0
- package/dist/api/index.d.ts +2087 -0
- package/dist/api/text.d.ts +93 -0
- package/dist/engine/construction-operations.js +189 -0
- package/dist/engine/execution.js +227 -0
- package/dist/engine/export-3mf.js +100 -0
- package/dist/engine/extrusion-finishing.js +82 -0
- package/dist/engine/finalization.js +214 -0
- package/dist/engine/index.js +83 -0
- package/dist/engine/inspection-operations.js +52 -0
- package/dist/engine/kernel/LICENSE +201 -0
- package/dist/engine/kernel/manifold.js +2 -0
- package/dist/engine/kernel/manifold.wasm +0 -0
- package/dist/engine/loft-operations.js +156 -0
- package/dist/engine/mechanical-operations.js +51 -0
- package/dist/engine/modeling-context.js +38 -0
- package/dist/engine/native/adapter.js +20 -0
- package/dist/engine/native/context.js +209 -0
- package/dist/engine/native/copied-views.js +46 -0
- package/dist/engine/native/index.js +13 -0
- package/dist/engine/native/operations.js +115 -0
- package/dist/engine/native/ownership.js +98 -0
- package/dist/engine/native/regions.js +138 -0
- package/dist/engine/native/scoped-primitives.js +127 -0
- package/dist/engine/native/scoped-threads.js +34 -0
- package/dist/engine/native/threads.js +52 -0
- package/dist/engine/native/transforms.js +111 -0
- package/dist/engine/operation-environment.js +20 -0
- package/dist/engine/pattern-operations.js +36 -0
- package/dist/engine/pattern-placements.js +176 -0
- package/dist/engine/primitive-anchors.js +70 -0
- package/dist/engine/primitive-correspondence.js +74 -0
- package/dist/engine/primitive-operations.js +151 -0
- package/dist/engine/primitive-profile-types.js +1 -0
- package/dist/engine/primitive-profiles.js +250 -0
- package/dist/engine/profile-mesh.js +279 -0
- package/dist/engine/section-operations.js +93 -0
- package/dist/engine/shape-anchors.js +88 -0
- package/dist/engine/transform-operations.js +225 -0
- package/dist/engine/types.js +1 -0
- package/dist/geometry/anchors.js +226 -0
- package/dist/geometry/curves/evaluation.js +162 -0
- package/dist/geometry/curves/sampling.js +266 -0
- package/dist/geometry/curves/types.js +1 -0
- package/dist/geometry/cylinder.js +117 -0
- package/dist/geometry/edge-finishing.js +160 -0
- package/dist/geometry/errors.js +76 -0
- package/dist/geometry/extrusion.js +150 -0
- package/dist/geometry/heightfield.js +115 -0
- package/dist/geometry/index.js +42 -0
- package/dist/geometry/loft-correspondence.js +204 -0
- package/dist/geometry/loft-geometry.js +184 -0
- package/dist/geometry/loft-options.js +56 -0
- package/dist/geometry/loft-simple-mesh.js +92 -0
- package/dist/geometry/loft-triangulation.js +64 -0
- package/dist/geometry/loft.js +95 -0
- package/dist/geometry/math/vector3.js +71 -0
- package/dist/geometry/mechanical/gear-motion.js +94 -0
- package/dist/geometry/mechanical/gears.js +484 -0
- package/dist/geometry/mechanical/index.js +13 -0
- package/dist/geometry/mechanical/standards.js +67 -0
- package/dist/geometry/mechanical/thread-helices.js +44 -0
- package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
- package/dist/geometry/mechanical/thread-mesh.js +142 -0
- package/dist/geometry/mechanical/thread-profiles.js +373 -0
- package/dist/geometry/mechanical/thread-solids.js +230 -0
- package/dist/geometry/mechanical/thread-surface.js +433 -0
- package/dist/geometry/mechanical/threads.js +138 -0
- package/dist/geometry/mesh-edge-table.js +126 -0
- package/dist/geometry/mesh-planar.js +408 -0
- package/dist/geometry/mesh-quality.js +396 -0
- package/dist/geometry/mesh-signature.js +137 -0
- package/dist/geometry/mesh.js +283 -0
- package/dist/geometry/model-limits.js +50 -0
- package/dist/geometry/numeric.js +11 -0
- package/dist/geometry/paths/frames.js +118 -0
- package/dist/geometry/paths/helix.js +49 -0
- package/dist/geometry/paths/path.js +32 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +125 -0
- package/dist/geometry/polygon-clipping.js +52 -0
- package/dist/geometry/primitive-options.js +1 -0
- package/dist/geometry/primitive-resolution.js +17 -0
- package/dist/geometry/primitive-resolvers.js +235 -0
- package/dist/geometry/profiles/matched.js +192 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +91 -0
- package/dist/geometry/profiles/rectangle.js +249 -0
- package/dist/geometry/profiles/shapes.js +41 -0
- package/dist/geometry/profiles/span-sampling.js +28 -0
- package/dist/geometry/profiles/types.js +1 -0
- package/dist/geometry/profiles/validation.js +20 -0
- package/dist/geometry/regions.js +371 -0
- package/dist/geometry/resolution.js +236 -0
- package/dist/geometry/revolve/revolve-mesh.js +129 -0
- package/dist/geometry/sampling-limits.js +29 -0
- package/dist/geometry/stroke-path.js +181 -0
- package/dist/geometry/svg/errors.js +23 -0
- package/dist/geometry/svg/fill.js +172 -0
- package/dist/geometry/svg/index.js +1 -0
- package/dist/geometry/svg/matrix.js +130 -0
- package/dist/geometry/svg/parser.js +536 -0
- package/dist/geometry/svg/path.js +349 -0
- package/dist/geometry/svg/types.js +1 -0
- package/dist/geometry/svg-import.js +662 -0
- package/dist/geometry/sweeps/options.js +28 -0
- package/dist/geometry/sweeps/orientation.js +106 -0
- package/dist/geometry/sweeps/stations.js +161 -0
- package/dist/geometry/sweeps/sweep-closed.js +96 -0
- package/dist/geometry/sweeps/sweep-mesh.js +30 -0
- package/dist/geometry/sweeps/sweep-open.js +76 -0
- package/dist/geometry/sweeps/validation.js +116 -0
- package/dist/geometry/text-contours.js +32 -0
- package/dist/geometry/text-layout.js +1 -0
- package/dist/geometry/tolerance.js +53 -0
- package/dist/geometry/transform-geometry.js +41 -0
- package/dist/geometry/transform3.js +340 -0
- package/dist/geometry/types.js +1 -0
- package/dist/index.js +1 -0
- package/dist/model/execution-binding.js +30 -0
- package/dist/model/operations.js +154 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +58 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +100 -3
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/** Periodic surface columns and their phase-aware zipper stitching. */
|
|
2
|
+
import { cross3, dot3, length3, subtract3 } from "../math/vector3.js";
|
|
3
|
+
import { applyThreadSurfaceBevel, requirePositiveThreadSurfaceRadius, threadSurfaceBevelBoundaryAt, } from "./thread-mesh-plan.js";
|
|
4
|
+
const threadLeadInScale = (progress, shape, leadInLength) => {
|
|
5
|
+
if (shape === "cut")
|
|
6
|
+
return progress > 0 ? { height: 1, width: 1 } : { height: 0, width: 1 };
|
|
7
|
+
if (progress <= 0)
|
|
8
|
+
return { height: 0, width: shape === "linear" ? 0.325 : 0.2875 };
|
|
9
|
+
if (progress > 1)
|
|
10
|
+
return { height: 1, width: 1 };
|
|
11
|
+
if (shape === "linear") {
|
|
12
|
+
const scale = 0.1 + 0.9 * progress;
|
|
13
|
+
return { height: scale, width: 0.75 * scale + 0.25 };
|
|
14
|
+
}
|
|
15
|
+
if (shape === "smooth") {
|
|
16
|
+
const radius = (leadInLength * leadInLength + (1 - 0.05 * 0.05)) / (2 * (1 - 0.05));
|
|
17
|
+
const scale = Math.sqrt(radius * radius - (leadInLength * (1 - progress)) ** 2) - (radius - 1);
|
|
18
|
+
return { height: scale, width: 0.75 * scale + 0.25 };
|
|
19
|
+
}
|
|
20
|
+
const scale = Math.sqrt(progress + 0.05 * 0.05 * (1 - progress));
|
|
21
|
+
return { height: scale, width: 0.75 * scale + 0.25 };
|
|
22
|
+
};
|
|
23
|
+
const threadLeadInScaleAtCenter = (center, resolved) => {
|
|
24
|
+
if (resolved.leadInShape === "cut" || !(resolved.leadInAxialLength > 0)) {
|
|
25
|
+
return { height: 1, width: 1 };
|
|
26
|
+
}
|
|
27
|
+
const progressFromTip = (distance) => Math.abs(distance) <= resolved.tolerance.linear ? 0 : distance / resolved.leadInAxialLength;
|
|
28
|
+
const bottom = resolved.bottom === "blunt"
|
|
29
|
+
? threadLeadInScale(progressFromTip(center - resolved.bottomLeadTipCenter), resolved.leadInShape, resolved.leadInLength)
|
|
30
|
+
: { height: 1, width: 1 };
|
|
31
|
+
const top = resolved.top === "blunt"
|
|
32
|
+
? threadLeadInScale(progressFromTip(resolved.topLeadTipCenter - center), resolved.leadInShape, resolved.leadInLength)
|
|
33
|
+
: { height: 1, width: 1 };
|
|
34
|
+
return { height: Math.min(bottom.height, top.height), width: Math.min(bottom.width, top.width) };
|
|
35
|
+
};
|
|
36
|
+
const wrappedThreadCoordinate = (value, pitch) => {
|
|
37
|
+
const wrapped = (((value + pitch / 2) % pitch) + pitch) % pitch;
|
|
38
|
+
return wrapped - pitch / 2;
|
|
39
|
+
};
|
|
40
|
+
const threadSectionRadialAt = (section, envelopeRadial, coordinate, linearTolerance) => {
|
|
41
|
+
if (coordinate < section[0][0] - linearTolerance ||
|
|
42
|
+
coordinate > section.at(-1)[0] + linearTolerance) {
|
|
43
|
+
return envelopeRadial;
|
|
44
|
+
}
|
|
45
|
+
for (let index = 1; index < section.length; index += 1) {
|
|
46
|
+
const previous = section[index - 1];
|
|
47
|
+
const next = section[index];
|
|
48
|
+
if (coordinate > next[0] + linearTolerance)
|
|
49
|
+
continue;
|
|
50
|
+
const run = next[0] - previous[0];
|
|
51
|
+
if (run <= linearTolerance)
|
|
52
|
+
return Math.max(previous[1], next[1]);
|
|
53
|
+
const factor = Math.max(0, Math.min(1, (coordinate - previous[0]) / run));
|
|
54
|
+
return previous[1] + (next[1] - previous[1]) * factor;
|
|
55
|
+
}
|
|
56
|
+
return envelopeRadial;
|
|
57
|
+
};
|
|
58
|
+
const periodicThreadSectionRadialAt = (section, envelopeRadial, axial, pitch, linearTolerance) => threadSectionRadialAt(section, envelopeRadial, wrappedThreadCoordinate(axial, pitch), linearTolerance);
|
|
59
|
+
const threadEndFactor = (z, profile, resolved) => {
|
|
60
|
+
const lower = resolved.bottomBoundary;
|
|
61
|
+
const upper = resolved.topBoundary;
|
|
62
|
+
if (z < lower - resolved.tolerance.linear || z > upper + resolved.tolerance.linear)
|
|
63
|
+
return 0;
|
|
64
|
+
const bottomFactor = resolved.bottom === "tapered" ? Math.max(0, (z - lower) / profile.depth) : 1;
|
|
65
|
+
const topFactor = resolved.top === "tapered" ? Math.max(0, (upper - z) / profile.depth) : 1;
|
|
66
|
+
return Math.max(0, Math.min(1, bottomFactor, topFactor));
|
|
67
|
+
};
|
|
68
|
+
/** @internal Emits one angular column with semantic profile and end stations. */
|
|
69
|
+
export const buildThreadSurfaceColumn = (profile, resolved, options, angle, maximumAxialSegmentLength, positions) => {
|
|
70
|
+
const rootRadius = resolved.diameter / 2 - profile.depth;
|
|
71
|
+
const majorRadius = resolved.diameter / 2;
|
|
72
|
+
const usesMajorEnvelopeSurface = resolved.leadInEnvelope === "major" &&
|
|
73
|
+
resolved.leadInAxialLength > 0 &&
|
|
74
|
+
(resolved.bottom === "blunt" || resolved.top === "blunt");
|
|
75
|
+
const leadInEnvelopeRadial = usesMajorEnvelopeSurface ? 0 : -profile.depth;
|
|
76
|
+
const leadInEnvelopeRadius = usesMajorEnvelopeSurface ? majorRadius : rootRadius;
|
|
77
|
+
const phaseShift = resolved.surfacePhaseBase +
|
|
78
|
+
(resolved.direction * resolved.lead * (angle - resolved.phase)) / (2 * Math.PI);
|
|
79
|
+
const profileSegmentOrdinalAt = (axial) => {
|
|
80
|
+
const period = Math.round((axial - phaseShift) / profile.pitch);
|
|
81
|
+
const localAxial = axial - phaseShift - period * profile.pitch;
|
|
82
|
+
for (let segment = 0; segment < resolved.surfaceSection.length - 1; segment += 1) {
|
|
83
|
+
const start = resolved.surfaceSection[segment][0];
|
|
84
|
+
const end = resolved.surfaceSection[segment + 1][0];
|
|
85
|
+
if (localAxial < start - resolved.tolerance.linear)
|
|
86
|
+
continue;
|
|
87
|
+
if (localAxial > end + resolved.tolerance.linear)
|
|
88
|
+
continue;
|
|
89
|
+
return period * resolved.surfaceSection.length + segment + 0.5;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
const usesScaledLeadIn = resolved.leadInAxialLength > 0 && (resolved.bottom === "blunt" || resolved.top === "blunt");
|
|
94
|
+
const bevelOffset = options.bevelOffset ?? 0;
|
|
95
|
+
const scaledRadiusAt = (axial) => {
|
|
96
|
+
if ((resolved.bottom !== "standard" && axial < resolved.bottomBoundary) ||
|
|
97
|
+
(resolved.top !== "standard" && axial > resolved.topBoundary)) {
|
|
98
|
+
return leadInEnvelopeRadius;
|
|
99
|
+
}
|
|
100
|
+
const period = Math.round((axial - phaseShift) / profile.pitch);
|
|
101
|
+
const center = phaseShift + period * profile.pitch;
|
|
102
|
+
const scale = threadLeadInScaleAtCenter(center, resolved);
|
|
103
|
+
const localAxial = (axial - center) / scale.width;
|
|
104
|
+
const profileRadius = majorRadius +
|
|
105
|
+
threadSectionRadialAt(resolved.surfaceSection, leadInEnvelopeRadial, localAxial, resolved.tolerance.linear);
|
|
106
|
+
return leadInEnvelopeRadius + (profileRadius - leadInEnvelopeRadius) * scale.height;
|
|
107
|
+
};
|
|
108
|
+
const rawRadiusAt = (sample, axial) => {
|
|
109
|
+
if (sample.surfaceRadius !== undefined) {
|
|
110
|
+
return sample.surfaceRadius;
|
|
111
|
+
}
|
|
112
|
+
if (usesScaledLeadIn) {
|
|
113
|
+
return scaledRadiusAt(axial);
|
|
114
|
+
}
|
|
115
|
+
const endFactor = sample.endFactor ?? threadEndFactor(axial, profile, resolved);
|
|
116
|
+
const profileRadius = majorRadius +
|
|
117
|
+
(sample.profileRadial ??
|
|
118
|
+
periodicThreadSectionRadialAt(resolved.surfaceSection, leadInEnvelopeRadial, axial - phaseShift, profile.pitch, resolved.tolerance.linear));
|
|
119
|
+
const radial = leadInEnvelopeRadius + (profileRadius - leadInEnvelopeRadius) * endFactor;
|
|
120
|
+
return radial;
|
|
121
|
+
};
|
|
122
|
+
const radiusAt = (sample, axial) => {
|
|
123
|
+
return applyThreadSurfaceBevel(rawRadiusAt(sample, axial), axial, profile, resolved, options);
|
|
124
|
+
};
|
|
125
|
+
const candidates = [];
|
|
126
|
+
let order = 0;
|
|
127
|
+
const add = (z, kind = "regular", profileRadial, endFactor, surfaceRadius, profileOrdinal, atLeadTip = false) => {
|
|
128
|
+
if (z < -resolved.tolerance.linear || z > resolved.length + resolved.tolerance.linear)
|
|
129
|
+
return;
|
|
130
|
+
candidates.push({
|
|
131
|
+
z: Math.max(0, Math.min(resolved.length, z)),
|
|
132
|
+
kind,
|
|
133
|
+
order: order++,
|
|
134
|
+
...(profileRadial === undefined ? {} : { profileRadial }),
|
|
135
|
+
...(endFactor === undefined ? {} : { endFactor }),
|
|
136
|
+
...(surfaceRadius === undefined ? {} : { surfaceRadius }),
|
|
137
|
+
...(profileOrdinal === undefined ? {} : { profileOrdinal }),
|
|
138
|
+
...(atLeadTip ? { atLeadTip: true } : {}),
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
add(0);
|
|
142
|
+
add(resolved.length);
|
|
143
|
+
const bottomBevelSize = Math.min(options.bevelBottomSize ?? options.bevelSize ?? profile.depth, resolved.length / 2);
|
|
144
|
+
const topBevelSize = Math.min(options.bevelTopSize ?? options.bevelSize ?? profile.depth, resolved.length / 2);
|
|
145
|
+
const fixedSamples = [resolved.bottomBoundary, resolved.topBoundary];
|
|
146
|
+
if (resolved.bottom === "tapered")
|
|
147
|
+
fixedSamples.push(resolved.bottomBoundary + profile.depth);
|
|
148
|
+
if (resolved.top === "tapered")
|
|
149
|
+
fixedSamples.push(resolved.topBoundary - profile.depth);
|
|
150
|
+
if (options.bevelBottom === true) {
|
|
151
|
+
fixedSamples.push(bevelOffset, bevelOffset + bottomBevelSize);
|
|
152
|
+
}
|
|
153
|
+
if (options.bevelTop === true) {
|
|
154
|
+
fixedSamples.push(resolved.length - bevelOffset - topBevelSize, resolved.length - bevelOffset);
|
|
155
|
+
}
|
|
156
|
+
if (options.bevelBottom === true && options.bevelTop === true) {
|
|
157
|
+
const bottomBevelEnd = bevelOffset + bottomBevelSize;
|
|
158
|
+
const topBevelStart = resolved.length - bevelOffset - topBevelSize;
|
|
159
|
+
if (bottomBevelEnd >= topBevelStart) {
|
|
160
|
+
fixedSamples.push((bottomBevelEnd + topBevelStart) / 2);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const z of fixedSamples)
|
|
164
|
+
add(z);
|
|
165
|
+
const activeMinimum = Math.max(0, resolved.bottomBoundary);
|
|
166
|
+
const activeMaximum = Math.min(resolved.length, resolved.topBoundary);
|
|
167
|
+
if (usesScaledLeadIn) {
|
|
168
|
+
const firstPeriod = Math.floor((activeMinimum - phaseShift) / profile.pitch) - 1;
|
|
169
|
+
const lastPeriod = Math.ceil((activeMaximum - phaseShift) / profile.pitch) + 1;
|
|
170
|
+
for (let period = firstPeriod; period <= lastPeriod; period += 1) {
|
|
171
|
+
const center = phaseShift + period * profile.pitch;
|
|
172
|
+
const scale = threadLeadInScaleAtCenter(center, resolved);
|
|
173
|
+
const atBottomTip = resolved.bottom === "blunt" &&
|
|
174
|
+
Math.abs(center - resolved.bottomLeadTipCenter) <= resolved.tolerance.linear;
|
|
175
|
+
const atTopTip = resolved.top === "blunt" &&
|
|
176
|
+
Math.abs(center - resolved.topLeadTipCenter) <= resolved.tolerance.linear;
|
|
177
|
+
if (scale.height <= resolved.tolerance.linear && !atBottomTip && !atTopTip)
|
|
178
|
+
continue;
|
|
179
|
+
// Keep only the exact zero-height, width-scaled tip copy. Its collinear envelope vertices give
|
|
180
|
+
// the first raised angular column matching profile ordinals to taper into; omitting it makes
|
|
181
|
+
// the zipper collapse the whole tip into one abrupt fan/wedge. More zero-height copies are
|
|
182
|
+
// redundant in this sparse mesher and would reintroduce dense, near-collinear triangles.
|
|
183
|
+
for (const [profileIndex, [profileAxial, profileRadial],] of resolved.surfaceSection.entries()) {
|
|
184
|
+
const profileRadius = majorRadius + profileRadial;
|
|
185
|
+
add(center + profileAxial * scale.width, "profile", undefined, undefined, leadInEnvelopeRadius + (profileRadius - leadInEnvelopeRadius) * scale.height, period * resolved.surfaceSection.length + profileIndex, atBottomTip || atTopTip);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
for (const [profileIndex, [profileAxial, profileRadial]] of resolved.surfaceSection.entries()) {
|
|
191
|
+
const firstPeriod = Math.ceil((activeMinimum - phaseShift - profileAxial - resolved.tolerance.linear) / profile.pitch);
|
|
192
|
+
const lastPeriod = Math.floor((activeMaximum - phaseShift - profileAxial + resolved.tolerance.linear) / profile.pitch);
|
|
193
|
+
for (let period = firstPeriod; period <= lastPeriod; period += 1) {
|
|
194
|
+
add(phaseShift + profileAxial + period * profile.pitch, "profile", profileRadial, undefined, undefined, period * resolved.surfaceSection.length + profileIndex);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (resolved.bottom === "blunt" &&
|
|
199
|
+
!usesScaledLeadIn &&
|
|
200
|
+
resolved.bottomBoundary > resolved.tolerance.linear &&
|
|
201
|
+
resolved.bottomBoundary < resolved.length - resolved.tolerance.linear) {
|
|
202
|
+
add(resolved.bottomBoundary, "forced-bottom", undefined, 0);
|
|
203
|
+
}
|
|
204
|
+
const upperThreadBoundary = resolved.topBoundary;
|
|
205
|
+
if (resolved.top === "blunt" &&
|
|
206
|
+
!usesScaledLeadIn &&
|
|
207
|
+
upperThreadBoundary > resolved.tolerance.linear &&
|
|
208
|
+
upperThreadBoundary < resolved.length - resolved.tolerance.linear) {
|
|
209
|
+
add(upperThreadBoundary, "forced-top", undefined, 0);
|
|
210
|
+
}
|
|
211
|
+
candidates.sort((left, right) => left.z - right.z || left.order - right.order);
|
|
212
|
+
const normalized = [];
|
|
213
|
+
for (let start = 0; start < candidates.length;) {
|
|
214
|
+
let end = start + 1;
|
|
215
|
+
while (end < candidates.length &&
|
|
216
|
+
Math.abs(candidates[end].z - candidates[start].z) <= resolved.tolerance.linear) {
|
|
217
|
+
end += 1;
|
|
218
|
+
}
|
|
219
|
+
const group = candidates.slice(start, end);
|
|
220
|
+
const profiles = group.filter((sample) => sample.kind === "profile");
|
|
221
|
+
const regular = group.find((sample) => sample.kind === "regular");
|
|
222
|
+
let surfaceSamples = profiles.length > 0 ? profiles : regular === undefined ? [] : [regular];
|
|
223
|
+
const canonicalZ = group.find((sample) => sample.kind === "forced-bottom" || sample.kind === "forced-top")?.z ??
|
|
224
|
+
regular?.z ??
|
|
225
|
+
group[0].z;
|
|
226
|
+
if (canonicalZ <= resolved.tolerance.linear && surfaceSamples.length > 1) {
|
|
227
|
+
surfaceSamples = [surfaceSamples.at(-1)];
|
|
228
|
+
}
|
|
229
|
+
else if (canonicalZ >= resolved.length - resolved.tolerance.linear &&
|
|
230
|
+
surfaceSamples.length > 1) {
|
|
231
|
+
surfaceSamples = [surfaceSamples[0]];
|
|
232
|
+
}
|
|
233
|
+
const selected = [
|
|
234
|
+
...group.filter((sample) => sample.kind === "forced-bottom"),
|
|
235
|
+
...surfaceSamples,
|
|
236
|
+
...group.filter((sample) => sample.kind === "forced-top"),
|
|
237
|
+
];
|
|
238
|
+
for (const sample of selected)
|
|
239
|
+
normalized.push({ ...sample, z: canonicalZ });
|
|
240
|
+
start = end;
|
|
241
|
+
}
|
|
242
|
+
const withBevelIntersections = [];
|
|
243
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
244
|
+
const sample = normalized[index];
|
|
245
|
+
withBevelIntersections.push(sample);
|
|
246
|
+
const next = normalized[index + 1];
|
|
247
|
+
if (next === undefined || next.z - sample.z <= resolved.tolerance.linear)
|
|
248
|
+
continue;
|
|
249
|
+
const firstBoundary = threadSurfaceBevelBoundaryAt(sample.z, profile, resolved, options);
|
|
250
|
+
const secondBoundary = threadSurfaceBevelBoundaryAt(next.z, profile, resolved, options);
|
|
251
|
+
if (firstBoundary?.endStyle !== "standard" ||
|
|
252
|
+
secondBoundary?.endStyle !== "standard" ||
|
|
253
|
+
firstBoundary.kind !== secondBoundary.kind) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const firstDelta = rawRadiusAt(sample, sample.z) - firstBoundary.radius;
|
|
257
|
+
const secondDelta = rawRadiusAt(next, next.z) - secondBoundary.radius;
|
|
258
|
+
if (!((firstDelta < -resolved.tolerance.linear && secondDelta > resolved.tolerance.linear) ||
|
|
259
|
+
(firstDelta > resolved.tolerance.linear && secondDelta < -resolved.tolerance.linear))) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const factor = firstDelta / (firstDelta - secondDelta);
|
|
263
|
+
const crossingZ = sample.z + (next.z - sample.z) * factor;
|
|
264
|
+
if (crossingZ - sample.z <= resolved.tolerance.linear ||
|
|
265
|
+
next.z - crossingZ <= resolved.tolerance.linear) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const crossingOrdinal = profileSegmentOrdinalAt(crossingZ);
|
|
269
|
+
withBevelIntersections.push({
|
|
270
|
+
kind: "regular",
|
|
271
|
+
order: order++,
|
|
272
|
+
...(crossingOrdinal === undefined ? {} : { profileOrdinal: crossingOrdinal }),
|
|
273
|
+
surfaceRadius: firstBoundary.radius + (secondBoundary.radius - firstBoundary.radius) * factor,
|
|
274
|
+
z: crossingZ,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// A fixed standard-bevel station can land microscopically beside a semantic profile or bevel
|
|
278
|
+
// crossing. Keeping both creates a long, almost-collinear triangle around the circumference even
|
|
279
|
+
// though the radial difference is equally sub-facet. Preserve the fixed station so every column
|
|
280
|
+
// still meets the exact bevel-transition ring; favoring the nearby semantic sample instead makes
|
|
281
|
+
// that ring wander by one sub-facet and produces a visible bump. Faces, blunt transitions,
|
|
282
|
+
// vertical profile steps, forced boundaries, and pairs of semantic samples remain exact.
|
|
283
|
+
const stationSnapTolerance = Math.max(resolved.tolerance.linear, resolved.lead / resolved.segments / 16);
|
|
284
|
+
const isPlainStation = (sample) => sample.kind === "regular" &&
|
|
285
|
+
sample.profileOrdinal === undefined &&
|
|
286
|
+
sample.surfaceRadius === undefined;
|
|
287
|
+
const isCoalescibleStandardBevelStation = (sample) => isPlainStation(sample) &&
|
|
288
|
+
threadSurfaceBevelBoundaryAt(sample.z, profile, resolved, options)?.endStyle === "standard";
|
|
289
|
+
const isFace = (sample) => sample.z <= resolved.tolerance.linear ||
|
|
290
|
+
sample.z >= resolved.length - resolved.tolerance.linear;
|
|
291
|
+
const coalesced = [];
|
|
292
|
+
for (const sample of withBevelIntersections) {
|
|
293
|
+
const previous = coalesced.at(-1);
|
|
294
|
+
const previousIsPlain = previous !== undefined && isCoalescibleStandardBevelStation(previous);
|
|
295
|
+
const sampleIsPlain = isCoalescibleStandardBevelStation(sample);
|
|
296
|
+
const previousIsSemantic = previous !== undefined && !isPlainStation(previous);
|
|
297
|
+
const sampleIsSemantic = !isPlainStation(sample);
|
|
298
|
+
if (previous !== undefined &&
|
|
299
|
+
((previousIsPlain && sampleIsSemantic) || (sampleIsPlain && previousIsSemantic)) &&
|
|
300
|
+
!(previousIsPlain ? isFace(previous) : isFace(sample)) &&
|
|
301
|
+
Math.abs(sample.z - previous.z) <= stationSnapTolerance &&
|
|
302
|
+
Math.abs(radiusAt(sample, sample.z) - radiusAt(previous, previous.z)) <= stationSnapTolerance) {
|
|
303
|
+
if (sampleIsPlain)
|
|
304
|
+
coalesced[coalesced.length - 1] = sample;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
coalesced.push(sample);
|
|
308
|
+
}
|
|
309
|
+
const sampled = [];
|
|
310
|
+
for (let index = 0; index < coalesced.length; index += 1) {
|
|
311
|
+
const sample = coalesced[index];
|
|
312
|
+
sampled.push(sample);
|
|
313
|
+
const next = coalesced[index + 1];
|
|
314
|
+
if (next === undefined || maximumAxialSegmentLength === undefined)
|
|
315
|
+
continue;
|
|
316
|
+
const distance = next.z - sample.z;
|
|
317
|
+
if (distance <= resolved.tolerance.linear)
|
|
318
|
+
continue;
|
|
319
|
+
const intervals = Math.ceil(distance / maximumAxialSegmentLength);
|
|
320
|
+
for (let interval = 1; interval < intervals; interval += 1) {
|
|
321
|
+
sampled.push({
|
|
322
|
+
atLeadTip: sample.atLeadTip === true && next.atLeadTip === true,
|
|
323
|
+
kind: "regular",
|
|
324
|
+
order: order++,
|
|
325
|
+
z: sample.z + (distance * interval) / intervals,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const indices = [];
|
|
330
|
+
const leadTips = [];
|
|
331
|
+
const profileOrdinals = [];
|
|
332
|
+
const z = [];
|
|
333
|
+
const cosine = Math.cos(angle);
|
|
334
|
+
const sine = Math.sin(angle);
|
|
335
|
+
let previousRadius;
|
|
336
|
+
let previousStoredX;
|
|
337
|
+
let previousStoredY;
|
|
338
|
+
let previousStoredZ;
|
|
339
|
+
let previousZ;
|
|
340
|
+
for (const sample of sampled) {
|
|
341
|
+
const radius = radiusAt(sample, sample.z);
|
|
342
|
+
requirePositiveThreadSurfaceRadius(radius, resolved, "buildHelicalThreadSolidMesh");
|
|
343
|
+
if (previousRadius !== undefined &&
|
|
344
|
+
previousZ !== undefined &&
|
|
345
|
+
Math.abs(sample.z - previousZ) <= resolved.tolerance.linear &&
|
|
346
|
+
Math.abs(radius - previousRadius) <= resolved.tolerance.linear) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Topology is ultimately stored as Float32. Two double-precision axial samples can be farther
|
|
350
|
+
// apart than the modeling tolerance yet still become the same stored XYZ point, particularly
|
|
351
|
+
// where a regular sample lands within one Float32 ULP of a flat-profile breakpoint. Collapse
|
|
352
|
+
// that pair before stitching; same-Z vertical steps remain because their stored radii differ.
|
|
353
|
+
const storedX = Math.fround(radius * cosine);
|
|
354
|
+
const storedY = Math.fround(radius * sine);
|
|
355
|
+
const storedZ = Math.fround(sample.z);
|
|
356
|
+
if (storedX === previousStoredX && storedY === previousStoredY && storedZ === previousStoredZ) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
indices.push(positions.length / 3);
|
|
360
|
+
leadTips.push(sample.atLeadTip === true);
|
|
361
|
+
profileOrdinals.push(sample.profileOrdinal);
|
|
362
|
+
z.push(sample.z);
|
|
363
|
+
positions.push(storedX, storedY, storedZ);
|
|
364
|
+
previousRadius = radius;
|
|
365
|
+
previousStoredX = storedX;
|
|
366
|
+
previousStoredY = storedY;
|
|
367
|
+
previousStoredZ = storedZ;
|
|
368
|
+
previousZ = sample.z;
|
|
369
|
+
}
|
|
370
|
+
return { indices, leadTips, profileOrdinals, z };
|
|
371
|
+
};
|
|
372
|
+
/** @internal Joins adjacent phase grids while preserving duplicate-Z flank order. */
|
|
373
|
+
export const stitchThreadSurfaceColumns = (first, second, direction, secondProfileOrdinalOffset, linearTolerance, positions, useConcaveLeadTip, indices) => {
|
|
374
|
+
const positionAt = (index) => {
|
|
375
|
+
const offset = index * 3;
|
|
376
|
+
return [positions[offset], positions[offset + 1], positions[offset + 2]];
|
|
377
|
+
};
|
|
378
|
+
const usesSecondDiagonal = (i1, i2, i3, i4, isLeadTipQuad) => {
|
|
379
|
+
if (!useConcaveLeadTip || !isLeadTipQuad)
|
|
380
|
+
return direction === -1;
|
|
381
|
+
const [logicalI1, logicalI2, logicalI3, logicalI4] = direction === 1 ? [i1, i2, i3, i4] : [i2, i1, i4, i3];
|
|
382
|
+
const firstPosition = positionAt(logicalI1);
|
|
383
|
+
const normal = cross3(subtract3(positionAt(logicalI2), firstPosition), subtract3(positionAt(logicalI3), firstPosition));
|
|
384
|
+
const normalLength = length3(normal);
|
|
385
|
+
if (normalLength <= linearTolerance * linearTolerance)
|
|
386
|
+
return direction === -1;
|
|
387
|
+
const signedHeight = direction * dot3(normal, subtract3(positionAt(logicalI4), firstPosition));
|
|
388
|
+
const planeTolerance = linearTolerance * normalLength;
|
|
389
|
+
// BOSL2 selects the locally concave diagonal for internal threads. Evaluate left-handed quads
|
|
390
|
+
// in their mirrored logical order, then map that logical diagonal back to our stored
|
|
391
|
+
// increasing-angle column order.
|
|
392
|
+
const logicalUsesSecondDiagonal = signedHeight <= planeTolerance;
|
|
393
|
+
return direction === 1 ? logicalUsesSecondDiagonal : !logicalUsesSecondDiagonal;
|
|
394
|
+
};
|
|
395
|
+
const profilesCorrespond = (firstOrdinal, secondOrdinal) => firstOrdinal !== undefined &&
|
|
396
|
+
secondOrdinal !== undefined &&
|
|
397
|
+
secondOrdinal === firstOrdinal + secondProfileOrdinalOffset;
|
|
398
|
+
let firstIndex = 0;
|
|
399
|
+
let secondIndex = 0;
|
|
400
|
+
while (firstIndex < first.indices.length - 1 || secondIndex < second.indices.length - 1) {
|
|
401
|
+
const nextFirst = first.z[firstIndex + 1] ?? Number.POSITIVE_INFINITY;
|
|
402
|
+
const nextSecond = second.z[secondIndex + 1] ?? Number.POSITIVE_INFINITY;
|
|
403
|
+
const a = first.indices[firstIndex];
|
|
404
|
+
const b = second.indices[secondIndex];
|
|
405
|
+
const currentProfilesCorrespond = profilesCorrespond(first.profileOrdinals[firstIndex], second.profileOrdinals[secondIndex]);
|
|
406
|
+
const nextProfilesCorrespond = profilesCorrespond(first.profileOrdinals[firstIndex + 1], second.profileOrdinals[secondIndex + 1]);
|
|
407
|
+
if ((currentProfilesCorrespond && nextProfilesCorrespond) ||
|
|
408
|
+
Math.abs(nextFirst - nextSecond) <= linearTolerance) {
|
|
409
|
+
const nextA = first.indices[firstIndex + 1];
|
|
410
|
+
const nextB = second.indices[secondIndex + 1];
|
|
411
|
+
const isLeadTipQuad = (first.leadTips[firstIndex] === true && first.leadTips[firstIndex + 1] === true) ||
|
|
412
|
+
(second.leadTips[secondIndex] === true && second.leadTips[secondIndex + 1] === true);
|
|
413
|
+
if (usesSecondDiagonal(a, b, nextB, nextA, isLeadTipQuad)) {
|
|
414
|
+
indices.push(a, b, nextA, b, nextB, nextA);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
indices.push(a, b, nextB, a, nextB, nextA);
|
|
418
|
+
}
|
|
419
|
+
firstIndex += 1;
|
|
420
|
+
secondIndex += 1;
|
|
421
|
+
}
|
|
422
|
+
else if (profilesCorrespond(first.profileOrdinals[firstIndex + 1], second.profileOrdinals[secondIndex]) ||
|
|
423
|
+
(!profilesCorrespond(first.profileOrdinals[firstIndex], second.profileOrdinals[secondIndex + 1]) &&
|
|
424
|
+
nextFirst < nextSecond)) {
|
|
425
|
+
indices.push(a, b, first.indices[firstIndex + 1]);
|
|
426
|
+
firstIndex += 1;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
indices.push(a, b, second.indices[secondIndex + 1]);
|
|
430
|
+
secondIndex += 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { GeometryError } from "../errors.js";
|
|
2
|
+
import { resolveCircularSegments, resolveGeometryResolution, } from "../resolution.js";
|
|
3
|
+
import { resolveGeometryTolerance } from "../tolerance.js";
|
|
4
|
+
import { compensateThreadProfileForFaceting } from "./thread-profiles.js";
|
|
5
|
+
const frozenFit = (maleRadialInset, femaleRadialOutset, axialBacklash) => Object.freeze({ axialBacklash, femaleRadialOutset, maleRadialInset });
|
|
6
|
+
/**
|
|
7
|
+
* Conservative baseline print-fit policies in millimetres.
|
|
8
|
+
*
|
|
9
|
+
* These are modeling defaults, not manufacturing guarantees. A caller should
|
|
10
|
+
* calibrate them for its printer, material, layer height, and thread scale.
|
|
11
|
+
*/
|
|
12
|
+
export const PRINTABLE_THREAD_FIT_POLICIES = Object.freeze({
|
|
13
|
+
/** Small baseline clearance for calibrated, accurate printers. */
|
|
14
|
+
tight: frozenFit(0.06, 0.06, 0.05),
|
|
15
|
+
/** General-purpose baseline clearance. */
|
|
16
|
+
normal: frozenFit(0.12, 0.12, 0.1),
|
|
17
|
+
/** Generous baseline clearance for rougher prints or easy assembly. */
|
|
18
|
+
loose: frozenFit(0.2, 0.2, 0.2),
|
|
19
|
+
});
|
|
20
|
+
const finite = (value, label) => {
|
|
21
|
+
if (!Number.isFinite(value)) {
|
|
22
|
+
throw new GeometryError("NON_FINITE_VALUE", `${label} must be finite.`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
const validateFitPolicy = (policy) => {
|
|
27
|
+
const male = finite(policy.maleRadialInset, "Male radial inset");
|
|
28
|
+
const female = finite(policy.femaleRadialOutset, "Female radial outset");
|
|
29
|
+
const axial = finite(policy.axialBacklash, "Axial backlash");
|
|
30
|
+
if (male < 0 || female < 0 || axial < 0) {
|
|
31
|
+
throw new GeometryError("INVALID_ARGUMENT", "Thread fit allowances must be non-negative.");
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({ axialBacklash: axial, femaleRadialOutset: female, maleRadialInset: male });
|
|
34
|
+
};
|
|
35
|
+
/** Resolves and validates a named or caller-defined print-fit policy. */
|
|
36
|
+
export function resolvePrintableThreadFitPolicy(fit = "normal") {
|
|
37
|
+
if (typeof fit !== "string")
|
|
38
|
+
return validateFitPolicy(fit);
|
|
39
|
+
const policy = PRINTABLE_THREAD_FIT_POLICIES[fit];
|
|
40
|
+
if (policy === undefined) {
|
|
41
|
+
throw new GeometryError("INVALID_ARGUMENT", `Unsupported printable thread fit: ${fit}.`);
|
|
42
|
+
}
|
|
43
|
+
return policy;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Applies role-specific radial compensation while preserving thread depth.
|
|
47
|
+
*
|
|
48
|
+
* Male envelopes move inward by the policy inset; female envelopes move
|
|
49
|
+
* outward by the policy outset. Combining both parts produces the sum of those
|
|
50
|
+
* allowances as radial mating clearance.
|
|
51
|
+
*/
|
|
52
|
+
export function compensateThreadDiameters(options) {
|
|
53
|
+
const nominalMajorDiameter = finite(options.nominalMajorDiameter, "Nominal major diameter");
|
|
54
|
+
const threadDepth = finite(options.threadDepth, "Thread depth");
|
|
55
|
+
const scale = Math.max(Math.abs(nominalMajorDiameter), Math.abs(threadDepth));
|
|
56
|
+
const tolerance = resolveGeometryTolerance(scale, options.tolerance);
|
|
57
|
+
if (!(nominalMajorDiameter > tolerance.linear) || !(threadDepth > tolerance.linear)) {
|
|
58
|
+
throw new GeometryError("INVALID_ARGUMENT", "Nominal major diameter and thread depth must be greater than tolerance.");
|
|
59
|
+
}
|
|
60
|
+
const nominalMinorDiameter = nominalMajorDiameter - threadDepth * 2;
|
|
61
|
+
if (!(nominalMinorDiameter > tolerance.linear)) {
|
|
62
|
+
throw new GeometryError("INVALID_ARGUMENT", "Thread depth consumes the nominal minor diameter.");
|
|
63
|
+
}
|
|
64
|
+
const policy = resolvePrintableThreadFitPolicy(options.fit);
|
|
65
|
+
if (options.role !== "male" && options.role !== "female") {
|
|
66
|
+
throw new GeometryError("INVALID_ARGUMENT", "Unsupported thread role.");
|
|
67
|
+
}
|
|
68
|
+
const radialShift = options.role === "male" ? -policy.maleRadialInset : policy.femaleRadialOutset;
|
|
69
|
+
const majorDiameter = nominalMajorDiameter + radialShift * 2;
|
|
70
|
+
const minorDiameter = nominalMinorDiameter + radialShift * 2;
|
|
71
|
+
if (!(minorDiameter > tolerance.linear) || !(majorDiameter > minorDiameter)) {
|
|
72
|
+
throw new GeometryError("DEGENERATE_GEOMETRY", "Thread compensation leaves invalid major or minor diameters.");
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
axialBacklash: policy.axialBacklash,
|
|
76
|
+
majorDiameter,
|
|
77
|
+
minorDiameter,
|
|
78
|
+
radialShift,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolves a role-independent, start-aligned facet count from the larger female mating envelope.
|
|
83
|
+
*
|
|
84
|
+
* An exact `circularSegments` value must already be divisible by `starts`; it is never silently
|
|
85
|
+
* raised. Adaptive policies align upward and fail if that would exceed `maximumSegments`.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveThreadCircularSegments(options) {
|
|
88
|
+
const operation = "resolveThreadCircularSegments";
|
|
89
|
+
const nominalMajorDiameter = finite(options.nominalMajorDiameter, "Nominal major diameter");
|
|
90
|
+
const maximumRadialOutset = finite(options.maximumRadialOutset ?? 0, "Maximum radial outset");
|
|
91
|
+
const pitch = finite(options.profile.pitch, "Profile pitch");
|
|
92
|
+
const starts = finite(options.starts ?? 1, "Starts");
|
|
93
|
+
if (!(nominalMajorDiameter > 0) ||
|
|
94
|
+
!(pitch > 0) ||
|
|
95
|
+
maximumRadialOutset < 0 ||
|
|
96
|
+
!Number.isInteger(starts) ||
|
|
97
|
+
starts < 1 ||
|
|
98
|
+
starts > 32) {
|
|
99
|
+
throw new GeometryError("INVALID_ARGUMENT", "Nominal diameter and pitch must be positive, maximum radial outset must be non-negative, and starts must be an integer from 1 through 32.", { operation });
|
|
100
|
+
}
|
|
101
|
+
const policy = resolvePrintableThreadFitPolicy(options.fit);
|
|
102
|
+
const resolution = resolveGeometryResolution(options.resolution, options.limits);
|
|
103
|
+
const femaleMajorDiameter = nominalMajorDiameter + policy.femaleRadialOutset * 2;
|
|
104
|
+
const maximumFemaleRadius = femaleMajorDiameter / 2 + maximumRadialOutset;
|
|
105
|
+
const effectiveRadius = Math.hypot(Math.PI * maximumFemaleRadius * 2, pitch * starts) / (Math.PI * 2);
|
|
106
|
+
const baseSegments = resolveCircularSegments(effectiveRadius, options.resolution, options.limits);
|
|
107
|
+
if (resolution.circularSegments !== undefined && baseSegments % starts !== 0) {
|
|
108
|
+
throw new GeometryError("INVALID_ARGUMENT", "Exact circularSegments must be divisible by the requested thread starts.", { operation, details: { circularSegments: baseSegments, starts } });
|
|
109
|
+
}
|
|
110
|
+
let segments = Math.ceil(baseSegments / starts) * starts;
|
|
111
|
+
if (segments > resolution.maximumSegments) {
|
|
112
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Start-aligned thread tessellation exceeds the configured segment safety limit.", { operation, details: { maximumSegments: resolution.maximumSegments, segments } });
|
|
113
|
+
}
|
|
114
|
+
// Physical circle error is only the lower bound for a mating thread grid. Independently
|
|
115
|
+
// triangulated male and female helices also need enough angular facets to distribute their full
|
|
116
|
+
// phase-span allowance without consuming either profile's crest or root flats. Adaptive policies
|
|
117
|
+
// therefore refine by complete start-aligned steps; an exact caller override remains exact and
|
|
118
|
+
// fails instead of being silently changed.
|
|
119
|
+
while (segments <= resolution.maximumSegments) {
|
|
120
|
+
try {
|
|
121
|
+
compensateThreadProfileForFaceting(options.profile, policy.axialBacklash, "male", starts, segments);
|
|
122
|
+
compensateThreadProfileForFaceting(options.profile, policy.axialBacklash, "female", starts, segments);
|
|
123
|
+
return segments;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (resolution.circularSegments !== undefined ||
|
|
127
|
+
!(error instanceof GeometryError) ||
|
|
128
|
+
error.code !== "COMPLEXITY_LIMIT") {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
segments += starts;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Mating thread faceting requires more segments than the configured safety limit.", {
|
|
135
|
+
operation,
|
|
136
|
+
details: { maximumSegments: resolution.maximumSegments, starts },
|
|
137
|
+
});
|
|
138
|
+
}
|