@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/screw.js
ADDED
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
// Copyright 2026 Faicad. All rights reserved.
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
/**
|
|
5
|
+
* screw.ts — 螺钉 12 类(W5)—— 上游 `cq_warehouse.fastener.Screw` 基类 + 12 个子类的逐位复刻。
|
|
6
|
+
*
|
|
7
|
+
* 装配流(`fastener.py:1513 make_head` + `:176 __init__`):
|
|
8
|
+
* ```
|
|
9
|
+
* head = revolve(head_profile) ∩ extrude(head_plan) # 有 recess 时先 cut 再交
|
|
10
|
+
* ∪ revolve(flange_profile) # HexHeadWithFlange 才有
|
|
11
|
+
* cq_object = head.translate((0,0,-length_offset)) ∪ shank.translate((0,0,-length))
|
|
12
|
+
* shank = circle(min_radius).extrude(thread_length)(simple)或 shank.fuse(thread)
|
|
13
|
+
* ```
|
|
14
|
+
* `SetScrew` 走 `custom_make`:无头、全杆长、`cylinder − 六角沉孔`。
|
|
15
|
+
*
|
|
16
|
+
* 几何坐标:轮廓画在 XZ 平面(局部 x → 世界 X=半径、局部 y → 世界 +Z),
|
|
17
|
+
* `revolveProfile` 绕 Z 轴 360°。杆部 z ∈ [−length, −length_offset],
|
|
18
|
+
* 头部 z ∈ [−length_offset, −length_offset + head_height)。
|
|
19
|
+
*
|
|
20
|
+
* 逐位复刻的三个关键事实(A 侧实测见 `scripts/probe-screw-head-profiles.py`,
|
|
21
|
+
* 回归锁在 `src/screw.test.ts`;分析见
|
|
22
|
+
* `docs/analysis/2026-09-14-cq-warehouse-screw-probe.md`):
|
|
23
|
+
*
|
|
24
|
+
* 1. **`vertices(">X")` / `edges(">Z").vertices(">X")` 用的是 `Edge.Center()`**,
|
|
25
|
+
* 而圆弧的 `Edge.Center()` 是**圆弧质心而非圆心**——见 {@link edgeCenter}。
|
|
26
|
+
* 2. **`CheeseHeadScrew` 的 `k / cos(degrees(5))` 单位怪癖**:`math.degrees(5)`
|
|
27
|
+
* 的结果(286.4789)被当**弧度**喂给 `cos`,故长度为负;与 `5-90 = -85°`
|
|
28
|
+
* 方向叠加后实测等效于「沿 5° 内倾母线向上」。直接照抄公式即得正确几何。
|
|
29
|
+
* 3. **`fillet2D` 的圆弧邻边必须走圆心连线的精确解**:`t = r/tan(θ/2)` 只对
|
|
30
|
+
* 「两直线角」精确,用在「圆弧-直线」角上会把切点放到**弧外**
|
|
31
|
+
* (RCOS 实测 `|p1 − 圆心| = 12.00099 ≠ rf = 12`,误差 2.0e-3)——见
|
|
32
|
+
* {@link filletAt}。
|
|
33
|
+
*/
|
|
34
|
+
import { arcEdge, bboxOf, cut, cylinderBetween, extrudeFace, fuse, interpolateEdgeWithTangents, intersect, lineEdge, planarFace, polygonWire, radiusArcMidpoint, revolveProfile, translate as translateShape, wireFromEdges, } from './primitives.js';
|
|
35
|
+
import { clearanceHoleDiameters, isolateFastenerType, SCREW_TABLES, screwTypes, } from './params.js';
|
|
36
|
+
import { defaultHeadRecess, recessCutter } from './recess.js';
|
|
37
|
+
import { isoThread, isoThreadDimensions } from './thread.js';
|
|
38
|
+
import { decodeImperialSize, polygonDiagonal } from './nut.js';
|
|
39
|
+
const DEG = Math.PI / 180;
|
|
40
|
+
const TWO_PI = 2 * Math.PI;
|
|
41
|
+
/** 旋转轴(Z 轴)——上游 `Workplane("XZ").revolve()` 的隐式轴。 */
|
|
42
|
+
const AXIS_Z = { point: { x: 0, y: 0, z: 0 }, direction: { x: 0, y: 0, z: 1 } };
|
|
43
|
+
/** `num(row)`:参数行取数(gen-data 已在生成期求值,运行期只做类型收窄)。 */
|
|
44
|
+
function num(row, key, where) {
|
|
45
|
+
const v = row[key];
|
|
46
|
+
if (typeof v !== 'number')
|
|
47
|
+
throw new Error(`${where}: screw_data["${key}"] is not numeric (got ${JSON.stringify(v)})`);
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
// ── 2D 轮廓构造原语(复刻 cq Workplane XZ 语义) ────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* 上游 `polarLine(start, angle)`:从 start 沿 angle 度方向画一条线。
|
|
53
|
+
* 角度 0 = 局部 +x,逆时针为正。**长度允许为负**(CheeseHead 的 `degrees(5)`
|
|
54
|
+
* 单位怪癖就产生负长度,见文件头说明)——照抄公式即正确。
|
|
55
|
+
* @param start - 起点。
|
|
56
|
+
* @param angleDeg - 方向角(度)。
|
|
57
|
+
* @param length - 长度(可负)。
|
|
58
|
+
* @returns 终点。
|
|
59
|
+
*/
|
|
60
|
+
function polarLineEnd(start, angleDeg, length) {
|
|
61
|
+
return {
|
|
62
|
+
x: start.x + length * Math.cos(angleDeg * DEG),
|
|
63
|
+
y: start.y + length * Math.sin(angleDeg * DEG),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 复刻 cq `radiusArc(end, radius)` 的符号语义:radius 的符号选择凹凸方向
|
|
68
|
+
* (正半径 = 劣弧一侧,负半径 = 优弧一侧),与 `radiusArcMidpoint` 的
|
|
69
|
+
* sagitta 符号规则一致。
|
|
70
|
+
* @param start - 弧起点。
|
|
71
|
+
* @param end - 弧终点。
|
|
72
|
+
* @param radius - 带符号半径。
|
|
73
|
+
* @returns 弧中点(供 `arcEdge` 使用)。
|
|
74
|
+
*/
|
|
75
|
+
function radiusArcMid(start, end, radius) {
|
|
76
|
+
return radiusArcMidpoint(start, end, radius);
|
|
77
|
+
}
|
|
78
|
+
/** 直线段(`hLineTo` / `vLineTo` / `lineTo` / `polarLine` 的产物)。 */
|
|
79
|
+
function line(a, b) {
|
|
80
|
+
return { kind: 'line', a, b };
|
|
81
|
+
}
|
|
82
|
+
/** 圆弧段(`radiusArc(end, radius)`:mid 按 cq sagitta 语义由 {@link radiusArcMid} 算)。 */
|
|
83
|
+
function arcSeg(a, b, radius) {
|
|
84
|
+
return { kind: 'arc', a, mid: radiusArcMid(a, b, radius), b };
|
|
85
|
+
}
|
|
86
|
+
/** 插值样条段(`spline([p], tangents=[t0,t1], includeCurrent=True)`)。 */
|
|
87
|
+
function splineSeg(a, b, t0, t1) {
|
|
88
|
+
return { kind: 'spline', a, b, t0, t1 };
|
|
89
|
+
}
|
|
90
|
+
/** 二维单位向量。 */
|
|
91
|
+
function unit(v) {
|
|
92
|
+
const l = Math.hypot(v.x, v.y);
|
|
93
|
+
return { x: v.x / l, y: v.y / l };
|
|
94
|
+
}
|
|
95
|
+
/** 归一化到 [0, 2π)。 */
|
|
96
|
+
function norm2pi(x) {
|
|
97
|
+
return ((x % TWO_PI) + TWO_PI) % TWO_PI;
|
|
98
|
+
}
|
|
99
|
+
/** 圆弧段的有向张角(沿 a→mid→b 行进,可正可负)。 */
|
|
100
|
+
function arcSweep(s, c) {
|
|
101
|
+
const t0 = Math.atan2(s.a.y - c.y, s.a.x - c.x);
|
|
102
|
+
const t1 = Math.atan2(s.b.y - c.y, s.b.x - c.x);
|
|
103
|
+
const tm = Math.atan2(s.mid.y - c.y, s.mid.x - c.x);
|
|
104
|
+
let d = norm2pi(t1 - t0);
|
|
105
|
+
if (norm2pi(tm - t0) > d)
|
|
106
|
+
d -= TWO_PI;
|
|
107
|
+
return d;
|
|
108
|
+
}
|
|
109
|
+
/** 圆弧段的圆心(三点外接圆)。 */
|
|
110
|
+
function arcCenter(s) {
|
|
111
|
+
const { a, mid: m, b } = s;
|
|
112
|
+
const d = 2 * (a.x * (m.y - b.y) + m.x * (b.y - a.y) + b.x * (a.y - m.y));
|
|
113
|
+
const sa = a.x * a.x + a.y * a.y;
|
|
114
|
+
const sm = m.x * m.x + m.y * m.y;
|
|
115
|
+
const sb = b.x * b.x + b.y * b.y;
|
|
116
|
+
return {
|
|
117
|
+
x: (sa * (m.y - b.y) + sm * (b.y - a.y) + sb * (a.y - m.y)) / d,
|
|
118
|
+
y: (sa * (b.x - m.x) + sm * (a.x - b.x) + sb * (m.x - a.x)) / d,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** 圆弧在 p 处的单位切向,方向取「朝 ref 一侧」。 */
|
|
122
|
+
function arcTangentAt(p, c, ref) {
|
|
123
|
+
const v = { x: p.x - c.x, y: p.y - c.y };
|
|
124
|
+
const t = { x: -v.y, y: v.x };
|
|
125
|
+
const towards = { x: ref.x - p.x, y: ref.y - p.y };
|
|
126
|
+
const sgn = t.x * towards.x + t.y * towards.y >= 0 ? 1 : -1;
|
|
127
|
+
return unit({ x: sgn * t.x, y: sgn * t.y });
|
|
128
|
+
}
|
|
129
|
+
/** 段在终点处的行进单位切向。 */
|
|
130
|
+
function tangentAtEnd(s) {
|
|
131
|
+
if (s.kind === 'line')
|
|
132
|
+
return unit({ x: s.b.x - s.a.x, y: s.b.y - s.a.y });
|
|
133
|
+
if (s.kind === 'spline')
|
|
134
|
+
throw new Error('fillet2D: spline edge tangent unsupported(上游无此用例)');
|
|
135
|
+
const back = arcTangentAt(s.b, arcCenter(s), s.mid);
|
|
136
|
+
return { x: -back.x, y: -back.y };
|
|
137
|
+
}
|
|
138
|
+
/** 段在起点处的行进单位切向。 */
|
|
139
|
+
function tangentAtStart(s) {
|
|
140
|
+
if (s.kind === 'line')
|
|
141
|
+
return unit({ x: s.b.x - s.a.x, y: s.b.y - s.a.y });
|
|
142
|
+
if (s.kind === 'spline')
|
|
143
|
+
throw new Error('fillet2D: spline edge tangent unsupported(上游无此用例)');
|
|
144
|
+
return arcTangentAt(s.a, arcCenter(s), s.mid);
|
|
145
|
+
}
|
|
146
|
+
/** 段的可裁剪长度(直线 = 弦长;圆弧 = 弧长;spline 只报弦长——上游从不裁它)。 */
|
|
147
|
+
function segLength(s) {
|
|
148
|
+
if (s.kind !== 'arc')
|
|
149
|
+
return Math.hypot(s.b.x - s.a.x, s.b.y - s.a.y);
|
|
150
|
+
const c = arcCenter(s);
|
|
151
|
+
return Math.abs(arcSweep(s, c)) * Math.hypot(s.a.x - c.x, s.a.y - c.y);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `Edge.Center()`——cq 方向选择器 `>Z` / `>X` 的排序键用的就是它,即边的**质心**
|
|
155
|
+
* (圆弧给的是圆弧质心,**不是圆心**——把圆心当质心是复刻 cq 选择器最容易踩的坑,
|
|
156
|
+
* 会让 RCOS 的 `edges(">Z")` 选错边)。直线 = 中点。
|
|
157
|
+
*
|
|
158
|
+
* 圆弧质心到圆心的距离 = `2R·sin(Δ/2)/Δ`(Δ 为有向张角),方向取弧中点角。
|
|
159
|
+
* @param s - 轮廓段。
|
|
160
|
+
* @returns 该边的质心(XZ 平面)。
|
|
161
|
+
*/
|
|
162
|
+
function edgeCenter(s) {
|
|
163
|
+
if (s.kind !== 'arc')
|
|
164
|
+
return { x: (s.a.x + s.b.x) / 2, y: (s.a.y + s.b.y) / 2 };
|
|
165
|
+
const c = arcCenter(s);
|
|
166
|
+
const r = Math.hypot(s.a.x - c.x, s.a.y - c.y);
|
|
167
|
+
const d = arcSweep(s, c);
|
|
168
|
+
const amp = (2 * r * Math.sin(Math.abs(d) / 2)) / Math.abs(d);
|
|
169
|
+
const t0 = Math.atan2(s.a.y - c.y, s.a.x - c.x);
|
|
170
|
+
return { x: c.x + amp * Math.cos(t0 + d / 2), y: c.y + amp * Math.sin(t0 + d / 2) };
|
|
171
|
+
}
|
|
172
|
+
/** 圆上 A→B 的弧中点(绕行方向由 origMid 指示,origMid 须在原弧上)。 */
|
|
173
|
+
function subArcMid(c, r, a, b, origMid) {
|
|
174
|
+
const d = arcSweep({ a, mid: origMid, b }, c);
|
|
175
|
+
const t0 = Math.atan2(a.y - c.y, a.x - c.x);
|
|
176
|
+
const am = t0 + d / 2;
|
|
177
|
+
return { x: c.x + r * Math.cos(am), y: c.y + r * Math.sin(am) };
|
|
178
|
+
}
|
|
179
|
+
/** 段上两点(须都在该段上)之间的行进距离:直线 = 弦长,圆弧 = 弧长。 */
|
|
180
|
+
function distAlong(s, from, to) {
|
|
181
|
+
if (s.kind !== 'arc')
|
|
182
|
+
return Math.hypot(to.x - from.x, to.y - from.y);
|
|
183
|
+
const c = arcCenter(s);
|
|
184
|
+
const rad = Math.hypot(s.a.x - c.x, s.a.y - c.y);
|
|
185
|
+
const a0 = Math.atan2(from.y - c.y, from.x - c.x);
|
|
186
|
+
const a1 = Math.atan2(to.y - c.y, to.x - c.x);
|
|
187
|
+
let d = a1 - a0;
|
|
188
|
+
while (d > Math.PI)
|
|
189
|
+
d -= TWO_PI;
|
|
190
|
+
while (d < -Math.PI)
|
|
191
|
+
d += TWO_PI;
|
|
192
|
+
return Math.abs(d) * rad;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* `Wire.fillet2D(radius, vertices)` 的单顶点复刻(**精确解**)。
|
|
196
|
+
*
|
|
197
|
+
* 圆角圆心 C 由「到入边距离 = r」与「到出边距离 = r」两条约束确定:
|
|
198
|
+
* - 直线边:`(C − a)·n = r`(n 取料内侧单位法向,由初始猜测点定侧);
|
|
199
|
+
* - 圆弧边:`|C − Q| = R ∓ r`(Q 圆心;圆角落在圆内取 `R−r`、圆外取 `R+r`),
|
|
200
|
+
* 切点 = `Q + R·unit(C − Q)`(内切/外切同式)。
|
|
201
|
+
*
|
|
202
|
+
* 两直线角退化为经典 `t = r/tan(θ/2)`(与旧实现逐位等价,已对 CounterSunk /
|
|
203
|
+
* SocketHeadCap 等用例验证)。
|
|
204
|
+
*
|
|
205
|
+
* ⚠️ **圆弧邻边绝不能当直线截**:把 `t = r/tan(θ/2)` 当弦长去截弧会得到弧外的点
|
|
206
|
+
* (RCOS 的「圆弧-直线」角会偏 1e-3,实测 `|p1 − Q| = 12.00099 ≠ rf`)。圆弧邻边
|
|
207
|
+
* 必须走「圆心连线」的精确解,否则 STEP 回归锁直接红。
|
|
208
|
+
* @param prev - 入边(角点 = `prev.b`)。
|
|
209
|
+
* @param next - 出边(起点 = 角点)。
|
|
210
|
+
* @param r - 圆角半径。
|
|
211
|
+
* @returns 切点 P1(在 prev 上)/ P2(在 next 上)与圆角弧中点。
|
|
212
|
+
* @throws 半径超过任一边可用长度、或与邻边无相容解时(响亮失败)。
|
|
213
|
+
*/
|
|
214
|
+
function filletAt(prev, next, r) {
|
|
215
|
+
const corner = prev.b;
|
|
216
|
+
const inDir = tangentAtEnd(prev);
|
|
217
|
+
const outDir = tangentAtStart(next);
|
|
218
|
+
const u1 = { x: -inDir.x, y: -inDir.y };
|
|
219
|
+
const cosT = u1.x * outDir.x + u1.y * outDir.y;
|
|
220
|
+
const theta = Math.acos(Math.max(-1, Math.min(1, cosT)));
|
|
221
|
+
const bis = unit({ x: u1.x + outDir.x, y: u1.y + outDir.y });
|
|
222
|
+
/** 圆心 → 圆角弧中点 = 圆上离角点最近的点。 */
|
|
223
|
+
const midNear = (ctr) => {
|
|
224
|
+
const v = unit({ x: corner.x - ctr.x, y: corner.y - ctr.y });
|
|
225
|
+
return { x: ctr.x + v.x * r, y: ctr.y + v.y * r };
|
|
226
|
+
};
|
|
227
|
+
const availIn = segLength(prev);
|
|
228
|
+
const availOut = segLength(next);
|
|
229
|
+
// ① 两直线角:经典闭式(精确)
|
|
230
|
+
if (prev.kind === 'line' && next.kind === 'line') {
|
|
231
|
+
const t = r / Math.tan(theta / 2);
|
|
232
|
+
if (t > availIn || t > availOut)
|
|
233
|
+
throw new Error(`fillet2D: radius ${r} too large for corner edges (${availIn}, ${availOut})`);
|
|
234
|
+
return {
|
|
235
|
+
p1: { x: corner.x + u1.x * t, y: corner.y + u1.y * t },
|
|
236
|
+
p2: { x: corner.x + outDir.x * t, y: corner.y + outDir.y * t },
|
|
237
|
+
mid: midNear({ x: corner.x + bis.x * (r / Math.sin(theta / 2)), y: corner.y + bis.y * (r / Math.sin(theta / 2)) }),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (prev.kind === 'spline' || next.kind === 'spline')
|
|
241
|
+
throw new Error('fillet2D: 不支持裁剪样条边(上游无此用例)');
|
|
242
|
+
// ② 直线 + 圆弧角:直线边参数化 C = a + s·d + r·n,代入圆弧约束解 s 的二次式
|
|
243
|
+
const lineIsPrev = prev.kind === 'line';
|
|
244
|
+
const ln = lineIsPrev ? prev : next;
|
|
245
|
+
const arc = lineIsPrev ? next : prev;
|
|
246
|
+
const ld = unit({ x: ln.b.x - ln.a.x, y: ln.b.y - ln.a.y });
|
|
247
|
+
const ctr0d = r / Math.sin(theta / 2);
|
|
248
|
+
const guess = { x: corner.x + bis.x * ctr0d, y: corner.y + bis.y * ctr0d };
|
|
249
|
+
// 料内侧法向:取使初始猜测点落在 +n 侧的那个法向
|
|
250
|
+
const nRaw = { x: -ld.y, y: ld.x };
|
|
251
|
+
const sgn = (guess.x - ln.a.x) * nRaw.x + (guess.y - ln.a.y) * nRaw.y >= 0 ? 1 : -1;
|
|
252
|
+
const n = { x: sgn * nRaw.x, y: sgn * nRaw.y };
|
|
253
|
+
const Q = arcCenter(arc);
|
|
254
|
+
const R = Math.hypot(arc.a.x - Q.x, arc.a.y - Q.y);
|
|
255
|
+
const Rc = Math.hypot(guess.x - Q.x, guess.y - Q.y) < R ? R - r : R + r;
|
|
256
|
+
const w = { x: ln.a.x + r * n.x - Q.x, y: ln.a.y + r * n.y - Q.y };
|
|
257
|
+
const wd = w.x * ld.x + w.y * ld.y;
|
|
258
|
+
const c0 = w.x * w.x + w.y * w.y - Rc * Rc;
|
|
259
|
+
const disc = wd * wd - c0;
|
|
260
|
+
if (disc < 0)
|
|
261
|
+
throw new Error(`fillet2D: radius ${r} 与该角不相容(无解)`);
|
|
262
|
+
const root = Math.sqrt(disc);
|
|
263
|
+
const cand = [
|
|
264
|
+
{ x: ln.a.x + (-wd + root) * ld.x, y: ln.a.y + (-wd + root) * ld.y },
|
|
265
|
+
{ x: ln.a.x + (-wd - root) * ld.x, y: ln.a.y + (-wd - root) * ld.y },
|
|
266
|
+
];
|
|
267
|
+
const linePt = cand.reduce((best, p) => Math.abs((p.x - corner.x) * ld.x + (p.y - corner.y) * ld.y) <
|
|
268
|
+
Math.abs((best.x - corner.x) * ld.x + (best.y - corner.y) * ld.y)
|
|
269
|
+
? p
|
|
270
|
+
: best);
|
|
271
|
+
const ctr = { x: linePt.x + r * n.x, y: linePt.y + r * n.y };
|
|
272
|
+
const au = unit({ x: ctr.x - Q.x, y: ctr.y - Q.y });
|
|
273
|
+
const arcPt = { x: Q.x + R * au.x, y: Q.y + R * au.y };
|
|
274
|
+
const p1 = lineIsPrev ? linePt : arcPt;
|
|
275
|
+
const p2 = lineIsPrev ? arcPt : linePt;
|
|
276
|
+
if (distAlong(prev, corner, p1) > availIn || distAlong(next, corner, p2) > availOut)
|
|
277
|
+
throw new Error(`fillet2D: radius ${r} too large for corner edges (${availIn}, ${availOut})`);
|
|
278
|
+
return { p1, p2, mid: midNear(ctr) };
|
|
279
|
+
}
|
|
280
|
+
/** 把段截到新终点 p(p 须在该段上;圆弧重算中点)。 */
|
|
281
|
+
function trimEnd(s, p) {
|
|
282
|
+
if (s.kind === 'line')
|
|
283
|
+
return { kind: 'line', a: s.a, b: p };
|
|
284
|
+
if (s.kind === 'spline')
|
|
285
|
+
throw new Error('fillet2D: 不支持裁剪样条边(上游无此用例)');
|
|
286
|
+
const c = arcCenter(s);
|
|
287
|
+
const r = Math.hypot(s.a.x - c.x, s.a.y - c.y);
|
|
288
|
+
return { kind: 'arc', a: s.a, mid: subArcMid(c, r, s.a, p, s.mid), b: p };
|
|
289
|
+
}
|
|
290
|
+
/** 把段截到新起点 p(p 须在该段上;圆弧重算中点)。 */
|
|
291
|
+
function trimStart(s, p) {
|
|
292
|
+
if (s.kind === 'line')
|
|
293
|
+
return { kind: 'line', a: p, b: s.b };
|
|
294
|
+
if (s.kind === 'spline')
|
|
295
|
+
throw new Error('fillet2D: 不支持裁剪样条边(上游无此用例)');
|
|
296
|
+
const c = arcCenter(s);
|
|
297
|
+
const r = Math.hypot(s.a.x - c.x, s.a.y - c.y);
|
|
298
|
+
return { kind: 'arc', a: p, mid: subArcMid(c, r, p, s.b, s.mid), b: s.b };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 在指定**角点**(= `segs[i].b`)处倒半径 r 的圆角,返回新段序列
|
|
302
|
+
* (相邻边已按切点裁剪,圆角弧插在两者之间)。
|
|
303
|
+
* @param segs - 原始段序列。
|
|
304
|
+
* @param corners - 角点索引(= 该角点所属段的索引 i,裁的是 i 与 i+1)。
|
|
305
|
+
* @param r - 圆角半径。
|
|
306
|
+
* @returns 倒角后的段序列。
|
|
307
|
+
*/
|
|
308
|
+
function filletSegs(segs, corners, r) {
|
|
309
|
+
const out = segs.slice();
|
|
310
|
+
// 降序处理:splice 只会影响更大的下标,先做大的可保持小下标仍指向原段
|
|
311
|
+
for (const i of [...corners].sort((x, y) => y - x)) {
|
|
312
|
+
const prev = out[i];
|
|
313
|
+
const next = out[i + 1];
|
|
314
|
+
if (!prev || !next)
|
|
315
|
+
throw new Error(`fillet2D: corner index ${i} out of range`);
|
|
316
|
+
const g = filletAt(prev, next, r);
|
|
317
|
+
out[i] = trimEnd(prev, g.p1);
|
|
318
|
+
out[i + 1] = trimStart(next, g.p2);
|
|
319
|
+
out.splice(i + 1, 0, { kind: 'arc', a: g.p1, mid: g.mid, b: g.p2 });
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* `vertices(">X")`:轮廓上**全部** x 取最大值的顶点(cq 返回列表,可能多于一个——
|
|
325
|
+
* ButtonHeadWithCollar 的 `(dc/2,c)` 与 `(dc/2,0)` 就同时中选)。
|
|
326
|
+
* @param segs - 轮廓段序列。
|
|
327
|
+
* @returns 角点索引列表(角点 = `segs[i].b`)。
|
|
328
|
+
*/
|
|
329
|
+
function maxXCornerIndices(segs) {
|
|
330
|
+
const mx = Math.max(...segs.map((s) => s.b.x));
|
|
331
|
+
return segs.map((s, i) => (Math.abs(s.b.x - mx) <= 1e-9 ? i : -1)).filter((i) => i >= 0);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* `edges(">Z").vertices(">X")`(CheeseHead / CounterSunk / SocketHeadCap / RCOS 的
|
|
335
|
+
* 圆角落点):先按 `Edge.Center()` 的 z 取最大 z 的边,再取这些边端点里 x 最大的
|
|
336
|
+
* **顶点**,最后把该顶点映射回「以它为终点的段索引」。
|
|
337
|
+
* @param segs - 轮廓段序列。
|
|
338
|
+
* @returns 角点索引列表。
|
|
339
|
+
*/
|
|
340
|
+
function topEdgeMaxXCornerIndices(segs) {
|
|
341
|
+
const zs = segs.map((s) => edgeCenter(s).y);
|
|
342
|
+
const mz = Math.max(...zs);
|
|
343
|
+
const cand = [];
|
|
344
|
+
for (let i = 0; i < segs.length; i++)
|
|
345
|
+
if (Math.abs(zs[i] - mz) <= 1e-9)
|
|
346
|
+
cand.push(segs[i].a, segs[i].b);
|
|
347
|
+
const mx = Math.max(...cand.map((p) => p.x));
|
|
348
|
+
const verts = cand.filter((p) => Math.abs(p.x - mx) <= 1e-9);
|
|
349
|
+
return [
|
|
350
|
+
...new Set(segs
|
|
351
|
+
.map((s, i) => verts.some((p) => Math.abs(p.x - s.b.x) <= 1e-9 && Math.abs(p.y - s.b.y) <= 1e-9)
|
|
352
|
+
? i
|
|
353
|
+
: -1)
|
|
354
|
+
.filter((i) => i >= 0)),
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
/** XZ 轮廓点 → 世界坐标(局部 x → 世界 X=半径、局部 y → 世界 +Z)。 */
|
|
358
|
+
function v3(p) {
|
|
359
|
+
return { x: p.x, y: 0, z: p.y };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 轮廓**顶点**的最大 z(= 上游 `profile.vertices(">Z").val().Z`)。
|
|
363
|
+
*
|
|
364
|
+
* ⚠️ 只取段端点,**不含弧中点**:cq 的 `vertices()` 返回的是真顶点。这个值会被用作
|
|
365
|
+
* 沉孔切割器的落位高度与 plan 挤出高度,多算一点就会整体偏移几何。
|
|
366
|
+
* @param segs - 轮廓段序列。
|
|
367
|
+
* @returns 顶点最大 z。
|
|
368
|
+
*/
|
|
369
|
+
function profileMaxZ(segs) {
|
|
370
|
+
let m = -Infinity;
|
|
371
|
+
for (const s of segs)
|
|
372
|
+
m = Math.max(m, s.a.y, s.b.y);
|
|
373
|
+
return m;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 轮廓**顶点**的最大 x(= 上游 `profile.vertices(">X").val().X`,同上只取端点)。
|
|
377
|
+
* @param segs - 轮廓段序列。
|
|
378
|
+
* @returns 顶点最大 x。
|
|
379
|
+
*/
|
|
380
|
+
function profileMaxX(segs) {
|
|
381
|
+
let m = -Infinity;
|
|
382
|
+
for (const s of segs)
|
|
383
|
+
m = Math.max(m, s.a.x, s.b.x);
|
|
384
|
+
return m;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* XZ 轮廓段序列 → 封闭 wire → 绕 Z 轴旋转成**实体**。
|
|
388
|
+
*
|
|
389
|
+
* 末段若未回到起点则补一条闭合边(上游 `.close()`)。
|
|
390
|
+
* @param segs - 轮廓段序列。
|
|
391
|
+
* @returns 旋转实体(经 {@link revolveProfile} 保证是 solid 而非 shell)。
|
|
392
|
+
*/
|
|
393
|
+
function revolveXZProfile(segs) {
|
|
394
|
+
const edges = [];
|
|
395
|
+
for (const s of segs) {
|
|
396
|
+
if (s.kind === 'line')
|
|
397
|
+
edges.push(lineEdge(v3(s.a), v3(s.b)));
|
|
398
|
+
else if (s.kind === 'arc')
|
|
399
|
+
edges.push(arcEdge(v3(s.a), v3(s.mid), v3(s.b)));
|
|
400
|
+
else
|
|
401
|
+
edges.push(interpolateEdgeWithTangents([v3(s.a), v3(s.b)], s.t0, s.t1));
|
|
402
|
+
}
|
|
403
|
+
const first = segs[0];
|
|
404
|
+
const last = segs[segs.length - 1];
|
|
405
|
+
if (first && last && Math.hypot(last.b.x - first.a.x, last.b.y - first.a.y) > 1e-9)
|
|
406
|
+
edges.push(lineEdge(v3(last.b), v3(first.a)));
|
|
407
|
+
return revolveProfile(planarFace(wireFromEdges(edges)), AXIS_Z, TWO_PI);
|
|
408
|
+
}
|
|
409
|
+
/** 默认 oversized plan:3·maxR 方形(上游 `make_head` 的 `rect(3*max_head_radius, …)`)。 */
|
|
410
|
+
function rectPlanWire(half) {
|
|
411
|
+
return polygonWire([
|
|
412
|
+
{ x: -half, y: -half, z: 0 },
|
|
413
|
+
{ x: half, y: -half, z: 0 },
|
|
414
|
+
{ x: half, y: half, z: 0 },
|
|
415
|
+
{ x: -half, y: half, z: 0 },
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
/** plan 点序列 → wire → 面沿 +Z 挤出 `height`。 */
|
|
419
|
+
function extrudePlanSolid(planWire, height) {
|
|
420
|
+
return extrudeFace(planarFace(planWire), height);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* 正六边形平面顶点列(`cq.Workplane("XY").polygon(6, e)`:首顶点在 +X,
|
|
424
|
+
* `e` 是外接圆直径——cq 的 `polygon(n, diameter)` 内部取 `radius = diameter/2`,
|
|
425
|
+
* 实测 HexHead `polygon(6, polygon_diagonal(10))` 顶点半径 = `e/2` = 5.773503)。
|
|
426
|
+
* @param circumDiameter - 外接圆直径(= `polygonDiagonal(widthAcrossFlats, 6)`)。
|
|
427
|
+
* @returns 6 个顶点(z=0)。
|
|
428
|
+
*/
|
|
429
|
+
function hexPlanPoints(circumDiameter) {
|
|
430
|
+
const r = circumDiameter / 2;
|
|
431
|
+
const pts = [];
|
|
432
|
+
for (let i = 0; i < 6; i++) {
|
|
433
|
+
const a = (Math.PI / 3) * i;
|
|
434
|
+
pts.push({ x: r * Math.cos(a), y: r * Math.sin(a), z: 0 });
|
|
435
|
+
}
|
|
436
|
+
return pts;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* 指定 z 区间内的正六棱柱(SetScrew 的六角沉孔)。
|
|
440
|
+
* @param circumDiameter - 外接圆直径。
|
|
441
|
+
* @param zFrom - 起始高度(mm)。
|
|
442
|
+
* @param zTo - 结束高度(mm)。
|
|
443
|
+
* @returns 六棱柱实体(`zTo < zFrom` 时自动反向)。
|
|
444
|
+
*/
|
|
445
|
+
function hexPrism(circumDiameter, zFrom, zTo) {
|
|
446
|
+
const solid = extrudeFace(planarFace(polygonWire(hexPlanPoints(circumDiameter))), zTo - zFrom);
|
|
447
|
+
return translateShape(solid, 0, 0, Math.min(zFrom, zTo));
|
|
448
|
+
}
|
|
449
|
+
// ── 12 类头型轮廓:逐位复刻 ─────────────────────────────────────────────────
|
|
450
|
+
/** 全部螺钉类名(= `SCREW_TABLES` 的键,按声明序;测试与比对脚本共用)。 */
|
|
451
|
+
export const SCREW_CLASSES = Object.keys(SCREW_TABLES);
|
|
452
|
+
/**
|
|
453
|
+
* `countersink_profile` 的默认实现(`fastener.py:378`):
|
|
454
|
+
* `rect(width/2, k, centered=False)`,`width = clearance_hole_diameter − d + dk`。
|
|
455
|
+
* @param d - 解析后的 screw_data。
|
|
456
|
+
* @param fit - `Close` / `Normal` / `Loose`。
|
|
457
|
+
* @returns XZ 轮廓段序列((0,0)→(w,0)→(w,k)→(0,k)→(0,0))。
|
|
458
|
+
*/
|
|
459
|
+
function defaultCountersink(d, fit = 'Normal') {
|
|
460
|
+
const clr = clearanceHoleDiameters(d.threadSize.split('-')[0]);
|
|
461
|
+
const dia = clr[fit];
|
|
462
|
+
if (dia === undefined)
|
|
463
|
+
throw new Error(`countersink_profile: ${fit} invalid, must be one of ${Object.keys(clr).join(', ')}`);
|
|
464
|
+
const width = dia - d.threadDiameter + num(d.data, 'dk', 'default_countersink_profile');
|
|
465
|
+
const k = num(d.data, 'k', 'default_countersink_profile');
|
|
466
|
+
const o = { x: 0, y: 0 };
|
|
467
|
+
return [
|
|
468
|
+
line(o, { x: width / 2, y: 0 }),
|
|
469
|
+
line({ x: width / 2, y: 0 }, { x: width / 2, y: k }),
|
|
470
|
+
line({ x: width / 2, y: k }, { x: 0, y: k }),
|
|
471
|
+
line({ x: 0, y: k }, o),
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
/** 各类的钩子定义表(`method_exists` 判定的显式化)。 */
|
|
475
|
+
const SCREW_DEFS = {
|
|
476
|
+
// 圆头(hex 沉孔):vLineTo(k) → hLineTo(dl/2) → radiusArc((dk/2,0), rf) → hLineTo(0)
|
|
477
|
+
ButtonHeadScrew: {
|
|
478
|
+
profile: (d) => {
|
|
479
|
+
const o = { x: 0, y: 0 };
|
|
480
|
+
const p1 = { x: 0, y: num(d.data, 'k', 'ButtonHeadScrew') };
|
|
481
|
+
const p2 = { x: num(d.data, 'dl', 'ButtonHeadScrew') / 2, y: p1.y };
|
|
482
|
+
const p3 = { x: num(d.data, 'dk', 'ButtonHeadScrew') / 2, y: 0 };
|
|
483
|
+
return [line(o, p1), line(p1, p2), arcSeg(p2, p3, num(d.data, 'rf', 'ButtonHeadScrew')), line(p3, o)];
|
|
484
|
+
},
|
|
485
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
486
|
+
countersink: (d) => defaultCountersink(d),
|
|
487
|
+
},
|
|
488
|
+
// 带台肩圆头:fillet2D(0.45c) 落在 `vertices(">X")` 的两个顶点上
|
|
489
|
+
ButtonHeadWithCollarScrew: {
|
|
490
|
+
profile: (d) => {
|
|
491
|
+
const o = { x: 0, y: 0 };
|
|
492
|
+
const k = num(d.data, 'k', 'ButtonHeadWithCollarScrew');
|
|
493
|
+
const dl = num(d.data, 'dl', 'ButtonHeadWithCollarScrew');
|
|
494
|
+
const dk = num(d.data, 'dk', 'ButtonHeadWithCollarScrew');
|
|
495
|
+
const dc = num(d.data, 'dc', 'ButtonHeadWithCollarScrew');
|
|
496
|
+
const c = num(d.data, 'c', 'ButtonHeadWithCollarScrew');
|
|
497
|
+
const rf = num(d.data, 'rf', 'ButtonHeadWithCollarScrew');
|
|
498
|
+
const segs = [
|
|
499
|
+
line(o, { x: 0, y: k }),
|
|
500
|
+
line({ x: 0, y: k }, { x: dl / 2, y: k }),
|
|
501
|
+
arcSeg({ x: dl / 2, y: k }, { x: dk / 2, y: c }, rf),
|
|
502
|
+
line({ x: dk / 2, y: c }, { x: dc / 2, y: c }),
|
|
503
|
+
line({ x: dc / 2, y: c }, { x: dc / 2, y: 0 }),
|
|
504
|
+
line({ x: dc / 2, y: 0 }, o),
|
|
505
|
+
];
|
|
506
|
+
return filletSegs(segs, maxXCornerIndices(segs), 0.45 * c);
|
|
507
|
+
},
|
|
508
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
509
|
+
// 上游覆写:`width = clearance − d + dc`
|
|
510
|
+
countersink: (d) => {
|
|
511
|
+
const clr = clearanceHoleDiameters(d.threadSize.split('-')[0]);
|
|
512
|
+
const dia = clr['Normal'];
|
|
513
|
+
if (dia === undefined)
|
|
514
|
+
throw new Error('countersink_profile: Normal clearance missing');
|
|
515
|
+
const dc = num(d.data, 'dc', 'ButtonHeadWithCollarScrew');
|
|
516
|
+
const k = num(d.data, 'k', 'ButtonHeadWithCollarScrew');
|
|
517
|
+
const w = dia - d.threadDiameter + dc;
|
|
518
|
+
const o = { x: 0, y: 0 };
|
|
519
|
+
return [
|
|
520
|
+
line(o, { x: w / 2, y: 0 }),
|
|
521
|
+
line({ x: w / 2, y: 0 }, { x: w / 2, y: k }),
|
|
522
|
+
line({ x: w / 2, y: k }, { x: 0, y: k }),
|
|
523
|
+
line({ x: 0, y: k }, o),
|
|
524
|
+
];
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
// 圆柱头(slot / T 沉孔):5° 收顶 + `polarLine` 单位怪癖(见文件头)
|
|
528
|
+
CheeseHeadScrew: {
|
|
529
|
+
profile: (d) => {
|
|
530
|
+
const k = num(d.data, 'k', 'CheeseHeadScrew');
|
|
531
|
+
const dk = num(d.data, 'dk', 'CheeseHeadScrew');
|
|
532
|
+
// 上游 `k / cos(degrees(5))`:`math.degrees(5)` = 286.4789 被当作弧度 →
|
|
533
|
+
// cos 为负 → 长度为负;与 `5 − 90 = −85°` 叠加后 = 沿 5° 内倾母线向上。
|
|
534
|
+
const len = k / Math.cos((5 * 180) / Math.PI);
|
|
535
|
+
const o = { x: 0, y: 0 };
|
|
536
|
+
const p0 = { x: dk / 2, y: 0 };
|
|
537
|
+
const p1 = polarLineEnd(p0, 5 - 90, len);
|
|
538
|
+
const p2 = { x: 0, y: p1.y };
|
|
539
|
+
const segs = [line(o, p0), line(p0, p1), line(p1, p2), line(p2, o)];
|
|
540
|
+
return filletSegs(segs, topEdgeMaxXCornerIndices(segs), k * 0.25);
|
|
541
|
+
},
|
|
542
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
543
|
+
countersink: (d) => defaultCountersink(d),
|
|
544
|
+
},
|
|
545
|
+
// 沉头(length_offset = k):-90-a/2 方向的锥面母线 + fillet2D(0.075k)
|
|
546
|
+
CounterSunkScrew: {
|
|
547
|
+
profile: (d) => {
|
|
548
|
+
const a = num(d.data, 'a', 'CounterSunkScrew');
|
|
549
|
+
const k = num(d.data, 'k', 'CounterSunkScrew');
|
|
550
|
+
const dk = num(d.data, 'dk', 'CounterSunkScrew');
|
|
551
|
+
const o = { x: 0, y: 0 };
|
|
552
|
+
const top = { x: 0, y: k };
|
|
553
|
+
const rim = { x: dk / 2, y: k };
|
|
554
|
+
const foot = polarLineEnd(rim, -90 - a / 2, k / Math.cos((a / 2) * DEG));
|
|
555
|
+
const segs = [line(o, top), line(top, rim), line(rim, foot), line(foot, o)];
|
|
556
|
+
return filletSegs(segs, topEdgeMaxXCornerIndices(segs), k * 0.075);
|
|
557
|
+
},
|
|
558
|
+
lengthOffset: (d) => num(d.data, 'k', 'CounterSunkScrew'),
|
|
559
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
560
|
+
// 上游覆写:vLineTo(k) → hLineTo(dk/2) → polarLine → close(顶端 z = k)
|
|
561
|
+
countersink: (d) => counterSunkCountersink(d),
|
|
562
|
+
},
|
|
563
|
+
// 六角头(head_plan 六角 + 15° 倒角)
|
|
564
|
+
HexHeadScrew: {
|
|
565
|
+
profile: (d) => hexHeadProfile(d),
|
|
566
|
+
plan: (d) => hexPlanPoints(polygonDiagonal(num(d.data, 's', 'HexHeadScrew'), 6)),
|
|
567
|
+
// 上游覆写:`width = e + socket_clearance + e`
|
|
568
|
+
countersink: (d) => {
|
|
569
|
+
const s = num(d.data, 's', 'HexHeadScrew');
|
|
570
|
+
const k = num(d.data, 'k', 'HexHeadScrew');
|
|
571
|
+
const e = polygonDiagonal(s, 6);
|
|
572
|
+
return rectSegs((e + d.socketClearance + e) / 2, k);
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
// 六角法兰头:头型/plan 复用 HexHead,另加 flange_profile
|
|
576
|
+
HexHeadWithFlangeScrew: {
|
|
577
|
+
profile: (d) => hexHeadProfile(d),
|
|
578
|
+
plan: (d) => hexPlanPoints(polygonDiagonal(num(d.data, 's', 'HexHeadWithFlangeScrew'), 6)),
|
|
579
|
+
flange: (d) => {
|
|
580
|
+
const dc = num(d.data, 'dc', 'HexHeadWithFlangeScrew');
|
|
581
|
+
const c = num(d.data, 'c', 'HexHeadWithFlangeScrew');
|
|
582
|
+
const flangeAngle = 25;
|
|
583
|
+
const tangent = {
|
|
584
|
+
x: (c / 2) * Math.cos((90 - flangeAngle) * DEG) + (dc - c) / 2,
|
|
585
|
+
y: (c / 2) * Math.sin((90 - flangeAngle) * DEG) + c / 2,
|
|
586
|
+
};
|
|
587
|
+
const half = dc / 2 - c / 2;
|
|
588
|
+
const o = { x: 0, y: 0 };
|
|
589
|
+
const cap = polarLineEnd(tangent, 180 - flangeAngle, half);
|
|
590
|
+
return [line(o, { x: half, y: 0 }), arcSeg({ x: half, y: 0 }, tangent, -c / 2), line(tangent, cap), line(cap, { x: 0, y: cap.y }), line({ x: 0, y: cap.y }, o)];
|
|
591
|
+
},
|
|
592
|
+
// 上游覆写:`width = max(dc + shaft_clearance, e + socket_clearance)`
|
|
593
|
+
countersink: (d) => {
|
|
594
|
+
const clr = clearanceHoleDiameters(d.threadSize.split('-')[0]);
|
|
595
|
+
const dia = clr['Normal'];
|
|
596
|
+
if (dia === undefined)
|
|
597
|
+
throw new Error('countersink_profile: Normal clearance missing');
|
|
598
|
+
const dc = num(d.data, 'dc', 'HexHeadWithFlangeScrew');
|
|
599
|
+
const s = num(d.data, 's', 'HexHeadWithFlangeScrew');
|
|
600
|
+
const k = num(d.data, 'k', 'HexHeadWithFlangeScrew');
|
|
601
|
+
const shaftClearance = dia - d.threadDiameter;
|
|
602
|
+
return rectSegs(Math.max(dc + shaftClearance, polygonDiagonal(s, 6) + d.socketClearance) / 2, k);
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
// 盘头(spline 头型,无 fillet)
|
|
606
|
+
PanHeadScrew: {
|
|
607
|
+
profile: (d) => {
|
|
608
|
+
const k = num(d.data, 'k', 'PanHeadScrew');
|
|
609
|
+
const dk = num(d.data, 'dk', 'PanHeadScrew');
|
|
610
|
+
const o = { x: 0, y: 0 };
|
|
611
|
+
// cq `spline([(dk*0.25,k)], tangents=[(-sin5°, cos5°), (-1,0)], includeCurrent=True)`
|
|
612
|
+
const t0 = { x: -Math.sin(5 * DEG), y: 0, z: Math.cos(5 * DEG) };
|
|
613
|
+
const segs = [
|
|
614
|
+
line(o, { x: dk / 2, y: 0 }),
|
|
615
|
+
splineSeg({ x: dk / 2, y: 0 }, { x: dk * 0.25, y: k }, t0, { x: -1, y: 0, z: 0 }),
|
|
616
|
+
line({ x: dk * 0.25, y: k }, { x: 0, y: k }),
|
|
617
|
+
line({ x: 0, y: k }, o),
|
|
618
|
+
];
|
|
619
|
+
return segs;
|
|
620
|
+
},
|
|
621
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
622
|
+
countersink: (d) => defaultCountersink(d),
|
|
623
|
+
},
|
|
624
|
+
// 带台肩盘头:flat = √((k−c)(2rf−(k−c))),-rf 优弧收顶。唯一类型 din967 是 PH
|
|
625
|
+
// (cross)沉孔 → W5 已知缺口(B 侧 `default_head_recess` 显式抛错)。
|
|
626
|
+
PanHeadWithCollarScrew: {
|
|
627
|
+
profile: (d) => {
|
|
628
|
+
const rf = num(d.data, 'rf', 'PanHeadWithCollarScrew');
|
|
629
|
+
const k = num(d.data, 'k', 'PanHeadWithCollarScrew');
|
|
630
|
+
const dk = num(d.data, 'dk', 'PanHeadWithCollarScrew');
|
|
631
|
+
const c = num(d.data, 'c', 'PanHeadWithCollarScrew');
|
|
632
|
+
const flat = Math.sqrt(k - c) * Math.sqrt(2 * rf - (k - c));
|
|
633
|
+
const o = { x: 0, y: 0 };
|
|
634
|
+
return [
|
|
635
|
+
line(o, { x: dk / 2, y: 0 }),
|
|
636
|
+
line({ x: dk / 2, y: 0 }, { x: dk / 2, y: c }),
|
|
637
|
+
line({ x: dk / 2, y: c }, { x: flat, y: c }),
|
|
638
|
+
arcSeg({ x: flat, y: c }, { x: 0, y: k }, -rf),
|
|
639
|
+
line({ x: 0, y: k }, o),
|
|
640
|
+
];
|
|
641
|
+
},
|
|
642
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
643
|
+
countersink: (d) => defaultCountersink(d),
|
|
644
|
+
},
|
|
645
|
+
// 高圆柱头:oval_height = rf − √(4rf²−dk²)/2。唯一类型 iso7045 是 PH → W5 缺口。
|
|
646
|
+
RaisedCheeseHeadScrew: {
|
|
647
|
+
profile: (d) => {
|
|
648
|
+
const dk = num(d.data, 'dk', 'RaisedCheeseHeadScrew');
|
|
649
|
+
const k = num(d.data, 'k', 'RaisedCheeseHeadScrew');
|
|
650
|
+
const rf = num(d.data, 'rf', 'RaisedCheeseHeadScrew');
|
|
651
|
+
const oval = rf - Math.sqrt(4 * rf * rf - dk * dk) / 2;
|
|
652
|
+
const o = { x: 0, y: 0 };
|
|
653
|
+
return [
|
|
654
|
+
line(o, { x: 0, y: k }),
|
|
655
|
+
arcSeg({ x: 0, y: k }, { x: dk / 2, y: k - oval }, rf),
|
|
656
|
+
line({ x: dk / 2, y: k - oval }, { x: dk / 2, y: 0 }),
|
|
657
|
+
line({ x: dk / 2, y: 0 }, o),
|
|
658
|
+
];
|
|
659
|
+
},
|
|
660
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
661
|
+
countersink: (d) => defaultCountersink(d),
|
|
662
|
+
},
|
|
663
|
+
// 高沉头椭圆头(length_offset = k):-rf 优弧顶 + 锥面母线 + fillet2D(0.075k)
|
|
664
|
+
RaisedCounterSunkOvalHeadScrew: {
|
|
665
|
+
profile: (d) => {
|
|
666
|
+
const a = num(d.data, 'a', 'RaisedCounterSunkOvalHeadScrew');
|
|
667
|
+
const k = num(d.data, 'k', 'RaisedCounterSunkOvalHeadScrew');
|
|
668
|
+
const rf = num(d.data, 'rf', 'RaisedCounterSunkOvalHeadScrew');
|
|
669
|
+
const dk = num(d.data, 'dk', 'RaisedCounterSunkOvalHeadScrew');
|
|
670
|
+
const oval = rf - Math.sqrt(4 * rf * rf - dk * dk) / 2;
|
|
671
|
+
const o = { x: 0, y: 0 };
|
|
672
|
+
const rim = { x: dk / 2, y: k };
|
|
673
|
+
const foot = polarLineEnd(rim, -90 - a / 2, k / Math.cos((a / 2) * DEG));
|
|
674
|
+
const segs = [
|
|
675
|
+
line(o, { x: 0, y: k + oval }),
|
|
676
|
+
arcSeg({ x: 0, y: k + oval }, rim, rf),
|
|
677
|
+
line(rim, foot),
|
|
678
|
+
line(foot, o),
|
|
679
|
+
];
|
|
680
|
+
return filletSegs(segs, topEdgeMaxXCornerIndices(segs), k * 0.075);
|
|
681
|
+
},
|
|
682
|
+
lengthOffset: (d) => num(d.data, 'k', 'RaisedCounterSunkOvalHeadScrew'),
|
|
683
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
684
|
+
countersink: (d) => counterSunkCountersink(d),
|
|
685
|
+
},
|
|
686
|
+
// 无头紧定螺钉(custom_make):圆柱(min_radius) − 六角沉孔(外接圆 e、深 t)
|
|
687
|
+
SetScrew: {
|
|
688
|
+
custom: (d) => {
|
|
689
|
+
const s = num(d.data, 's', 'SetScrew');
|
|
690
|
+
const t = num(d.data, 't', 'SetScrew');
|
|
691
|
+
const e = polygonDiagonal(s, 6);
|
|
692
|
+
const dims = isoThreadDimensions({
|
|
693
|
+
major_diameter: d.threadDiameter,
|
|
694
|
+
pitch: d.threadPitch,
|
|
695
|
+
external: true,
|
|
696
|
+
});
|
|
697
|
+
// 上游 `circle(min_radius).polygon(6, e).extrude(t).faces(">Z").workplane()
|
|
698
|
+
// .circle(min_radius).extrude(length−t).mirror()` —— 嵌套 wire 成孔后镜像,
|
|
699
|
+
// 等价于「绕 Z 的圆柱(z∈[−length,0])减六角棱柱(z∈[−t,0])」(探针逐位吻合)。
|
|
700
|
+
const core = cut(cylinderBetween(dims.minRadius, -d.length, 0), hexPrism(e, -t, 0));
|
|
701
|
+
if (d.simple)
|
|
702
|
+
return { handle: core, threadLength: d.length };
|
|
703
|
+
const thread = isoThread({
|
|
704
|
+
major_diameter: d.threadDiameter,
|
|
705
|
+
pitch: d.threadPitch,
|
|
706
|
+
length: d.length,
|
|
707
|
+
external: true,
|
|
708
|
+
hand: d.hand,
|
|
709
|
+
end_finishes: ['fade', 'fade'],
|
|
710
|
+
simple: false,
|
|
711
|
+
});
|
|
712
|
+
return {
|
|
713
|
+
handle: fuse(core, translateShape(thread.handle, 0, 0, -d.length)),
|
|
714
|
+
threadLength: d.length,
|
|
715
|
+
};
|
|
716
|
+
},
|
|
717
|
+
countersink: () => null,
|
|
718
|
+
},
|
|
719
|
+
// 内六角圆柱头:rect(dk/2, k) + fillet2D(0.075k)
|
|
720
|
+
SocketHeadCapScrew: {
|
|
721
|
+
profile: (d) => {
|
|
722
|
+
const dk = num(d.data, 'dk', 'SocketHeadCapScrew');
|
|
723
|
+
const k = num(d.data, 'k', 'SocketHeadCapScrew');
|
|
724
|
+
const o = { x: 0, y: 0 };
|
|
725
|
+
const segs = [
|
|
726
|
+
line(o, { x: dk / 2, y: 0 }),
|
|
727
|
+
line({ x: dk / 2, y: 0 }, { x: dk / 2, y: k }),
|
|
728
|
+
line({ x: dk / 2, y: k }, { x: 0, y: k }),
|
|
729
|
+
line({ x: 0, y: k }, o),
|
|
730
|
+
];
|
|
731
|
+
return filletSegs(segs, topEdgeMaxXCornerIndices(segs), k * 0.075);
|
|
732
|
+
},
|
|
733
|
+
recess: (d) => defaultHeadRecess(d.data),
|
|
734
|
+
countersink: (d) => defaultCountersink(d),
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
/** `rect(w, h, centered=False)` 的轮廓段序列((0,0)→(w,0)→(w,h)→(0,h)→(0,0))。 */
|
|
738
|
+
function rectSegs(w, h) {
|
|
739
|
+
const o = { x: 0, y: 0 };
|
|
740
|
+
return [line(o, { x: w, y: 0 }), line({ x: w, y: 0 }, { x: w, y: h }), line({ x: w, y: h }, { x: 0, y: h }), line({ x: 0, y: h }, o)];
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* 六角头轮廓(`fastener.py:177`):`e = polygon_diagonal(s, 6)`、
|
|
744
|
+
* `cs = (e − s)·tan15°/2`(15° 顶倒角)。
|
|
745
|
+
* @param d - 解析后的 screw_data。
|
|
746
|
+
* @returns XZ 轮廓段序列。
|
|
747
|
+
*/
|
|
748
|
+
function hexHeadProfile(d) {
|
|
749
|
+
const k = num(d.data, 'k', 'HexHeadScrew');
|
|
750
|
+
const s = num(d.data, 's', 'HexHeadScrew');
|
|
751
|
+
const e = polygonDiagonal(s, 6);
|
|
752
|
+
const cs = ((e - s) * Math.tan(15 * DEG)) / 2;
|
|
753
|
+
const o = { x: 0, y: 0 };
|
|
754
|
+
return [
|
|
755
|
+
line(o, { x: e / 2, y: 0 }),
|
|
756
|
+
line({ x: e / 2, y: 0 }, { x: e / 2, y: k - cs }),
|
|
757
|
+
line({ x: e / 2, y: k - cs }, { x: s / 2, y: k }),
|
|
758
|
+
line({ x: s / 2, y: k }, { x: 0, y: k }),
|
|
759
|
+
line({ x: 0, y: k }, o),
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 沉头族共用的 `countersink_profile`(`fastener.py:147` / `:404`):
|
|
764
|
+
* `vLineTo(k) → hLineTo(dk/2) → polarLine(k/cos(a/2), −90−a/2) → close`。
|
|
765
|
+
* @param d - 解析后的 screw_data。
|
|
766
|
+
* @returns XZ 轮廓段序列(顶点最大 z = k)。
|
|
767
|
+
*/
|
|
768
|
+
function counterSunkCountersink(d) {
|
|
769
|
+
const a = num(d.data, 'a', 'countersink_profile');
|
|
770
|
+
const k = num(d.data, 'k', 'countersink_profile');
|
|
771
|
+
const dk = num(d.data, 'dk', 'countersink_profile');
|
|
772
|
+
const o = { x: 0, y: 0 };
|
|
773
|
+
const rim = { x: dk / 2, y: k };
|
|
774
|
+
const foot = polarLineEnd(rim, -90 - a / 2, k / Math.cos((a / 2) * DEG));
|
|
775
|
+
return [
|
|
776
|
+
line(o, { x: 0, y: k }),
|
|
777
|
+
line({ x: 0, y: k }, rim),
|
|
778
|
+
line(rim, foot),
|
|
779
|
+
line(foot, o),
|
|
780
|
+
];
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* 入参解析(上游 `Screw.__init__` 的前半段,`fastener.py:186-223`):尺寸拆解 +
|
|
784
|
+
* 类型/旋向校验 + `isolate_fastener_type` 查表。
|
|
785
|
+
* @param className - 12 类之一(决定参数表与钩子集)。
|
|
786
|
+
* @param p - 入参。
|
|
787
|
+
* @returns 解析后的 screw_data 与基本量。
|
|
788
|
+
* @throws 尺寸串格式错、类型/旋向非法、该规格在表中缺失时(全部与上游同文)。
|
|
789
|
+
*/
|
|
790
|
+
function resolveScrewData(className, p) {
|
|
791
|
+
const table = SCREW_TABLES[className];
|
|
792
|
+
const types = screwTypes(className);
|
|
793
|
+
const size = p.size.trim();
|
|
794
|
+
const sizeParts = size.split('-');
|
|
795
|
+
if (sizeParts.length !== 2)
|
|
796
|
+
throw new Error(`${JSON.stringify(sizeParts)} invalid, must be formatted as size-pitch or size-TPI`);
|
|
797
|
+
const threadSize = size;
|
|
798
|
+
const isMetric = threadSize.startsWith('M');
|
|
799
|
+
let threadDiameter;
|
|
800
|
+
let threadPitch;
|
|
801
|
+
if (isMetric) {
|
|
802
|
+
threadDiameter = Number(sizeParts[0].slice(1));
|
|
803
|
+
threadPitch = Number(sizeParts[1]);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
;
|
|
807
|
+
[threadDiameter, threadPitch] = decodeImperialSize(threadSize);
|
|
808
|
+
}
|
|
809
|
+
if (!types.includes(p.fastener_type))
|
|
810
|
+
throw new Error(`${p.fastener_type} invalid, must be one of ${types.join(', ')}`);
|
|
811
|
+
const hand = p.hand ?? 'right';
|
|
812
|
+
if (hand !== 'left' && hand !== 'right')
|
|
813
|
+
throw new Error(`${hand} invalid, must be one of 'left' or 'right'`);
|
|
814
|
+
const isolatedTable = isolateFastenerType(p.fastener_type, table);
|
|
815
|
+
const screwData = isolatedTable[threadSize];
|
|
816
|
+
if (!screwData)
|
|
817
|
+
throw new Error(`${size} invalid, must be one of ${Object.keys(isolatedTable).join(', ')}`);
|
|
818
|
+
return {
|
|
819
|
+
data: screwData,
|
|
820
|
+
threadSize,
|
|
821
|
+
threadDiameter,
|
|
822
|
+
threadPitch,
|
|
823
|
+
length: p.length,
|
|
824
|
+
simple: p.simple ?? true,
|
|
825
|
+
hand,
|
|
826
|
+
socketClearance: p.socket_clearance ?? 6,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* 头型轮廓的**顶点序列**(首段起点 + 各段终点,含 `fillet2D` 切点)——对应 A 侧
|
|
831
|
+
* `head_profile` 的边端点。逐位对照的回归锁通道(见 `src/screw.test.ts`),
|
|
832
|
+
* 也是 W9·P1-b 复用沉孔轮廓的入口。
|
|
833
|
+
* @param className - 12 类之一。
|
|
834
|
+
* @param p - 入参(只需 `size` / `fastener_type` 等,`length` 不参与轮廓)。
|
|
835
|
+
* @returns XZ 顶点列(局部 x→半径、y→轴向);`SetScrew` 无头型时返回 `[]`。
|
|
836
|
+
*/
|
|
837
|
+
export function screwProfilePoints(className, p) {
|
|
838
|
+
const hooks = SCREW_DEFS[className];
|
|
839
|
+
if (!hooks.profile)
|
|
840
|
+
return [];
|
|
841
|
+
const segs = hooks.profile(resolveScrewData(className, p));
|
|
842
|
+
return [segs[0].a, ...segs.map((s) => s.b)];
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* `Screw.__init__`(`fastener.py:176`)的 B 侧复刻:解析 + 装配 + 派生量。
|
|
846
|
+
* @param className - 12 类之一(决定参数表与钩子集)。
|
|
847
|
+
* @param p - 入参。
|
|
848
|
+
* @returns 螺钉实体与上游同名派生量。
|
|
849
|
+
*/
|
|
850
|
+
export function buildScrew(className, p) {
|
|
851
|
+
const hooks = SCREW_DEFS[className];
|
|
852
|
+
const d = resolveScrewData(className, p);
|
|
853
|
+
const { threadSize, threadDiameter, threadPitch, hand, simple } = d;
|
|
854
|
+
const size = threadSize;
|
|
855
|
+
// ── make_head ──(上游 fastener.py:1513)
|
|
856
|
+
let head = null;
|
|
857
|
+
if (hooks.profile) {
|
|
858
|
+
const profileSegs = hooks.profile(d);
|
|
859
|
+
const maxHeadHeight = profileMaxZ(profileSegs);
|
|
860
|
+
const maxHeadRadius = profileMaxX(profileSegs);
|
|
861
|
+
const revolved = revolveXZProfile(profileSegs);
|
|
862
|
+
const planPoints = hooks.plan?.(d);
|
|
863
|
+
if (hooks.recess) {
|
|
864
|
+
const recess = recessCutter(hooks.recess(d), maxHeadHeight);
|
|
865
|
+
const planSolid = extrudePlanSolid(planPoints ? polygonWire(planPoints) : rectPlanWire(3 * maxHeadRadius), maxHeadHeight);
|
|
866
|
+
head = intersect(revolved, cut(planSolid, recess));
|
|
867
|
+
}
|
|
868
|
+
else if (planPoints) {
|
|
869
|
+
head = intersect(revolved, extrudePlanSolid(polygonWire(planPoints), maxHeadHeight));
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
throw new Error(`${className}: head_profile without plan or recess is unsupported`);
|
|
873
|
+
}
|
|
874
|
+
if (hooks.flange)
|
|
875
|
+
head = fuse(head, revolveXZProfile(hooks.flange(d)));
|
|
876
|
+
}
|
|
877
|
+
// ── length_offset / 装配 ──
|
|
878
|
+
const lengthOffset = hooks.lengthOffset ? hooks.lengthOffset(d) : 0;
|
|
879
|
+
if (lengthOffset >= p.length)
|
|
880
|
+
throw new Error(`Screw length ${p.length} is <= countersunk screw head ${lengthOffset}`);
|
|
881
|
+
const maxThreadLength = p.length - lengthOffset;
|
|
882
|
+
const threadLength = p.length - lengthOffset;
|
|
883
|
+
let handle = null;
|
|
884
|
+
let headHeight = 0;
|
|
885
|
+
let headDiameter = 0;
|
|
886
|
+
if (hooks.custom) {
|
|
887
|
+
// SetScrew:无头。上游 `if method_exists(custom_make): cq_object = custom_make()`
|
|
888
|
+
const r = hooks.custom(d);
|
|
889
|
+
handle = r.handle;
|
|
890
|
+
}
|
|
891
|
+
else if (head !== null) {
|
|
892
|
+
const bb = bboxOf(head);
|
|
893
|
+
headHeight = bb.zmax;
|
|
894
|
+
headDiameter = 2 * Math.max(bb.xmax, bb.ymax);
|
|
895
|
+
const headT = translateShape(head, 0, 0, -lengthOffset);
|
|
896
|
+
const dims = isoThreadDimensions({
|
|
897
|
+
major_diameter: threadDiameter,
|
|
898
|
+
pitch: threadPitch,
|
|
899
|
+
external: true,
|
|
900
|
+
});
|
|
901
|
+
const shank = cylinderBetween(dims.minRadius, -p.length, -lengthOffset);
|
|
902
|
+
if (!simple) {
|
|
903
|
+
const thread = isoThread({
|
|
904
|
+
major_diameter: threadDiameter,
|
|
905
|
+
pitch: threadPitch,
|
|
906
|
+
length: threadLength,
|
|
907
|
+
external: true,
|
|
908
|
+
hand,
|
|
909
|
+
end_finishes: ['fade', 'raw'],
|
|
910
|
+
simple: false,
|
|
911
|
+
});
|
|
912
|
+
handle = fuse(headT, fuse(shank, translateShape(thread.handle, 0, 0, -lengthOffset - threadLength)));
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
handle = fuse(headT, shank);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// ── `min_hole_depth` 相关派生量 ──
|
|
919
|
+
const cs = hooks.countersink ? hooks.countersink(d) : null;
|
|
920
|
+
const headOffset = cs ? profileMaxZ(cs) : null;
|
|
921
|
+
return {
|
|
922
|
+
handle,
|
|
923
|
+
screwClass: className,
|
|
924
|
+
size,
|
|
925
|
+
threadSize,
|
|
926
|
+
isMetric: threadSize.startsWith('M'),
|
|
927
|
+
threadDiameter,
|
|
928
|
+
threadPitch,
|
|
929
|
+
length: p.length,
|
|
930
|
+
fastenerType: p.fastener_type,
|
|
931
|
+
hand,
|
|
932
|
+
simple,
|
|
933
|
+
screwData: d.data,
|
|
934
|
+
headHeight,
|
|
935
|
+
headDiameter,
|
|
936
|
+
maxThreadLength,
|
|
937
|
+
threadLength,
|
|
938
|
+
socketClearance: d.socketClearance,
|
|
939
|
+
headOffset,
|
|
940
|
+
minHoleDepth: headOffset === null ? null : p.length + headOffset - lengthOffset,
|
|
941
|
+
minHoleDepthStraight: p.length - lengthOffset,
|
|
942
|
+
info: `${className}(${p.fastener_type}): ${threadSize}x${p.length}${hand === 'left' ? ' left hand thread' : ''}`,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
//# sourceMappingURL=screw.js.map
|