@code3d/screws 0.0.1-alpha.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/LICENSE +22 -0
- package/README.md +46 -0
- package/bld/library/index.d.ts +2 -0
- package/bld/library/index.d.ts.map +1 -0
- package/bld/library/index.js +2 -0
- package/bld/library/index.js.map +1 -0
- package/bld/library/iso-4762.d.ts +184 -0
- package/bld/library/iso-4762.d.ts.map +1 -0
- package/bld/library/iso-4762.js +219 -0
- package/bld/library/iso-4762.js.map +1 -0
- package/bld/library/thread.d.ts +10 -0
- package/bld/library/thread.d.ts.map +1 -0
- package/bld/library/thread.js +181 -0
- package/bld/library/thread.js.map +1 -0
- package/package.json +33 -0
- package/src/library/index.ts +1 -0
- package/src/library/iso-4762.ts +372 -0
- package/src/library/thread.ts +296 -0
- package/src/library/tsconfig.json +15 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
definePrimitive,
|
|
3
|
+
replicad,
|
|
4
|
+
type Drawing,
|
|
5
|
+
type Face,
|
|
6
|
+
type Shape3D,
|
|
7
|
+
type Sketch,
|
|
8
|
+
} from '@code3d/core/replicad';
|
|
9
|
+
|
|
10
|
+
const {Plane, draw, makeCylinder, makeHelix, makeSolid, weldShellsAndFaces} =
|
|
11
|
+
replicad;
|
|
12
|
+
|
|
13
|
+
type HelicalThreadOptions = Readonly<{
|
|
14
|
+
pitch: number;
|
|
15
|
+
y: number;
|
|
16
|
+
majorDiameter: number;
|
|
17
|
+
minorDiameter: number;
|
|
18
|
+
rootWidth: number;
|
|
19
|
+
crestWidth: number;
|
|
20
|
+
leftHanded?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
const loopSteps = Array.from({length: 21}, (_, index) => index / 20);
|
|
24
|
+
const threadBreps = new Map<string, string>();
|
|
25
|
+
const maximumCachedBrepCharacters = 8 * 1024 * 1024;
|
|
26
|
+
let cachedBrepCharacters = 0;
|
|
27
|
+
|
|
28
|
+
export const helicalThread = definePrimitive(
|
|
29
|
+
(options: HelicalThreadOptions) => {
|
|
30
|
+
validateHelicalThread(options);
|
|
31
|
+
const {
|
|
32
|
+
majorDiameter,
|
|
33
|
+
minorDiameter,
|
|
34
|
+
leftHanded = false,
|
|
35
|
+
...dimensions
|
|
36
|
+
} = options;
|
|
37
|
+
return cachedHelicalThreadShape({
|
|
38
|
+
...dimensions,
|
|
39
|
+
majorRadius: majorDiameter / 2,
|
|
40
|
+
minorRadius: minorDiameter / 2,
|
|
41
|
+
leftHanded,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
function validateHelicalThread({
|
|
47
|
+
pitch,
|
|
48
|
+
y,
|
|
49
|
+
majorDiameter,
|
|
50
|
+
minorDiameter,
|
|
51
|
+
rootWidth,
|
|
52
|
+
crestWidth,
|
|
53
|
+
}: HelicalThreadOptions): void {
|
|
54
|
+
assertPositive('pitch', pitch);
|
|
55
|
+
assertPositive('y', y);
|
|
56
|
+
assertPositive('majorDiameter', majorDiameter);
|
|
57
|
+
assertPositive('minorDiameter', minorDiameter);
|
|
58
|
+
assertPositive('rootWidth', rootWidth);
|
|
59
|
+
assertPositive('crestWidth', crestWidth);
|
|
60
|
+
if (y < pitch) {
|
|
61
|
+
throw new Error('y must be at least one thread pitch.');
|
|
62
|
+
}
|
|
63
|
+
if (minorDiameter >= majorDiameter) {
|
|
64
|
+
throw new Error('minorDiameter must be smaller than majorDiameter.');
|
|
65
|
+
}
|
|
66
|
+
if (crestWidth >= rootWidth || rootWidth > pitch) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'The thread profile requires crestWidth < rootWidth <= pitch.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type HelicalThreadShapeOptions = Readonly<{
|
|
74
|
+
pitch: number;
|
|
75
|
+
y: number;
|
|
76
|
+
majorRadius: number;
|
|
77
|
+
minorRadius: number;
|
|
78
|
+
rootWidth: number;
|
|
79
|
+
crestWidth: number;
|
|
80
|
+
leftHanded: boolean;
|
|
81
|
+
}>;
|
|
82
|
+
|
|
83
|
+
function cachedHelicalThreadShape(options: HelicalThreadShapeOptions): Shape3D {
|
|
84
|
+
const key = JSON.stringify([
|
|
85
|
+
options.pitch,
|
|
86
|
+
options.y,
|
|
87
|
+
options.majorRadius,
|
|
88
|
+
options.minorRadius,
|
|
89
|
+
options.rootWidth,
|
|
90
|
+
options.crestWidth,
|
|
91
|
+
options.leftHanded,
|
|
92
|
+
]);
|
|
93
|
+
let brep = threadBreps.get(key);
|
|
94
|
+
if (brep === undefined) {
|
|
95
|
+
const shape = makeHelicalThreadShape(options);
|
|
96
|
+
try {
|
|
97
|
+
brep = shape.serialize();
|
|
98
|
+
} finally {
|
|
99
|
+
shape.delete();
|
|
100
|
+
}
|
|
101
|
+
if (brep.length <= maximumCachedBrepCharacters) {
|
|
102
|
+
threadBreps.set(key, brep);
|
|
103
|
+
cachedBrepCharacters += brep.length;
|
|
104
|
+
while (cachedBrepCharacters > maximumCachedBrepCharacters) {
|
|
105
|
+
const [oldestKey, oldestBrep] = threadBreps.entries().next().value!;
|
|
106
|
+
threadBreps.delete(oldestKey);
|
|
107
|
+
cachedBrepCharacters -= oldestBrep.length;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
threadBreps.delete(key);
|
|
112
|
+
threadBreps.set(key, brep);
|
|
113
|
+
}
|
|
114
|
+
// Cache data, not native handles or disposable models. Each invocation owns
|
|
115
|
+
// a fresh shape in the current kernel. Reading both misses and hits also
|
|
116
|
+
// applies the same B-Rep normalization before core identifies the geometry.
|
|
117
|
+
return replicad.deserializeShape(brep) as Shape3D;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Builds a closed external thread around its minor-diameter core. The lofted
|
|
122
|
+
* end treatment follows the MIT-licensed replicad-threads construction by
|
|
123
|
+
* Steve Genoud, adapted here to code3d's Y-axis and geometry ownership model.
|
|
124
|
+
*/
|
|
125
|
+
function makeHelicalThreadShape({
|
|
126
|
+
pitch,
|
|
127
|
+
y,
|
|
128
|
+
majorRadius,
|
|
129
|
+
minorRadius,
|
|
130
|
+
rootWidth,
|
|
131
|
+
crestWidth,
|
|
132
|
+
leftHanded,
|
|
133
|
+
}: HelicalThreadShapeOptions): Shape3D {
|
|
134
|
+
const toothHeight = majorRadius - minorRadius;
|
|
135
|
+
const profile = threadProfile(rootWidth, crestWidth, toothHeight);
|
|
136
|
+
const teeth = fadedThread(pitch, minorRadius, y, profile, leftHanded);
|
|
137
|
+
const core = makeCylinder(minorRadius + 0.05, y);
|
|
138
|
+
const threaded = core.fuse(teeth);
|
|
139
|
+
core.delete();
|
|
140
|
+
teeth.delete();
|
|
141
|
+
return threaded.rotate(-90, [0, 0, 0], [1, 0, 0]).translate([0, -y / 2, 0]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function threadProfile(
|
|
145
|
+
rootWidth: number,
|
|
146
|
+
crestWidth: number,
|
|
147
|
+
toothHeight: number,
|
|
148
|
+
): Drawing {
|
|
149
|
+
const rootHalf = rootWidth / 2;
|
|
150
|
+
const crestHalf = crestWidth / 2;
|
|
151
|
+
return draw([rootHalf, -0.05])
|
|
152
|
+
.vLine(0.05)
|
|
153
|
+
.lineTo([crestHalf, toothHeight])
|
|
154
|
+
.hLineTo(-crestHalf)
|
|
155
|
+
.lineTo([-rootHalf, 0])
|
|
156
|
+
.vLine(-0.05)
|
|
157
|
+
.close();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fadedThread(
|
|
161
|
+
pitch: number,
|
|
162
|
+
radius: number,
|
|
163
|
+
length: number,
|
|
164
|
+
profile: Drawing,
|
|
165
|
+
leftHanded: boolean,
|
|
166
|
+
): Shape3D {
|
|
167
|
+
const bottomEnd = fadedEnd(pitch, radius, profile, true, leftHanded);
|
|
168
|
+
const totalLoops = length / pitch - 0.5;
|
|
169
|
+
const fullLoops = Math.floor(totalLoops);
|
|
170
|
+
const leftover = totalLoops - fullLoops;
|
|
171
|
+
const singleLoop = basicThreadLoop(
|
|
172
|
+
pitch,
|
|
173
|
+
radius,
|
|
174
|
+
profile,
|
|
175
|
+
1,
|
|
176
|
+
'none',
|
|
177
|
+
leftHanded,
|
|
178
|
+
);
|
|
179
|
+
const shells = Array.from({length: fullLoops}, (_, index) =>
|
|
180
|
+
singleLoop.clone().translate([0, 0, index * pitch]),
|
|
181
|
+
);
|
|
182
|
+
singleLoop.delete();
|
|
183
|
+
if (leftover > 1e-9) {
|
|
184
|
+
shells.push(
|
|
185
|
+
basicThreadLoop(
|
|
186
|
+
pitch,
|
|
187
|
+
radius,
|
|
188
|
+
profile,
|
|
189
|
+
leftover,
|
|
190
|
+
'none',
|
|
191
|
+
leftHanded,
|
|
192
|
+
).translate([0, 0, fullLoops * pitch]),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const topEnd = fadedEnd(pitch, radius, profile, false, leftHanded)
|
|
196
|
+
.translate([0, 0, totalLoops * pitch])
|
|
197
|
+
.rotate(360 * leftover, [0, 0, 0], [0, 0, 1]);
|
|
198
|
+
const thread = makeSolid([bottomEnd, ...shells, topEnd]).translate([
|
|
199
|
+
0,
|
|
200
|
+
0,
|
|
201
|
+
pitch / 4,
|
|
202
|
+
]);
|
|
203
|
+
bottomEnd.delete();
|
|
204
|
+
shells.forEach(shell => shell.delete());
|
|
205
|
+
topEnd.delete();
|
|
206
|
+
return thread;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function basicThreadLoop(
|
|
210
|
+
pitch: number,
|
|
211
|
+
radius: number,
|
|
212
|
+
profile: Drawing,
|
|
213
|
+
loopFraction: number,
|
|
214
|
+
includeEnd: 'none' | 'first' | 'last' | 'both',
|
|
215
|
+
leftHanded: boolean,
|
|
216
|
+
) {
|
|
217
|
+
const helix = makeHelix(
|
|
218
|
+
pitch,
|
|
219
|
+
pitch * clamp(loopFraction, 0, 1),
|
|
220
|
+
radius,
|
|
221
|
+
[0, 0, 0],
|
|
222
|
+
[0, 0, 1],
|
|
223
|
+
leftHanded,
|
|
224
|
+
);
|
|
225
|
+
const profiles = loopSteps.map(step => {
|
|
226
|
+
const position = helix.pointAt(step);
|
|
227
|
+
const tangent = helix.tangentAt(step);
|
|
228
|
+
const plane = new Plane(position, [0, 0, 1], tangent);
|
|
229
|
+
const sketch = profile.sketchOnPlane(plane) as Sketch;
|
|
230
|
+
plane.delete();
|
|
231
|
+
return sketch;
|
|
232
|
+
});
|
|
233
|
+
helix.delete();
|
|
234
|
+
|
|
235
|
+
const ends: Face[] = [];
|
|
236
|
+
if (includeEnd === 'first' || includeEnd === 'both') {
|
|
237
|
+
ends.push(profiles[0].faces());
|
|
238
|
+
}
|
|
239
|
+
if (includeEnd === 'last' || includeEnd === 'both') {
|
|
240
|
+
const face = profiles.at(-1)!.faces();
|
|
241
|
+
face.wrapped.Reverse();
|
|
242
|
+
ends.push(face);
|
|
243
|
+
}
|
|
244
|
+
const shell = profiles[0].loftWith(profiles.slice(1), {ruled: false}, true);
|
|
245
|
+
if (ends.length === 0) return shell;
|
|
246
|
+
const closed = weldShellsAndFaces([shell, ...ends]);
|
|
247
|
+
shell.delete();
|
|
248
|
+
ends.forEach(face => face.delete());
|
|
249
|
+
return closed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function fadedEnd(
|
|
253
|
+
pitch: number,
|
|
254
|
+
radius: number,
|
|
255
|
+
profile: Drawing,
|
|
256
|
+
bottom: boolean,
|
|
257
|
+
leftHanded: boolean,
|
|
258
|
+
) {
|
|
259
|
+
const length = pitch / 4;
|
|
260
|
+
const helix = makeHelix(
|
|
261
|
+
pitch,
|
|
262
|
+
length,
|
|
263
|
+
radius,
|
|
264
|
+
[0, 0, 0],
|
|
265
|
+
[0, 0, bottom ? -1 : 1],
|
|
266
|
+
leftHanded,
|
|
267
|
+
);
|
|
268
|
+
const profiles = loopSteps.map((step, index) => {
|
|
269
|
+
const position = helix.pointAt(step);
|
|
270
|
+
let tangent = helix.tangentAt(step);
|
|
271
|
+
if (bottom) tangent = tangent.multiply(-1);
|
|
272
|
+
const plane = new Plane(position, [0, 0, 1], tangent);
|
|
273
|
+
const sketch = profile
|
|
274
|
+
.scale((loopSteps.length - index) / loopSteps.length, [0, 0])
|
|
275
|
+
.sketchOnPlane(plane) as Sketch;
|
|
276
|
+
plane.delete();
|
|
277
|
+
return sketch;
|
|
278
|
+
});
|
|
279
|
+
helix.delete();
|
|
280
|
+
const endFace = profiles.at(-1)!.faces();
|
|
281
|
+
const shell = profiles[0].loftWith(profiles.slice(1), {ruled: false}, true);
|
|
282
|
+
const closed = weldShellsAndFaces([shell, endFace]);
|
|
283
|
+
shell.delete();
|
|
284
|
+
endFace.delete();
|
|
285
|
+
return bottom ? closed.rotate(180, [0, 0, 0], [0, 0, 1]) : closed;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function assertPositive(name: string, value: number): void {
|
|
289
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
290
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
295
|
+
return Math.min(Math.max(value, minimum), maximum);
|
|
296
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"rootDir": ".",
|
|
6
|
+
"outDir": "../../bld/library",
|
|
7
|
+
"tsBuildInfoFile": "../../.cache/library.tsbuildinfo",
|
|
8
|
+
"declarationMap": true
|
|
9
|
+
},
|
|
10
|
+
"references": [
|
|
11
|
+
{
|
|
12
|
+
"path": "../../../core/src/library"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|