@faicad/fai-cq-warehouse 0.13.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/dist/bearing.d.ts +81 -0
- package/dist/bearing.d.ts.map +1 -0
- package/dist/bearing.js +462 -0
- package/dist/bearing.js.map +1 -0
- package/dist/chain.d.ts +96 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +322 -0
- package/dist/chain.js.map +1 -0
- package/dist/contract.d.ts +9 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +10 -0
- package/dist/contract.js.map +1 -0
- package/dist/data/brad_tee_nut_parameters.json +29 -0
- package/dist/data/button_head_parameters.json +82 -0
- package/dist/data/button_head_with_collar_parameters.json +98 -0
- package/dist/data/chamfered_washer_parameters.json +262 -0
- package/dist/data/cheese_head_parameters.json +182 -0
- package/dist/data/cheese_head_washer_parameters.json +112 -0
- package/dist/data/clearance_hole_sizes.json +427 -0
- package/dist/data/countersunk_head_parameters.json +1752 -0
- package/dist/data/domed_cap_nut_parameters.json +62 -0
- package/dist/data/drill_sizes.json +320 -0
- package/dist/data/heatset_nut_parameters.json +354 -0
- package/dist/data/hex_head_parameters.json +408 -0
- package/dist/data/hex_head_with_flange_parameters.json +114 -0
- package/dist/data/hex_nut_parameters.json +250 -0
- package/dist/data/hex_nut_with_flange_parameters.json +92 -0
- package/dist/data/imperial_set_screw_parameters.json +150 -0
- package/dist/data/iso10664def.json +82 -0
- package/dist/data/metric_set_screw_parameters.json +50 -0
- package/dist/data/nominal_screw_lengths.json +106 -0
- package/dist/data/pan_head_parameters.json +322 -0
- package/dist/data/pan_head_with_collar_parameters.json +56 -0
- package/dist/data/plain_washer_parameters.json +464 -0
- package/dist/data/raised_cheese_head_parameters.json +82 -0
- package/dist/data/raised_countersunk_oval_head_parameters.json +314 -0
- package/dist/data/setscrew_parameters.json +110 -0
- package/dist/data/single_row_angular_contact_ball_bearing_parameters.json +209 -0
- package/dist/data/single_row_capped_deep_groove_ball_bearing_parameters.json +527 -0
- package/dist/data/single_row_cylindrical_roller_bearing_parameters.json +227 -0
- package/dist/data/single_row_deep_groove_ball_bearing_parameters.json +653 -0
- package/dist/data/single_row_tapered_roller_bearing_parameters.json +756 -0
- package/dist/data/socket_head_cap_parameters.json +856 -0
- package/dist/data/square_nut_parameters.json +26 -0
- package/dist/data/tap_hole_sizes.json +398 -0
- package/dist/data/tapered-fillet-drop.json +109 -0
- package/dist/data/unchamfered_hex_nut_parameters.json +42 -0
- package/dist/holes.d.ts +186 -0
- package/dist/holes.d.ts.map +1 -0
- package/dist/holes.js +197 -0
- package/dist/holes.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel.d.ts +117 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +25 -0
- package/dist/kernel.js.map +1 -0
- package/dist/measure.d.ts +52 -0
- package/dist/measure.d.ts.map +1 -0
- package/dist/measure.js +158 -0
- package/dist/measure.js.map +1 -0
- package/dist/nut.d.ts +315 -0
- package/dist/nut.d.ts.map +1 -0
- package/dist/nut.js +700 -0
- package/dist/nut.js.map +1 -0
- package/dist/params.d.ts +178 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/params.js +330 -0
- package/dist/params.js.map +1 -0
- package/dist/primitives.d.ts +440 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +597 -0
- package/dist/primitives.js.map +1 -0
- package/dist/recess.d.ts +129 -0
- package/dist/recess.d.ts.map +1 -0
- package/dist/recess.js +323 -0
- package/dist/recess.js.map +1 -0
- package/dist/screw.d.ts +101 -0
- package/dist/screw.d.ts.map +1 -0
- package/dist/screw.js +945 -0
- package/dist/screw.js.map +1 -0
- package/dist/sprocket.d.ts +135 -0
- package/dist/sprocket.d.ts.map +1 -0
- package/dist/sprocket.js +261 -0
- package/dist/sprocket.js.map +1 -0
- package/dist/test-setup.d.ts +11 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +30 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/thread.d.ts +189 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +501 -0
- package/dist/thread.js.map +1 -0
- package/dist/washer.d.ts +126 -0
- package/dist/washer.d.ts.map +1 -0
- package/dist/washer.js +204 -0
- package/dist/washer.js.map +1 -0
- package/package.json +55 -0
package/dist/nut.js
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nut.ts — 螺母七类移植(上游 `cq_warehouse/fastener.py:365-1240`,方案 §8 W4)。
|
|
3
|
+
*
|
|
4
|
+
* 类映射(方案 §4.2):`Nut` 基类 → {@link buildNut};具体类 → `hexNut()` /
|
|
5
|
+
* `hexNutWithFlange()` / `unchamferedHexagonNut()` / `squareNut()` / `domedCapNut()` /
|
|
6
|
+
* `bradTeeNut()` / `heatSetNut()`;轮廓与 plan → `nutProfile()` / `nutPlanWire()`
|
|
7
|
+
* (纯几何,不建 kernel 对象,便于单测与复用)。
|
|
8
|
+
*
|
|
9
|
+
* ## `make_nut` 的四步映射(fastener.py:581-635)
|
|
10
|
+
*
|
|
11
|
+
* | 上游 | 本包 |
|
|
12
|
+
* |---|---|
|
|
13
|
+
* | `nut_profile().toPending().revolve()` | 轮廓段列 → wire → {@link primitives.revolveProfile} |
|
|
14
|
+
* | `Workplane("XY").add(nut_plan()).toPending().extrude(max_nut_height)` | plan wire → {@link primitives.planarFace} → {@link primitives.extrudeFace} |
|
|
15
|
+
* | `.faces("<Z").workplane().hole(d, depth)` | {@link primitives.cylinderBetween} 后 `cut` |
|
|
16
|
+
* | `nut.intersect(blank)` / `.union(flange)` / `.union(thread)` | `intersect` / `fuse` |
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ **plan 必须先成 face 再拉伸**:probe 实测(`scripts/kernel-nut-probe.ts` 段 1)
|
|
19
|
+
* `extrude(wire,0,0,3)` 对 4×2 矩形给体积 −16(错),传 face 才得 24(对)。内核不校验
|
|
20
|
+
* 输入拓扑——传错不抛错,只静默给错几何。
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ **`max_nut_height` 取轮廓最高顶点 z**(上游 `profile.vertices(">Z").val().Z`),
|
|
23
|
+
* 不是 `nut_data["m"]`:DomedCapNut 的球顶使之为 `m + dk/2`。
|
|
24
|
+
*
|
|
25
|
+
* ## cq 语义的逐条复现(读 cadquery `cq.py` 源码确认,证据见 W4 分析文档)
|
|
26
|
+
*
|
|
27
|
+
* - `polygon(nSides, diameter)`:顶点自角度 **0** 起(首顶点在 `(r, 0)`),半径 = `diameter/2`
|
|
28
|
+
* (`cq.py:2677-2688`)。
|
|
29
|
+
* - `rect(w, h, centered=False)`:轮廓为 `(0,0) → (w,0) → (w,h) → (0,h)`。
|
|
30
|
+
* - `radiusArc(end, radius)` → `sagittaArc`(`cq.py:2154-2188`):`sag = |r| − √(r²−half²)`,
|
|
31
|
+
* 符号随 radius;`sagittaArc` 把 sag 沿**弦法向旋转 ±90°**得第三点(`cq.py:2139-2150`)。
|
|
32
|
+
* - XZ 工作平面的局部 `(u, v)` → 世界 `(u, 0, v)`(局部 x 沿世界 X,局部 y 沿世界 Z)。
|
|
33
|
+
*
|
|
34
|
+
* ## 未实现(W4 内的已知缺口,显式抛错而非静默降级)
|
|
35
|
+
*
|
|
36
|
+
* `HeatSetNut` 的 `make_nut`(fastener.py:947)不走向轮廓旋转,而是
|
|
37
|
+
* `Face.makeNSidedSurface` 造 knurl 面 + `Shell.makeShell` + `Solid.makeSolid`。
|
|
38
|
+
* 本内核无 `makeNSidedSurface`;probe(`scripts/kernel-nut-probe.ts` 段 4)实测
|
|
39
|
+
* `makeNonPlanarFace(4 边扭面 wire)` 退化为 **3 边面**(面积 2.749170),几何不等价
|
|
40
|
+
* ——不构成可接受的替代。详见 `docs/analysis/2026-09-14-cq-warehouse-nut-washer-probe.md`。
|
|
41
|
+
*
|
|
42
|
+
* `BradTeeNut` **已实现**:其 `custom_make` 需要的 `extensions.clearanceHole` 由
|
|
43
|
+
* `src/recess.ts` 的 **W4 临时**等价实现承担(按方案 §8-W4 去重表,W9·P1-b 落地后替换)。
|
|
44
|
+
*/
|
|
45
|
+
import { clearanceHoleData, isolateFastenerType, tapHoleData, typesOf, NUT_TABLES, SCREW_TABLES, } from './params.js';
|
|
46
|
+
import { imperialStrToFloat, INCH } from './measure.js';
|
|
47
|
+
import { arcEdge, bboxDiagonal, bboxOf, circleWireXY, cut, cylinderBetween, extrudeFace, fuse, intersect, lineEdge, planarFace, polygonWire, revolveProfile, translate, wireFromEdges, } from './primitives.js';
|
|
48
|
+
import { tempCounterSunkCountersinkProfile, } from './recess.js';
|
|
49
|
+
import { fastenerHoleCutter } from './holes.js';
|
|
50
|
+
import { isoThread } from './thread.js';
|
|
51
|
+
/** 旋转轴(+Z)。 */
|
|
52
|
+
const AXIS_Z = { point: { x: 0, y: 0, z: 0 }, direction: { x: 0, y: 0, z: 1 } };
|
|
53
|
+
/** 上游 `Nut.socket_clearance`(fastener.py:554)= 6 mm。 */
|
|
54
|
+
export const SOCKET_CLEARANCE = 6;
|
|
55
|
+
// ── cq 轮廓数学(读 cq.py 源码逐条复现)────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* `fastener.py:54 polygon_diagonal` —— 对边距 → 对角(外接圆)直径。
|
|
58
|
+
* @param width - 对边距(正多边形两条平行边之间的距离)。
|
|
59
|
+
* @param numSides - 边数(默认 6,即六角螺母)。
|
|
60
|
+
* @returns 外接圆直径 `width / cos(π / numSides)`。
|
|
61
|
+
*/
|
|
62
|
+
export function polygonDiagonal(width, numSides = 6) {
|
|
63
|
+
return width / Math.cos(Math.PI / numSides);
|
|
64
|
+
}
|
|
65
|
+
/** `cq.py:2139-2150 sagittaArc` 的第三点(弧中点)。 */
|
|
66
|
+
function sagittaMidpoint(start, end, sag) {
|
|
67
|
+
const dr = end.r - start.r;
|
|
68
|
+
const dz = end.z - start.z;
|
|
69
|
+
const len = Math.hypot(dr, dz);
|
|
70
|
+
if (len === 0)
|
|
71
|
+
return { r: start.r, z: start.z };
|
|
72
|
+
const ur = dr / len;
|
|
73
|
+
const uz = dz / len;
|
|
74
|
+
// cq 在局部 (x, y) 平面内旋转 ±90°:(x,y) → (−y,x)(sag>0)或 (y,−x)(sag<0);
|
|
75
|
+
// 局部 y 对应世界 z。
|
|
76
|
+
const sr = (sag > 0 ? -uz : uz) * Math.abs(sag);
|
|
77
|
+
const sz = (sag > 0 ? ur : -ur) * Math.abs(sag);
|
|
78
|
+
return { r: (start.r + end.r) / 2 + sr, z: (start.z + end.z) / 2 + sz };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `cq.py:2154-2188 radiusArc` → `sagittaArc` 的弧中点。
|
|
82
|
+
* @param start - 弧起点(轮廓当前点)。
|
|
83
|
+
* @param end - 弧终点。
|
|
84
|
+
* @param radius - 弧半径(正=凸、负=凹,语义同 cq)。
|
|
85
|
+
* @returns 弧上一点(供三点弧 `makeArcEdge`)。
|
|
86
|
+
*/
|
|
87
|
+
export function radiusArcMidpoint(start, end, radius) {
|
|
88
|
+
const half = Math.hypot(end.r - start.r, end.z - start.z) / 2;
|
|
89
|
+
const r2l2 = radius * radius - half * half;
|
|
90
|
+
if (Math.abs(r2l2) < 1e-12)
|
|
91
|
+
return sagittaMidpoint(start, end, radius > 0 ? Math.abs(radius) : -Math.abs(radius));
|
|
92
|
+
if (r2l2 < 0)
|
|
93
|
+
throw new Error(`Nut: arc radius ${radius} is not large enough to reach the end point (chord half-length ${half})`);
|
|
94
|
+
const sag = Math.abs(radius) - Math.sqrt(r2l2);
|
|
95
|
+
return sagittaMidpoint(start, end, radius > 0 ? sag : -sag);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 轮廓的点列(起点 + 各段终点),用于取 `max_nut_height` 等。
|
|
99
|
+
* @param p - 轮廓({@link ProfileBuilder} 链式构造的产物)。
|
|
100
|
+
* @returns 顶点序列,首元素为轮廓起点。
|
|
101
|
+
*/
|
|
102
|
+
export function profilePoints(p) {
|
|
103
|
+
return [p.start, ...p.segments.map((s) => s.to)];
|
|
104
|
+
}
|
|
105
|
+
/** 链式轮廓构造器(方法名对齐 cq,`hLine`/`vLine` 为**相对**位移,`*To` 为绝对)。 */
|
|
106
|
+
class ProfileBuilder {
|
|
107
|
+
startPt;
|
|
108
|
+
segs = [];
|
|
109
|
+
cur;
|
|
110
|
+
constructor(start) {
|
|
111
|
+
this.startPt = start;
|
|
112
|
+
this.cur = start;
|
|
113
|
+
}
|
|
114
|
+
/** `lineTo(r, z)` —— 绝对值。 */
|
|
115
|
+
lineTo(r, z) {
|
|
116
|
+
this.segs.push({ kind: 'line', to: { r, z } });
|
|
117
|
+
this.cur = { r, z };
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
/** `hLineTo(r)` —— 水平线到绝对 `r`(保持当前 `z`)。 */
|
|
121
|
+
hLineTo(r) {
|
|
122
|
+
return this.lineTo(r, this.cur.z);
|
|
123
|
+
}
|
|
124
|
+
/** `vLineTo(z)` —— 垂直线到绝对 `z`(保持当前 `r`)。 */
|
|
125
|
+
vLineTo(z) {
|
|
126
|
+
return this.lineTo(this.cur.r, z);
|
|
127
|
+
}
|
|
128
|
+
/** `hLine(dr)` —— 水平**相对**位移。 */
|
|
129
|
+
hLine(dr) {
|
|
130
|
+
return this.lineTo(this.cur.r + dr, this.cur.z);
|
|
131
|
+
}
|
|
132
|
+
/** `vLine(dz)` —— 垂直**相对**位移。 */
|
|
133
|
+
vLine(dz) {
|
|
134
|
+
return this.lineTo(this.cur.r, this.cur.z + dz);
|
|
135
|
+
}
|
|
136
|
+
/** `radiusArc(end, radius)` —— 圆弧段。 */
|
|
137
|
+
arcTo(r, z, radius) {
|
|
138
|
+
this.segs.push({ kind: 'radiusArc', to: { r, z }, radius });
|
|
139
|
+
this.cur = { r, z };
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
/** `polarLine(distance, angleDeg)` —— 按极角走给定距离(cq `cq.py polarLine`)。 */
|
|
143
|
+
polarLine(distance, angleDeg) {
|
|
144
|
+
const a = (angleDeg * Math.PI) / 180;
|
|
145
|
+
return this.lineTo(this.cur.r + distance * Math.cos(a), this.cur.z + distance * Math.sin(a));
|
|
146
|
+
}
|
|
147
|
+
/** 收尾(`close()` 的直线边由 {@link profileWire} 补)。 */
|
|
148
|
+
build() {
|
|
149
|
+
return { start: this.startPt, segments: [...this.segs] };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function toWorld(p) {
|
|
153
|
+
return { x: p.r, y: 0, z: p.z };
|
|
154
|
+
}
|
|
155
|
+
/** 轮廓段列(从 `start` 起、闭合回 `start`)→ XZ 平面闭合 wire。 */
|
|
156
|
+
function profileWire(p) {
|
|
157
|
+
const edges = [];
|
|
158
|
+
let cur = p.start;
|
|
159
|
+
for (const seg of p.segments) {
|
|
160
|
+
if (seg.kind === 'line')
|
|
161
|
+
edges.push(lineEdge(toWorld(cur), toWorld(seg.to)));
|
|
162
|
+
else {
|
|
163
|
+
const mid = radiusArcMidpoint(cur, seg.to, seg.radius);
|
|
164
|
+
edges.push(arcEdge(toWorld(cur), toWorld(mid), toWorld(seg.to)));
|
|
165
|
+
}
|
|
166
|
+
cur = seg.to;
|
|
167
|
+
}
|
|
168
|
+
if (cur.r !== p.start.r || cur.z !== p.start.z)
|
|
169
|
+
edges.push(lineEdge(toWorld(cur), toWorld(p.start)));
|
|
170
|
+
return wireFromEdges(edges);
|
|
171
|
+
}
|
|
172
|
+
function num(row, key, where) {
|
|
173
|
+
const v = row[key];
|
|
174
|
+
if (typeof v !== 'number')
|
|
175
|
+
throw new Error(`${where}: nut_data["${key}"] is not numeric (got ${JSON.stringify(v)})`);
|
|
176
|
+
return v;
|
|
177
|
+
}
|
|
178
|
+
function requireMS(row, where) {
|
|
179
|
+
return { m: num(row, 'm', where), s: num(row, 's', where) };
|
|
180
|
+
}
|
|
181
|
+
const DEG15 = (15 * Math.PI) / 180;
|
|
182
|
+
// ── 轮廓(上游各 `nut_profile`)─────────────────────────────────────────────
|
|
183
|
+
/**
|
|
184
|
+
* `Nut.default_nut_profile`(fastener.py:637)—— 六角双倒角轮廓(HexNut / HexNutWithFlange 用)。
|
|
185
|
+
* @param data - 该规格的参数行(含 m/s)。
|
|
186
|
+
* @returns 轮廓(起点在轴上 `(0,0)`)。
|
|
187
|
+
*/
|
|
188
|
+
export function defaultNutProfile(data) {
|
|
189
|
+
const { m, s } = requireMS(data, 'Nut');
|
|
190
|
+
const e = polygonDiagonal(s, 6);
|
|
191
|
+
const cs = ((e - s) * Math.tan(DEG15)) / 2;
|
|
192
|
+
return new ProfileBuilder({ r: 0, z: 0 })
|
|
193
|
+
.hLineTo(s / 2)
|
|
194
|
+
.lineTo(e / 2 - 0.001, cs)
|
|
195
|
+
.vLineTo(m - cs)
|
|
196
|
+
.lineTo(s / 2, m)
|
|
197
|
+
.hLineTo(0)
|
|
198
|
+
.build();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* `DomedCapNut.nut_profile`(fastener.py:685)—— 六角 + 球顶(含两段 `radiusArc`)。
|
|
202
|
+
*
|
|
203
|
+
* ⚠️ 起点为 `(1, 0)`(上游 `moveTo(1*MM, 0)`)而非轴上:轮廓留下 r∈[0,1] 的内孔,
|
|
204
|
+
* 最终由 `nut_blank` 的螺纹孔覆盖(对 M≥4 无影响)。
|
|
205
|
+
* @param data - 该规格的参数行(含 dk/m/s)。
|
|
206
|
+
* @returns 轮廓。
|
|
207
|
+
*/
|
|
208
|
+
export function domedCapNutProfile(data) {
|
|
209
|
+
const { m, s } = requireMS(data, 'DomedCapNut');
|
|
210
|
+
const dk = num(data, 'dk', 'DomedCapNut');
|
|
211
|
+
const e = polygonDiagonal(s, 6);
|
|
212
|
+
const cs = ((e - s) * Math.tan(DEG15)) / 2;
|
|
213
|
+
return new ProfileBuilder({ r: 1, z: 0 })
|
|
214
|
+
.hLineTo(s / 2)
|
|
215
|
+
.lineTo(e / 2, cs)
|
|
216
|
+
.vLineTo(m - cs)
|
|
217
|
+
.lineTo(s / 2, m)
|
|
218
|
+
.hLineTo(dk / 2)
|
|
219
|
+
.arcTo(0, m + dk / 2, -dk / 2)
|
|
220
|
+
.vLineTo(m + dk / 2 - 1)
|
|
221
|
+
.arcTo(dk / 2 - 1, m, dk / 2 - 1)
|
|
222
|
+
.hLineTo(1)
|
|
223
|
+
.build();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* `UnchamferedHexagonNut.nut_profile`(fastener.py:1189)—— 无倒角矩形(六角对边距)。
|
|
227
|
+
* @param data - 该规格的参数行(含 m/s)。
|
|
228
|
+
* @returns 轮廓。
|
|
229
|
+
*/
|
|
230
|
+
export function unchamferedHexagonNutProfile(data) {
|
|
231
|
+
const { m, s } = requireMS(data, 'UnchamferedHexagonNut');
|
|
232
|
+
const w = polygonDiagonal(s, 6) / 2 - 0.001;
|
|
233
|
+
return new ProfileBuilder({ r: 0, z: 0 })
|
|
234
|
+
.hLineTo(w)
|
|
235
|
+
.vLineTo(m)
|
|
236
|
+
.hLineTo(0)
|
|
237
|
+
.build();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* `SquareNut.nut_profile`(fastener.py:1211)—— 四方双倒角(`polygon_diagonal(s, 4)`)。
|
|
241
|
+
* @param data - 该规格的参数行(含 m/s)。
|
|
242
|
+
* @returns 轮廓。
|
|
243
|
+
*/
|
|
244
|
+
export function squareNutProfile(data) {
|
|
245
|
+
const { m, s } = requireMS(data, 'SquareNut');
|
|
246
|
+
const e = polygonDiagonal(s, 4);
|
|
247
|
+
const cs = ((e - s) * Math.tan(DEG15)) / 2;
|
|
248
|
+
return new ProfileBuilder({ r: 0, z: 0 })
|
|
249
|
+
.hLineTo(e / 2 - 0.001)
|
|
250
|
+
.vLineTo(m - cs)
|
|
251
|
+
.lineTo(s / 2, m)
|
|
252
|
+
.hLineTo(0)
|
|
253
|
+
.build();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* `BradTeeNut.nut_profile`(fastener.py:751)—— 法兰盘 + 台肩(**相对**位移 `vLine`/`hLine`)。
|
|
257
|
+
* @param data - 该规格的参数行(含 c/dc/m/s)。
|
|
258
|
+
* @returns 轮廓(点序:`(0,0) → (0,m) → (dc/2,m) → (dc/2,m−c) → (s,m−c) → (s,0) → (0,0)`)。
|
|
259
|
+
*/
|
|
260
|
+
export function bradTeeNutProfile(data) {
|
|
261
|
+
const m = num(data, 'm', 'BradTeeNut');
|
|
262
|
+
const s = num(data, 's', 'BradTeeNut');
|
|
263
|
+
const dc = num(data, 'dc', 'BradTeeNut');
|
|
264
|
+
const c = num(data, 'c', 'BradTeeNut');
|
|
265
|
+
return new ProfileBuilder({ r: 0, z: 0 })
|
|
266
|
+
.vLine(m)
|
|
267
|
+
.hLine(dc / 2)
|
|
268
|
+
.vLine(-c)
|
|
269
|
+
.hLineTo(s)
|
|
270
|
+
.vLineTo(0)
|
|
271
|
+
.hLineTo(0)
|
|
272
|
+
.build();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* `HexNutWithFlange.flange_profile`(fastener.py:1139)—— 25° 过切线 + `radiusArc` + `polarLine`。
|
|
276
|
+
* @param data - 该规格的参数行(含 c/dc)。
|
|
277
|
+
* @returns 轮廓(起点在轴上 `(0,0)`)。
|
|
278
|
+
*/
|
|
279
|
+
export function hexNutFlangeProfile(data) {
|
|
280
|
+
const dc = num(data, 'dc', 'HexNutWithFlange');
|
|
281
|
+
const c = num(data, 'c', 'HexNutWithFlange');
|
|
282
|
+
const flangeAngle = 25;
|
|
283
|
+
// tangent_point = (c/2)·(cos(90−fa), sin(90−fa)) + ((dc−c)/2, c/2)
|
|
284
|
+
const rad = ((90 - flangeAngle) * Math.PI) / 180;
|
|
285
|
+
const tangentR = (c / 2) * Math.cos(rad) + (dc - c) / 2;
|
|
286
|
+
const tangentZ = (c / 2) * Math.sin(rad) + c / 2;
|
|
287
|
+
return new ProfileBuilder({ r: 0, z: 0 })
|
|
288
|
+
.hLineTo(dc / 2 - c / 2)
|
|
289
|
+
.arcTo(tangentR, tangentZ, -c / 2)
|
|
290
|
+
.polarLine(dc / 2 - c / 2, 180 - flangeAngle)
|
|
291
|
+
.hLineTo(0)
|
|
292
|
+
.build();
|
|
293
|
+
}
|
|
294
|
+
// ── plan(上游各 `nut_plan`)──────────────────────────────────────────────
|
|
295
|
+
/**
|
|
296
|
+
* 正多边形顶点(`cq polygon(nSides, diameter)` 等价:首顶点在 `(r, 0)`)。
|
|
297
|
+
* @param diameter - 外接圆直径(mm)。
|
|
298
|
+
* @param nSides - 边数。
|
|
299
|
+
* @returns XY 平面顶点列(z=0)。
|
|
300
|
+
*/
|
|
301
|
+
export function polygonPlanPoints(diameter, nSides) {
|
|
302
|
+
const radius = diameter / 2;
|
|
303
|
+
const pts = [];
|
|
304
|
+
for (let i = 0; i < nSides; i++) {
|
|
305
|
+
const o = ((2 * Math.PI) / nSides) * i;
|
|
306
|
+
pts.push({ x: radius * Math.cos(o), y: radius * Math.sin(o), z: 0 });
|
|
307
|
+
}
|
|
308
|
+
return pts;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* `Nut.default_nut_plan`(fastener.py:659)—— 正六边形。
|
|
312
|
+
* @param data - 该规格的参数行(含 s)。
|
|
313
|
+
* @returns XY 平面六边形 wire。
|
|
314
|
+
*/
|
|
315
|
+
export function defaultNutPlanWire(data) {
|
|
316
|
+
return polygonWire(polygonPlanPoints(polygonDiagonal(num(data, 's', 'Nut'), 6), 6));
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* `SquareNut.nut_plan`(fastener.py:1227)—— 边长 s 的正方形(`rect(s, s)`,居中)。
|
|
320
|
+
* @param data - 该规格的参数行(含 s)。
|
|
321
|
+
* @returns XY 平面正方形 wire。
|
|
322
|
+
*/
|
|
323
|
+
export function squareNutPlanWire(data) {
|
|
324
|
+
const h = num(data, 's', 'SquareNut') / 2;
|
|
325
|
+
return polygonWire([
|
|
326
|
+
{ x: -h, y: -h, z: 0 },
|
|
327
|
+
{ x: h, y: -h, z: 0 },
|
|
328
|
+
{ x: h, y: h, z: 0 },
|
|
329
|
+
{ x: -h, y: h, z: 0 },
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* `BradTeeNut.nut_plan`(fastener.py:764)—— 直径 dc 的圆。
|
|
334
|
+
* @param data - 该规格的参数行(含 dc)。
|
|
335
|
+
* @returns XY 平面圆 wire。
|
|
336
|
+
*/
|
|
337
|
+
export function bradTeeNutPlanWire(data) {
|
|
338
|
+
return circleWireXY(num(data, 'dc', 'BradTeeNut') / 2);
|
|
339
|
+
}
|
|
340
|
+
// ── 沉孔切割器轮廓(上游各 `countersink_profile`)───────────────────────────
|
|
341
|
+
/** 矩形切割器轮廓(`rect(width/2, height, centered=False)`)。 */
|
|
342
|
+
function rectProfile(width, height) {
|
|
343
|
+
return new ProfileBuilder({ r: 0, z: 0 })
|
|
344
|
+
.hLineTo(width)
|
|
345
|
+
.vLineTo(height)
|
|
346
|
+
.hLineTo(0)
|
|
347
|
+
.build();
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* `Nut.default_countersink_profile`(fastener.py:664)—— 含 `socket_clearance` 的矩形环。
|
|
351
|
+
* @param data - 该规格的参数行(含 m/s)。
|
|
352
|
+
* @returns 切割器轮廓。
|
|
353
|
+
*/
|
|
354
|
+
export function defaultNutCountersinkProfile(data) {
|
|
355
|
+
const { m, s } = requireMS(data, 'Nut');
|
|
356
|
+
const width = polygonDiagonal(s, 6) + SOCKET_CLEARANCE;
|
|
357
|
+
return rectProfile(width / 2, m);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* `SquareNut.countersink_profile`(fastener.py:1231)—— 四方版。
|
|
361
|
+
* @param data - 该规格的参数行(含 m/s)。
|
|
362
|
+
* @returns 切割器轮廓。
|
|
363
|
+
*/
|
|
364
|
+
export function squareNutCountersinkProfile(data) {
|
|
365
|
+
const { m, s } = requireMS(data, 'SquareNut');
|
|
366
|
+
const width = polygonDiagonal(s, 4) + SOCKET_CLEARANCE;
|
|
367
|
+
return rectProfile(width / 2, m);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* `DomedCapNut.countersink_profile`(fastener.py:707)—— 高度含球顶 `dk/2`。
|
|
371
|
+
* @param data - 该规格的参数行(含 dk/m/s)。
|
|
372
|
+
* @returns 切割器轮廓。
|
|
373
|
+
*/
|
|
374
|
+
export function domedCapNutCountersinkProfile(data) {
|
|
375
|
+
const { m, s } = requireMS(data, 'DomedCapNut');
|
|
376
|
+
const dk = num(data, 'dk', 'DomedCapNut');
|
|
377
|
+
const width = polygonDiagonal(s, 6) + SOCKET_CLEARANCE;
|
|
378
|
+
return rectProfile(width / 2, m + dk / 2);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* `HexNutWithFlange.countersink_profile`(fastener.py:1157)—— 取 `dc+clearance` 与
|
|
382
|
+
* `socket_clearance` 的较大者。
|
|
383
|
+
* @param data - 该规格的参数行(含 dc/m/s)。
|
|
384
|
+
* @param threadDiameter - 螺纹公称直径(mm)。
|
|
385
|
+
* @param clearanceHoleDiameter - 该配合等级的间隙孔直径(mm)。
|
|
386
|
+
* @returns 切割器轮廓。
|
|
387
|
+
*/
|
|
388
|
+
export function hexNutFlangeCountersinkProfile(data, threadDiameter, clearanceHoleDiameter) {
|
|
389
|
+
const { m, s } = requireMS(data, 'HexNutWithFlange');
|
|
390
|
+
const dc = num(data, 'dc', 'HexNutWithFlange');
|
|
391
|
+
const clearance = clearanceHoleDiameter - threadDiameter;
|
|
392
|
+
const width = Math.max(dc + clearance, polygonDiagonal(s, 6) + SOCKET_CLEARANCE);
|
|
393
|
+
return rectProfile(width / 2, m);
|
|
394
|
+
}
|
|
395
|
+
// ── 英制尺寸解码(fastener.py:74)──────────────────────────────────────────
|
|
396
|
+
/** 英制编号规格(`#0`…`#12`)的直径(mm,fastener.py:79-92)。 */
|
|
397
|
+
const IMPERIAL_NUMBERED_SIZES = {
|
|
398
|
+
'#0000': 0.021 * INCH,
|
|
399
|
+
'#000': 0.034 * INCH,
|
|
400
|
+
'#00': 0.047 * INCH,
|
|
401
|
+
'#0': 0.06 * INCH,
|
|
402
|
+
'#1': 0.073 * INCH,
|
|
403
|
+
'#2': 0.086 * INCH,
|
|
404
|
+
'#3': 0.099 * INCH,
|
|
405
|
+
'#4': 0.112 * INCH,
|
|
406
|
+
'#5': 0.125 * INCH,
|
|
407
|
+
'#6': 0.138 * INCH,
|
|
408
|
+
'#8': 0.164 * INCH,
|
|
409
|
+
'#10': 0.19 * INCH,
|
|
410
|
+
'#12': 0.216 * INCH,
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* `fastener.py:74 decode_imperial_size` —— 拆英寸规格为 (大径 mm, 螺距 mm)。
|
|
414
|
+
* @param size - 形如 `1/4-20` 或 `#6-32` 的英制规格。
|
|
415
|
+
* @returns `[大径 mm, 螺距 mm]`。
|
|
416
|
+
*/
|
|
417
|
+
export function decodeImperialSize(size) {
|
|
418
|
+
const parts = size.split('-');
|
|
419
|
+
if (parts.length < 2)
|
|
420
|
+
throw new Error(`Nut: imperial size ${JSON.stringify(size)} must be size-TPI`);
|
|
421
|
+
let major;
|
|
422
|
+
if (size.startsWith('#')) {
|
|
423
|
+
const v = IMPERIAL_NUMBERED_SIZES[parts[0]];
|
|
424
|
+
if (v === undefined)
|
|
425
|
+
throw new Error(`Nut: unknown imperial numbered size ${parts[0]}`);
|
|
426
|
+
major = v;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
const v = imperialStrToFloat(parts[0]);
|
|
430
|
+
if (typeof v !== 'number')
|
|
431
|
+
throw new Error(`Nut: imperial size ${JSON.stringify(parts[0])} is not a measure`);
|
|
432
|
+
major = v;
|
|
433
|
+
}
|
|
434
|
+
const tpi = imperialStrToFloat(parts[1]);
|
|
435
|
+
if (typeof tpi !== 'number')
|
|
436
|
+
throw new Error(`Nut: imperial TPI ${JSON.stringify(parts[1])} is not a measure`);
|
|
437
|
+
return [major, INCH / (tpi / INCH)];
|
|
438
|
+
}
|
|
439
|
+
function assertHand(hand, where) {
|
|
440
|
+
if (hand !== 'right' && hand !== 'left')
|
|
441
|
+
throw new Error(`${where}: hand must be one of "right" or "left" not ${hand}`);
|
|
442
|
+
}
|
|
443
|
+
function makeBlank(planWire, maxNutHeight, threadHeight, threadDiameter) {
|
|
444
|
+
const blank = extrudeFace(planarFace(planWire), maxNutHeight);
|
|
445
|
+
// `.faces("<Z").workplane().hole(d, depth)`:自 z=0 起钻 threadHeight 深
|
|
446
|
+
return cut(blank, cylinderBetween(threadDiameter / 2, 0, threadHeight));
|
|
447
|
+
}
|
|
448
|
+
function makeStandardNut(hooks, ctx) {
|
|
449
|
+
const profile = hooks.profile(ctx.nutData);
|
|
450
|
+
const maxNutHeight = Math.max(...profilePoints(profile).map((p) => p.z));
|
|
451
|
+
const nut = revolveProfile(profileWire(profile), AXIS_Z, 2 * Math.PI);
|
|
452
|
+
let out = intersect(nut, makeBlank(hooks.planWire(ctx.nutData), maxNutHeight, ctx.threadHeight, ctx.threadDiameter));
|
|
453
|
+
if (hooks.flangeProfile) {
|
|
454
|
+
let flange = revolveProfile(profileWire(hooks.flangeProfile(ctx.nutData)), AXIS_Z, 2 * Math.PI);
|
|
455
|
+
const fb = bboxOf(flange);
|
|
456
|
+
// `.faces(">Z").hole(d)` 无 depth → 穿透
|
|
457
|
+
flange = cut(flange, cylinderBetween(ctx.threadDiameter / 2, fb.zmin - 1, fb.zmax + 1));
|
|
458
|
+
out = fuse(out, flange);
|
|
459
|
+
}
|
|
460
|
+
if (!ctx.simple) {
|
|
461
|
+
const t = isoThread({
|
|
462
|
+
major_diameter: ctx.threadDiameter,
|
|
463
|
+
pitch: ctx.threadPitch,
|
|
464
|
+
length: ctx.threadHeight,
|
|
465
|
+
external: false,
|
|
466
|
+
end_finishes: ['fade', 'fade'],
|
|
467
|
+
hand: ctx.hand,
|
|
468
|
+
});
|
|
469
|
+
if (t.handle)
|
|
470
|
+
out = fuse(out, t.handle);
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
/** 规格串 → (大径, 螺距):公制 `M6-1` 直取,英制 `1/4-20` / `#6-32` 走 {@link decodeImperialSize}。 */
|
|
475
|
+
function parseThreadDistance(threadSize, isMetric, size) {
|
|
476
|
+
const parts = threadSize.split('-');
|
|
477
|
+
if (isMetric) {
|
|
478
|
+
const d = Number(parts[0].slice(1));
|
|
479
|
+
const p = Number(parts[1]);
|
|
480
|
+
if (!Number.isFinite(d) || !Number.isFinite(p))
|
|
481
|
+
throw new Error(`Nut: metric size ${JSON.stringify(size)} is malformed`);
|
|
482
|
+
return [d, p];
|
|
483
|
+
}
|
|
484
|
+
return decodeImperialSize(threadSize);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* `Nut.__init__`(fastener.py:520)的公共解析:规格拆分 + 表行定位 + hand/simple 归一。
|
|
488
|
+
* @param className - 螺母类名(决定参数表与报错文案)。
|
|
489
|
+
* @param p - 入参(`size` / `fastener_type` / `hand` / `simple`)。
|
|
490
|
+
* @returns 解析后的入参集合。
|
|
491
|
+
*/
|
|
492
|
+
function resolveNut(className, p) {
|
|
493
|
+
const table = NUT_TABLES[className];
|
|
494
|
+
const types = typesOf(table);
|
|
495
|
+
const size = p.size.trim();
|
|
496
|
+
const sizeParts = size.split('-');
|
|
497
|
+
if (sizeParts.length < 2)
|
|
498
|
+
throw new Error(`${JSON.stringify(sizeParts)} invalid, must be formatted as size-pitch(-length) or size-TPI(-length) where length is optional`);
|
|
499
|
+
const threadSize = sizeParts.slice(0, 2).join('-');
|
|
500
|
+
const lengthSize = sizeParts.length === 3 ? sizeParts[2] : undefined;
|
|
501
|
+
const isMetric = threadSize.startsWith('M');
|
|
502
|
+
const [threadDiameter, threadPitch] = parseThreadDistance(threadSize, isMetric, size);
|
|
503
|
+
if (!types.includes(p.fastener_type))
|
|
504
|
+
throw new Error(`${p.fastener_type} invalid, must be one of ${types.join(', ')}`);
|
|
505
|
+
const hand = p.hand ?? 'right';
|
|
506
|
+
assertHand(hand, className);
|
|
507
|
+
const simple = p.simple ?? true;
|
|
508
|
+
const isolated = isolateFastenerType(p.fastener_type, table);
|
|
509
|
+
const nutData = isolated[size];
|
|
510
|
+
if (!nutData)
|
|
511
|
+
throw new Error(`${size} invalid, must be one of ${Object.keys(isolated).join(', ')}`);
|
|
512
|
+
return { size, threadSize, lengthSize, isMetric, threadDiameter, threadPitch, hand, simple, nutData };
|
|
513
|
+
}
|
|
514
|
+
/** 由几何句柄补齐上游的派生量(`nut_thickness` / `nut_diameter` / `info` / 孔表)。 */
|
|
515
|
+
function finalizeNut(r, className, p, handle) {
|
|
516
|
+
const bb = bboxOf(handle);
|
|
517
|
+
const nutThickness = bb.zmax;
|
|
518
|
+
const nutDiameter = 2 * Math.max(Math.abs(bb.xmin), Math.abs(bb.xmax), Math.abs(bb.ymin), Math.abs(bb.ymax));
|
|
519
|
+
return {
|
|
520
|
+
handle,
|
|
521
|
+
nutClass: className,
|
|
522
|
+
size: r.size,
|
|
523
|
+
threadSize: r.threadSize,
|
|
524
|
+
lengthSize: r.lengthSize,
|
|
525
|
+
isMetric: r.isMetric,
|
|
526
|
+
threadDiameter: r.threadDiameter,
|
|
527
|
+
threadPitch: r.threadPitch,
|
|
528
|
+
fastenerType: p.fastener_type,
|
|
529
|
+
hand: r.hand,
|
|
530
|
+
simple: r.simple,
|
|
531
|
+
nutData: r.nutData,
|
|
532
|
+
nutThickness,
|
|
533
|
+
nutDiameter,
|
|
534
|
+
info: `${className}(${p.fastener_type}): ${r.threadSize}`,
|
|
535
|
+
clearanceHoleDiameters: clearanceHoleData[r.threadSize.split('-')[0]] ?? {},
|
|
536
|
+
tapHoleDiameters: tapHoleData[r.threadSize] ?? {},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* `Nut.__init__` + `make_nut`(fastener.py:520 / :581)的共享实现(标准族)。
|
|
541
|
+
*
|
|
542
|
+
* @param className - 螺母类名(决定参数表与输出标签)。
|
|
543
|
+
* @param hooks - 该类的轮廓 / plan / 法兰钩子。
|
|
544
|
+
* @param p - 入参(`size` / `fastener_type` / `hand` / `simple`)。
|
|
545
|
+
* @returns 螺母实体与上游同名派生量。
|
|
546
|
+
*/
|
|
547
|
+
export function buildNut(className, hooks, p) {
|
|
548
|
+
const r = resolveNut(className, p);
|
|
549
|
+
const threadHeight = num(r.nutData, 'm', className);
|
|
550
|
+
const handle = makeStandardNut(hooks, {
|
|
551
|
+
nutData: r.nutData,
|
|
552
|
+
threadDiameter: r.threadDiameter,
|
|
553
|
+
threadHeight,
|
|
554
|
+
simple: r.simple,
|
|
555
|
+
threadPitch: r.threadPitch,
|
|
556
|
+
hand: r.hand,
|
|
557
|
+
});
|
|
558
|
+
return finalizeNut(r, className, p, handle);
|
|
559
|
+
}
|
|
560
|
+
// ── 七个具体类 ────────────────────────────────────────────────────────────
|
|
561
|
+
/**
|
|
562
|
+
* `HexNut`(fastener.py:1105)——iso4032 / iso4033 / iso4035。
|
|
563
|
+
* @param p - 入参(`size` 如 `M6-1`;`fastener_type` 如 `iso4032`)。
|
|
564
|
+
* @returns 螺母实体与派生量。
|
|
565
|
+
*/
|
|
566
|
+
export function hexNut(p) {
|
|
567
|
+
return buildNut('HexNut', { profile: defaultNutProfile, planWire: defaultNutPlanWire }, p);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* `HexNutWithFlange`(fastener.py:1123)——din1665,带法兰分支。
|
|
571
|
+
* @param p - 入参(`fastener_type` 固定 `din1665`)。
|
|
572
|
+
* @returns 螺母实体与派生量。
|
|
573
|
+
*/
|
|
574
|
+
export function hexNutWithFlange(p) {
|
|
575
|
+
return buildNut('HexNutWithFlange', {
|
|
576
|
+
profile: defaultNutProfile,
|
|
577
|
+
planWire: defaultNutPlanWire,
|
|
578
|
+
flangeProfile: hexNutFlangeProfile,
|
|
579
|
+
}, p);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* `UnchamferedHexagonNut`(fastener.py:1176)——iso4036,无倒角。
|
|
583
|
+
* @param p - 入参(`fastener_type` 固定 `iso4036`)。
|
|
584
|
+
* @returns 螺母实体与派生量。
|
|
585
|
+
*/
|
|
586
|
+
export function unchamferedHexagonNut(p) {
|
|
587
|
+
return buildNut('UnchamferedHexagonNut', { profile: unchamferedHexagonNutProfile, planWire: defaultNutPlanWire }, p);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* `SquareNut`(fastener.py:1200)——din557,四方。
|
|
591
|
+
* @param p - 入参(`fastener_type` 固定 `din557`)。
|
|
592
|
+
* @returns 螺母实体与派生量。
|
|
593
|
+
*/
|
|
594
|
+
export function squareNut(p) {
|
|
595
|
+
return buildNut('SquareNut', { profile: squareNutProfile, planWire: squareNutPlanWire }, p);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* `DomedCapNut`(fastener.py:674)——din1587,球顶。
|
|
599
|
+
* @param p - 入参(`fastener_type` 固定 `din1587`)。
|
|
600
|
+
* @returns 螺母实体与派生量。
|
|
601
|
+
*/
|
|
602
|
+
export function domedCapNut(p) {
|
|
603
|
+
return buildNut('DomedCapNut', { profile: domedCapNutProfile, planWire: defaultNutPlanWire }, p);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* `cq.Workplane.polarArray(radius, startAngle, endAngle, count)`(cadquery `cq.py`)——
|
|
607
|
+
* **只取位置,不建几何**(本包不需要平台的 `polarArray` parity)。
|
|
608
|
+
*
|
|
609
|
+
* cq 的角度分布为 `startAngle + (endAngle − startAngle)·i/count`(`i = 0..count−1`),
|
|
610
|
+
* 即 `endAngle` 是**排他**上界:`(0, 360, 3)` → 0°/120°/240°。
|
|
611
|
+
* @param radius - 阵列半径(mm)。
|
|
612
|
+
* @param startAngleDeg - 起始角(度)。
|
|
613
|
+
* @param endAngleDeg - 结束角(度,排他)。
|
|
614
|
+
* @param count - 个数。
|
|
615
|
+
* @returns `[x, y]` 位置列(z=0)。
|
|
616
|
+
*/
|
|
617
|
+
export function polarArrayLocations(radius, startAngleDeg, endAngleDeg, count) {
|
|
618
|
+
if (!Number.isInteger(count) || count <= 0)
|
|
619
|
+
throw new Error(`Nut: polarArray count must be a positive integer, got ${count}`);
|
|
620
|
+
const out = [];
|
|
621
|
+
for (let i = 0; i < count; i++) {
|
|
622
|
+
const deg = startAngleDeg + ((endAngleDeg - startAngleDeg) * i) / count;
|
|
623
|
+
const a = (deg * Math.PI) / 180;
|
|
624
|
+
out.push([radius * Math.cos(a), radius * Math.sin(a)]);
|
|
625
|
+
}
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* `BradTeeNut.custom_make`(fastener.py:734)—— 大法兰盘 + `brad_num` 个沉头螺钉孔。
|
|
630
|
+
*
|
|
631
|
+
* 上游步骤:`make_nut()` → `faces(">Z").workplane()` → `polarArray(bcd/2, 0, 360, brad_num)`
|
|
632
|
+
* → `clearanceHole(fastener=CounterSunkScrew(brad_size, 2c, "iso10642"))`。
|
|
633
|
+
*
|
|
634
|
+
* 本包把 `clearanceHole` 走 {@link holes.fastenerHoleCutter}(W9·P1-b 正式实现,
|
|
635
|
+
* 原 `recess.ts` 的 W4 临时切割器已删除;沉头轮廓仍用 `tempCounterSunkCountersinkProfile`)。解析验证(`scripts/probe-bradtee-decomposition.py`
|
|
636
|
+
* 出 A 侧中间量;B 侧 STEP 等价由 `src/nut.test.ts` 锁定):
|
|
637
|
+
* - `make_nut()` = 3585.465923(与前剖面/plan 模型解析值一致);
|
|
638
|
+
* - 每孔切除 65.4292(截锥 ∪ 杆部 ∪ 钻尖 在法兰板内的并集),3 孔共 196.290;
|
|
639
|
+
* - 终值 = 3389.175287,与 A 侧 manifest 逐位一致。
|
|
640
|
+
*
|
|
641
|
+
* ⚠️ **不调上游的 `.clean()`**(OCCT `ShapeUpgrade_UnifySameDomain`)——只合同域面、
|
|
642
|
+
* 不改体积;内核无该封装(cq-compat parity 欠账)。见 `src/recess.ts` 文件头。
|
|
643
|
+
* @param p - 入参(`size` / `fastener_type`=`Hilitchi`)。
|
|
644
|
+
* @returns 螺母实体与派生量。
|
|
645
|
+
*/
|
|
646
|
+
export function bradTeeNut(p) {
|
|
647
|
+
const r = resolveNut('BradTeeNut', p);
|
|
648
|
+
const nutData = r.nutData;
|
|
649
|
+
const profile = bradTeeNutProfile(nutData);
|
|
650
|
+
const maxNutHeight = Math.max(...profilePoints(profile).map((q) => q.z));
|
|
651
|
+
const nutThreadHeight = num(nutData, 'm', 'BradTeeNut');
|
|
652
|
+
let nut = intersect(revolveProfile(profileWire(profile), AXIS_Z, 2 * Math.PI), makeBlank(bradTeeNutPlanWire(nutData), maxNutHeight, nutThreadHeight, r.threadDiameter));
|
|
653
|
+
// `brad = CounterSunkScrew(size=brad_size, length=2c, fastener_type="iso10642")`
|
|
654
|
+
// —— W5 之前只用它的两个参数面:间隙孔表 + 沉头轮廓(见 src/recess.ts 文件头)。
|
|
655
|
+
const bradSize = String(nutData['brad_size'] ?? '');
|
|
656
|
+
const bradParts = bradSize.split('-');
|
|
657
|
+
if (bradParts.length < 2 || !bradSize.startsWith('M'))
|
|
658
|
+
throw new Error(`BradTeeNut: brad_size ${JSON.stringify(bradSize)} must be a metric size-pitch`);
|
|
659
|
+
const clearanceRow = clearanceHoleData[bradParts[0]];
|
|
660
|
+
const clearance = clearanceRow?.['Normal'];
|
|
661
|
+
if (clearance === undefined)
|
|
662
|
+
throw new Error(`BradTeeNut: no Normal clearance hole for brad size ${bradSize} (table: ${Object.keys(clearanceRow ?? {}).join(', ') || 'missing'})`);
|
|
663
|
+
const screwData = isolateFastenerType('iso10642', SCREW_TABLES.CounterSunkScrew)[bradSize];
|
|
664
|
+
if (!screwData)
|
|
665
|
+
throw new Error(`BradTeeNut: no iso10642 countersunk data for ${bradSize}`);
|
|
666
|
+
// `depth=None` → 上游取 `self.largestDimension()`(包围盒对角线)
|
|
667
|
+
// W9·P1-b:孔切割器走 src/holes.ts 正式实现(原 recess.ts Temp* 已删)。
|
|
668
|
+
const countersinkProfile = tempCounterSunkCountersinkProfile(screwData, 'Normal');
|
|
669
|
+
const cutter = fastenerHoleCutter({
|
|
670
|
+
countersinkProfile,
|
|
671
|
+
holeRadius: clearance / 2,
|
|
672
|
+
depth: bboxDiagonal(nut),
|
|
673
|
+
});
|
|
674
|
+
// `polarArray(bcd/2, 0, 360, brad_num)` 落在 `>Z` 面上 → z = 顶面高度
|
|
675
|
+
const topZ = bboxOf(nut).zmax;
|
|
676
|
+
const bcd = num(nutData, 'bcd', 'BradTeeNut');
|
|
677
|
+
const bradNum = num(nutData, 'brad_num', 'BradTeeNut');
|
|
678
|
+
for (const [x, y] of polarArrayLocations(bcd / 2, 0, 360, bradNum))
|
|
679
|
+
nut = cut(nut, translate(cutter, x, y, topZ));
|
|
680
|
+
return finalizeNut(r, 'BradTeeNut', p, nut);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* `HeatSetNut`(fastener.py:791)—— McMaster-Carr / Hilitchi 热熔铜螺母。
|
|
684
|
+
*
|
|
685
|
+
* ⚠️ **W4 未实现(显式抛错)**。上游 `make_nut`(fastener.py:947)不走向轮廓旋转,
|
|
686
|
+
* 而是 `Face.makeNSidedSurface` 造 knurl 面 + `Shell.makeShell` + `Solid.makeSolid`。
|
|
687
|
+
* 本内核无 `makeNSidedSurface`;probe(`scripts/kernel-nut-probe.ts` 段 4)实测
|
|
688
|
+
* `makeNonPlanarFace(4 边扭面 wire)` 退化为 **3 边面**(面积 2.749170),几何不等价
|
|
689
|
+
* ——不构成可接受的替代。详见 W4 分析文档。
|
|
690
|
+
* @param p - 入参(`size` 形如 `M3-0.5-Standard`)。
|
|
691
|
+
* @throws 总是抛出,说明缺失内核能力。
|
|
692
|
+
* @returns 永不返回(抛错)。
|
|
693
|
+
*/
|
|
694
|
+
export function heatSetNut(p) {
|
|
695
|
+
throw new Error(`HeatSetNut: not implemented in W4 — make_nut needs Face.makeNSidedSurface for the knurl ` +
|
|
696
|
+
`faces (kernel has no such primitive; makeNonPlanarFace degenerates a 4-edge twisted ` +
|
|
697
|
+
`face to a 3-edge face, measured area 2.749170). Requested ${p.size}/${p.fastener_type}. ` +
|
|
698
|
+
`See docs/analysis/2026-09-14-cq-warehouse-nut-washer-probe.md`);
|
|
699
|
+
}
|
|
700
|
+
//# sourceMappingURL=nut.js.map
|