jscad-electronics 0.0.185 → 0.0.187
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/README.md +29 -0
- package/dist/cables.d.ts +52 -0
- package/dist/cables.js +582 -0
- package/dist/cables.js.map +1 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +1072 -1011
- package/dist/index.js.map +1 -1
- package/dist/vanilla.d.ts +16 -2
- package/dist/vanilla.js +231 -172
- package/dist/vanilla.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -50,6 +50,35 @@ This example creates a 3D model of an SOT-23-3P component with extruded pads.
|
|
|
50
50
|
|
|
51
51
|
## Available Components
|
|
52
52
|
|
|
53
|
+
### Cable meshes
|
|
54
|
+
|
|
55
|
+
`jscad-electronics/cables` creates indexed, colored triangle meshes for USB-C,
|
|
56
|
+
JST SH/PH, and NEMA 5-15P to IEC C13 cables. Pass a physical definition from
|
|
57
|
+
cableprinter and an already resolved path in millimeters (+Z up):
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createCableMeshes } from "jscad-electronics/cables"
|
|
61
|
+
import { parseCableString } from "@tscircuit/cableprinter"
|
|
62
|
+
|
|
63
|
+
const meshes = createCableMeshes({
|
|
64
|
+
definition: parseCableString("jst_ph_pins4"),
|
|
65
|
+
path: resolvedPath,
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The first and last path samples are cable exit centers. Connector poses follow
|
|
70
|
+
the endpoint tangents; bundle wires follow parallel-transported frames. Each
|
|
71
|
+
mesh provides `positions`, `indices`, `color`, `name`, and `smooth`. Connector
|
|
72
|
+
details include hollow shells/sockets, contacts, and strain reliefs. Physical
|
|
73
|
+
definitions, route generation and sagging remain separate from this renderer.
|
|
74
|
+
The cables entrypoint does not import React or a viewer.
|
|
75
|
+
|
|
76
|
+
Cable visual regression tests live in `tests/snapshots/cable-meshes.test.ts`.
|
|
77
|
+
They render local meshes with poppygl: all four cable types and mating faces,
|
|
78
|
+
JST SH/PH pin-count comparisons, and spatial paths viewed from both sides.
|
|
79
|
+
Annotations use `@tscircuit/alphabet`. Regenerate these six PNGs explicitly with
|
|
80
|
+
`BUN_UPDATE_SNAPSHOTS=1 bun test tests/snapshots/cable-meshes.test.ts`.
|
|
81
|
+
|
|
53
82
|
jscad-electronics includes models for various components, including:
|
|
54
83
|
|
|
55
84
|
- Resistors (0402, 0603, 0805)
|
package/dist/cables.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type CablePoint = [number, number, number];
|
|
2
|
+
type CableColor = [number, number, number, number];
|
|
3
|
+
type ConnectorBody = {
|
|
4
|
+
bodyWidth: number;
|
|
5
|
+
bodyHeight: number;
|
|
6
|
+
bodyDepth: number;
|
|
7
|
+
};
|
|
8
|
+
type CableConnectorSpec = ConnectorBody & ({
|
|
9
|
+
kind: "usb_c_plug";
|
|
10
|
+
shellWidth: number;
|
|
11
|
+
shellHeight: number;
|
|
12
|
+
shellDepth: number;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "jst_sh_housing" | "jst_ph_housing";
|
|
15
|
+
pinCount: number;
|
|
16
|
+
pitch: number;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "nema_5_15p" | "iec_c13";
|
|
19
|
+
});
|
|
20
|
+
/** Structural subset of a cableprinter definition; no runtime spec dependency. */
|
|
21
|
+
type CableGeometryDefinition = {
|
|
22
|
+
connectorA: CableConnectorSpec;
|
|
23
|
+
connectorB: CableConnectorSpec;
|
|
24
|
+
crossSection: {
|
|
25
|
+
kind: "round_jacket";
|
|
26
|
+
diameter: number;
|
|
27
|
+
color: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "wire_bundle";
|
|
30
|
+
wirePitch: number;
|
|
31
|
+
wires: {
|
|
32
|
+
diameter: number;
|
|
33
|
+
color: string;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
type CableMesh = {
|
|
38
|
+
name: string;
|
|
39
|
+
positions: number[];
|
|
40
|
+
indices: number[];
|
|
41
|
+
color: CableColor;
|
|
42
|
+
smooth: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Mesh generation only. Path samples are already resolved in world millimeters, +Z up. */
|
|
46
|
+
declare function createCableMeshes({ definition, path, radialSegments, }: {
|
|
47
|
+
definition: CableGeometryDefinition;
|
|
48
|
+
path: CablePoint[];
|
|
49
|
+
radialSegments?: number;
|
|
50
|
+
}): CableMesh[];
|
|
51
|
+
|
|
52
|
+
export { type CableColor, type CableConnectorSpec, type CableGeometryDefinition, type CableMesh, type CablePoint, createCableMeshes };
|
package/dist/cables.js
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
// lib/cables/connector-meshes.ts
|
|
2
|
+
import jscad2 from "@jscad/modeling";
|
|
3
|
+
|
|
4
|
+
// lib/cables/geometry-to-mesh.ts
|
|
5
|
+
import jscad from "@jscad/modeling";
|
|
6
|
+
|
|
7
|
+
// lib/cables/path-frames.ts
|
|
8
|
+
var add = (a, b) => [
|
|
9
|
+
a[0] + b[0],
|
|
10
|
+
a[1] + b[1],
|
|
11
|
+
a[2] + b[2]
|
|
12
|
+
];
|
|
13
|
+
var subtract = (a, b) => [
|
|
14
|
+
a[0] - b[0],
|
|
15
|
+
a[1] - b[1],
|
|
16
|
+
a[2] - b[2]
|
|
17
|
+
];
|
|
18
|
+
var scale = (point, factor) => [
|
|
19
|
+
point[0] * factor,
|
|
20
|
+
point[1] * factor,
|
|
21
|
+
point[2] * factor
|
|
22
|
+
];
|
|
23
|
+
var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
24
|
+
var cross = (a, b) => [
|
|
25
|
+
a[1] * b[2] - a[2] * b[1],
|
|
26
|
+
a[2] * b[0] - a[0] * b[2],
|
|
27
|
+
a[0] * b[1] - a[1] * b[0]
|
|
28
|
+
];
|
|
29
|
+
function normalize(point) {
|
|
30
|
+
const length = Math.hypot(...point);
|
|
31
|
+
if (length < 1e-10)
|
|
32
|
+
throw new Error(
|
|
33
|
+
"Cable path must not contain coincident points or a reversing tangent"
|
|
34
|
+
);
|
|
35
|
+
return scale(point, 1 / length);
|
|
36
|
+
}
|
|
37
|
+
function createCablePathFrames(path) {
|
|
38
|
+
if (path.length < 2 || path.some((point) => point.length !== 3 || !point.every(Number.isFinite))) {
|
|
39
|
+
throw new Error("Cable path needs at least two finite 3D points");
|
|
40
|
+
}
|
|
41
|
+
const segments = path.slice(1).map((point, index) => normalize(subtract(point, path[index])));
|
|
42
|
+
const tangents = path.map(
|
|
43
|
+
(_, index) => index === 0 ? segments[0] : index === path.length - 1 ? segments.at(-1) : normalize(add(segments[index - 1], segments[index]))
|
|
44
|
+
);
|
|
45
|
+
const initialUp = Math.abs(tangents[0][2]) > 0.95 ? [0, 1, 0] : [0, 0, 1];
|
|
46
|
+
let normal = normalize(cross(initialUp, tangents[0]));
|
|
47
|
+
return tangents.map((tangent, index) => {
|
|
48
|
+
if (index > 0) {
|
|
49
|
+
const previousTangent = tangents[index - 1];
|
|
50
|
+
const axis = cross(previousTangent, tangent);
|
|
51
|
+
const sine = Math.hypot(...axis);
|
|
52
|
+
const cosine = Math.max(-1, Math.min(1, dot(previousTangent, tangent)));
|
|
53
|
+
if (sine > 1e-10) {
|
|
54
|
+
const unitAxis = scale(axis, 1 / sine);
|
|
55
|
+
normal = normalize(
|
|
56
|
+
add(
|
|
57
|
+
add(scale(normal, cosine), scale(cross(unitAxis, normal), sine)),
|
|
58
|
+
scale(unitAxis, dot(unitAxis, normal) * (1 - cosine))
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
point: path[index],
|
|
65
|
+
tangent,
|
|
66
|
+
normal,
|
|
67
|
+
binormal: normalize(cross(tangent, normal))
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// lib/cables/geometry-to-mesh.ts
|
|
73
|
+
function geometryToCableMesh({
|
|
74
|
+
geometry,
|
|
75
|
+
color,
|
|
76
|
+
name
|
|
77
|
+
}) {
|
|
78
|
+
const triangles = jscad.geometries.geom3.toPolygons(geometry);
|
|
79
|
+
const positions = [];
|
|
80
|
+
const indices = [];
|
|
81
|
+
const vertices = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const triangle of triangles) {
|
|
83
|
+
const polygonIndices = [];
|
|
84
|
+
for (const point of triangle.vertices) {
|
|
85
|
+
const key = point.map((coordinate) => coordinate.toFixed(8)).join(",");
|
|
86
|
+
let index = vertices.get(key);
|
|
87
|
+
if (index === void 0) {
|
|
88
|
+
index = positions.length / 3;
|
|
89
|
+
positions.push(...point);
|
|
90
|
+
vertices.set(key, index);
|
|
91
|
+
}
|
|
92
|
+
polygonIndices.push(index);
|
|
93
|
+
}
|
|
94
|
+
for (let side = 1; side < polygonIndices.length - 1; side++) {
|
|
95
|
+
indices.push(
|
|
96
|
+
polygonIndices[0],
|
|
97
|
+
polygonIndices[side],
|
|
98
|
+
polygonIndices[side + 1]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { name, positions, indices, color, smooth: false };
|
|
103
|
+
}
|
|
104
|
+
function placeConnectorMesh({
|
|
105
|
+
mesh,
|
|
106
|
+
frame,
|
|
107
|
+
wireExitDepth,
|
|
108
|
+
isEnd
|
|
109
|
+
}) {
|
|
110
|
+
const tangent = scale(frame.tangent, isEnd ? -1 : 1);
|
|
111
|
+
const binormal = isEnd ? scale(frame.binormal, -1) : frame.binormal;
|
|
112
|
+
const origin = add(frame.point, scale(tangent, -wireExitDepth));
|
|
113
|
+
const positions = [];
|
|
114
|
+
for (let index = 0; index < mesh.positions.length; index += 3) {
|
|
115
|
+
positions.push(
|
|
116
|
+
...add(
|
|
117
|
+
origin,
|
|
118
|
+
add(
|
|
119
|
+
add(
|
|
120
|
+
scale(frame.normal, mesh.positions[index]),
|
|
121
|
+
scale(binormal, mesh.positions[index + 1])
|
|
122
|
+
),
|
|
123
|
+
scale(tangent, mesh.positions[index + 2])
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return { ...mesh, positions };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// lib/cables/connector-meshes.ts
|
|
132
|
+
var { cuboid, roundedCuboid, cylinder, roundedRectangle, polygon, sphere } = jscad2.primitives;
|
|
133
|
+
var { extrudeLinear } = jscad2.extrusions;
|
|
134
|
+
var { translate, rotateY } = jscad2.transforms;
|
|
135
|
+
var { subtract: subtract2, union } = jscad2.booleans;
|
|
136
|
+
var plastic = [0.055, 0.07, 0.09, 1];
|
|
137
|
+
var housing = [0.86, 0.84, 0.75, 1];
|
|
138
|
+
var metal = [0.6, 0.64, 0.7, 1];
|
|
139
|
+
var brass = [0.66, 0.46, 0.18, 1];
|
|
140
|
+
function connectorWireExitDepth(connector) {
|
|
141
|
+
if (connector.kind === "usb_c_plug")
|
|
142
|
+
return connector.shellDepth + connector.bodyDepth;
|
|
143
|
+
if (connector.kind === "nema_5_15p") return 19.05 + connector.bodyDepth;
|
|
144
|
+
return connector.bodyDepth;
|
|
145
|
+
}
|
|
146
|
+
function createJstMeshes(connector) {
|
|
147
|
+
const {
|
|
148
|
+
bodyWidth: width,
|
|
149
|
+
bodyHeight: height,
|
|
150
|
+
bodyDepth: depth,
|
|
151
|
+
pinCount,
|
|
152
|
+
pitch
|
|
153
|
+
} = connector;
|
|
154
|
+
const opening = pitch === 1 ? 0.55 : 1.05;
|
|
155
|
+
const holes = Array.from({ length: pinCount }, (_, index) => {
|
|
156
|
+
const x = (index - (pinCount - 1) / 2) * pitch;
|
|
157
|
+
return cuboid({
|
|
158
|
+
size: [opening, opening * 1.35, depth - 0.6],
|
|
159
|
+
center: [x, 0, (depth - 0.6) / 2 - 0.02]
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
const backHoles = Array.from(
|
|
163
|
+
{ length: pinCount },
|
|
164
|
+
(_, index) => cylinder({
|
|
165
|
+
radius: pitch * 0.34,
|
|
166
|
+
height: 1.3,
|
|
167
|
+
segments: 16,
|
|
168
|
+
center: [(index - (pinCount - 1) / 2) * pitch, 0, depth - 0.5]
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
const latchCut = cuboid({
|
|
172
|
+
size: [width * 0.45, 0.5, depth * 0.5],
|
|
173
|
+
center: [0, height / 2, depth * 0.65]
|
|
174
|
+
});
|
|
175
|
+
const body = subtract2(
|
|
176
|
+
roundedCuboid({
|
|
177
|
+
size: [width, height, depth],
|
|
178
|
+
center: [0, 0, depth / 2],
|
|
179
|
+
roundRadius: 0.1,
|
|
180
|
+
segments: 8
|
|
181
|
+
}),
|
|
182
|
+
...holes,
|
|
183
|
+
...backHoles,
|
|
184
|
+
latchCut,
|
|
185
|
+
...Array.from(
|
|
186
|
+
{ length: pinCount },
|
|
187
|
+
(_, index) => cuboid({
|
|
188
|
+
size: [opening * 0.8, height * 0.7, depth * 0.24],
|
|
189
|
+
center: [(index - (pinCount - 1) / 2) * pitch, height / 2, depth * 0.7]
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
const contacts = Array.from({ length: pinCount }, (_, index) => {
|
|
194
|
+
const x = (index - (pinCount - 1) / 2) * pitch;
|
|
195
|
+
return geometryToCableMesh({
|
|
196
|
+
name: `contact-${index + 1}`,
|
|
197
|
+
color: metal,
|
|
198
|
+
geometry: subtract2(
|
|
199
|
+
cuboid({
|
|
200
|
+
size: [opening * 0.92, opening * 1.25, depth * 0.65],
|
|
201
|
+
center: [x, 0, depth * 0.45]
|
|
202
|
+
}),
|
|
203
|
+
cuboid({
|
|
204
|
+
size: [
|
|
205
|
+
opening * 0.92 - 0.12,
|
|
206
|
+
opening * 1.25 - 0.12,
|
|
207
|
+
depth * 0.65 + 0.2
|
|
208
|
+
],
|
|
209
|
+
center: [x, 0, depth * 0.45]
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return [
|
|
215
|
+
geometryToCableMesh({ geometry: body, color: housing, name: "housing" }),
|
|
216
|
+
...contacts
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
function createUsbMeshes(connector) {
|
|
220
|
+
const {
|
|
221
|
+
shellWidth,
|
|
222
|
+
shellHeight,
|
|
223
|
+
shellDepth,
|
|
224
|
+
bodyWidth,
|
|
225
|
+
bodyHeight,
|
|
226
|
+
bodyDepth
|
|
227
|
+
} = connector;
|
|
228
|
+
const shell = subtract2(
|
|
229
|
+
extrudeLinear(
|
|
230
|
+
{ height: shellDepth },
|
|
231
|
+
roundedRectangle({
|
|
232
|
+
size: [shellWidth, shellHeight],
|
|
233
|
+
roundRadius: shellHeight / 2 - 0.02,
|
|
234
|
+
segments: 32
|
|
235
|
+
})
|
|
236
|
+
),
|
|
237
|
+
translate(
|
|
238
|
+
[0, 0, -0.1],
|
|
239
|
+
extrudeLinear(
|
|
240
|
+
{ height: shellDepth + 0.2 },
|
|
241
|
+
roundedRectangle({
|
|
242
|
+
size: [shellWidth - 0.4, shellHeight - 0.4],
|
|
243
|
+
roundRadius: (shellHeight - 0.4) / 2 - 0.02,
|
|
244
|
+
segments: 32
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
const liner = subtract2(
|
|
250
|
+
extrudeLinear(
|
|
251
|
+
{ height: shellDepth },
|
|
252
|
+
roundedRectangle({
|
|
253
|
+
size: [shellWidth - 0.41, shellHeight - 0.41],
|
|
254
|
+
roundRadius: 0.85,
|
|
255
|
+
segments: 32
|
|
256
|
+
})
|
|
257
|
+
),
|
|
258
|
+
translate(
|
|
259
|
+
[0, 0, -0.1],
|
|
260
|
+
extrudeLinear(
|
|
261
|
+
{ height: shellDepth - 0.3 },
|
|
262
|
+
roundedRectangle({
|
|
263
|
+
size: [shellWidth - 0.8, shellHeight - 0.8],
|
|
264
|
+
roundRadius: 0.65,
|
|
265
|
+
segments: 32
|
|
266
|
+
})
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
const body = roundedCuboid({
|
|
271
|
+
size: [bodyWidth, bodyHeight, bodyDepth],
|
|
272
|
+
center: [0, 0, shellDepth + bodyDepth / 2],
|
|
273
|
+
roundRadius: Math.min(1.3, bodyHeight / 4),
|
|
274
|
+
segments: 16
|
|
275
|
+
});
|
|
276
|
+
const contacts = Array.from(
|
|
277
|
+
{ length: 24 },
|
|
278
|
+
(_, index) => cuboid({
|
|
279
|
+
size: [0.22, 0.08, 3.5],
|
|
280
|
+
center: [(index % 12 - 5.5) * 0.5, index < 12 ? 0.77 : -0.77, 2.6]
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
return [
|
|
284
|
+
geometryToCableMesh({ geometry: body, color: plastic, name: "overmold" }),
|
|
285
|
+
geometryToCableMesh({ geometry: shell, color: metal, name: "shell" }),
|
|
286
|
+
geometryToCableMesh({
|
|
287
|
+
geometry: liner,
|
|
288
|
+
color: [0.025, 0.026, 0.03, 1],
|
|
289
|
+
name: "liner"
|
|
290
|
+
}),
|
|
291
|
+
...contacts.map(
|
|
292
|
+
(geometry, index) => geometryToCableMesh({
|
|
293
|
+
geometry,
|
|
294
|
+
color: brass,
|
|
295
|
+
name: `contact-${index + 1}`
|
|
296
|
+
})
|
|
297
|
+
)
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
function createMainsMeshes(connector) {
|
|
301
|
+
const { bodyWidth, bodyHeight, bodyDepth, kind } = connector;
|
|
302
|
+
const faceDepth = kind === "nema_5_15p" ? 19.05 : 0;
|
|
303
|
+
let body = roundedCuboid({
|
|
304
|
+
size: [bodyWidth, bodyHeight, bodyDepth],
|
|
305
|
+
center: [0, 0, faceDepth + bodyDepth / 2],
|
|
306
|
+
roundRadius: 2,
|
|
307
|
+
segments: 16
|
|
308
|
+
});
|
|
309
|
+
if (kind === "nema_5_15p") {
|
|
310
|
+
const blades = [-6.35, 6.35].map((x) => {
|
|
311
|
+
const blade = cuboid({
|
|
312
|
+
size: [1.524, 6.35, 15.875],
|
|
313
|
+
center: [x, 2.7, 19.05 - 15.875 / 2]
|
|
314
|
+
});
|
|
315
|
+
const hole = translate(
|
|
316
|
+
[x, 2.7, 7.3],
|
|
317
|
+
rotateY(
|
|
318
|
+
Math.PI / 2,
|
|
319
|
+
cylinder({ radius: 1.1, height: 3, segments: 20 })
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
return subtract2(blade, hole);
|
|
323
|
+
});
|
|
324
|
+
const ground = union(
|
|
325
|
+
cylinder({
|
|
326
|
+
radius: 2.38125,
|
|
327
|
+
height: 19.05 - 2.38125,
|
|
328
|
+
center: [0, -9.20625, (19.05 + 2.38125) / 2],
|
|
329
|
+
segments: 32
|
|
330
|
+
}),
|
|
331
|
+
sphere({ radius: 2.38125, center: [0, -9.20625, 2.38125], segments: 32 })
|
|
332
|
+
);
|
|
333
|
+
return [
|
|
334
|
+
geometryToCableMesh({
|
|
335
|
+
geometry: body,
|
|
336
|
+
color: plastic,
|
|
337
|
+
name: "plug-body"
|
|
338
|
+
}),
|
|
339
|
+
...blades.map(
|
|
340
|
+
(geometry, index) => geometryToCableMesh({
|
|
341
|
+
geometry,
|
|
342
|
+
color: brass,
|
|
343
|
+
name: `blade-${index + 1}`
|
|
344
|
+
})
|
|
345
|
+
),
|
|
346
|
+
geometryToCableMesh({
|
|
347
|
+
geometry: ground,
|
|
348
|
+
color: brass,
|
|
349
|
+
name: "ground-pin"
|
|
350
|
+
})
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
const recess = translate(
|
|
354
|
+
[0, 0, -0.1],
|
|
355
|
+
extrudeLinear(
|
|
356
|
+
{ height: 3.2 },
|
|
357
|
+
polygon({
|
|
358
|
+
points: [
|
|
359
|
+
[-9, -6],
|
|
360
|
+
[9, -6],
|
|
361
|
+
[9, 2],
|
|
362
|
+
[5, 6],
|
|
363
|
+
[-5, 6],
|
|
364
|
+
[-9, 2]
|
|
365
|
+
]
|
|
366
|
+
})
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
const sockets = [
|
|
370
|
+
[-4.4, -1.8, 8],
|
|
371
|
+
[4.4, -1.8, 8],
|
|
372
|
+
[0, 3.1, 8]
|
|
373
|
+
];
|
|
374
|
+
body = subtract2(
|
|
375
|
+
body,
|
|
376
|
+
recess,
|
|
377
|
+
...sockets.map((center) => cuboid({ size: [2.3, 3.4, 16.2], center }))
|
|
378
|
+
);
|
|
379
|
+
return [
|
|
380
|
+
geometryToCableMesh({
|
|
381
|
+
geometry: body,
|
|
382
|
+
color: plastic,
|
|
383
|
+
name: "appliance-body"
|
|
384
|
+
}),
|
|
385
|
+
...sockets.map(
|
|
386
|
+
([x, y], index) => geometryToCableMesh({
|
|
387
|
+
geometry: cuboid({ size: [0.15, 2.8, 6], center: [x - 1.05, y, 7.8] }),
|
|
388
|
+
color: brass,
|
|
389
|
+
name: `socket-contact-${index + 1}`
|
|
390
|
+
})
|
|
391
|
+
)
|
|
392
|
+
];
|
|
393
|
+
}
|
|
394
|
+
function createConnectorMeshes({
|
|
395
|
+
connector,
|
|
396
|
+
jacketDiameter
|
|
397
|
+
}) {
|
|
398
|
+
const meshes = connector.kind === "usb_c_plug" ? createUsbMeshes(connector) : "pinCount" in connector ? createJstMeshes(connector) : createMainsMeshes(connector);
|
|
399
|
+
if (jacketDiameter !== void 0) {
|
|
400
|
+
const exit = connectorWireExitDepth(connector);
|
|
401
|
+
const boot = union(
|
|
402
|
+
cylinder({
|
|
403
|
+
radius: jacketDiameter / 2 + 0.15,
|
|
404
|
+
height: 5,
|
|
405
|
+
center: [0, 0, exit + 2.5],
|
|
406
|
+
segments: 32
|
|
407
|
+
}),
|
|
408
|
+
...Array.from(
|
|
409
|
+
{ length: 7 },
|
|
410
|
+
(_, index) => cylinder({
|
|
411
|
+
radius: jacketDiameter / 2 + 0.8 - index * 0.06,
|
|
412
|
+
height: 0.28,
|
|
413
|
+
center: [0, 0, exit + 0.4 + index * 0.7],
|
|
414
|
+
segments: 32
|
|
415
|
+
})
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
meshes.push(
|
|
419
|
+
geometryToCableMesh({
|
|
420
|
+
geometry: boot,
|
|
421
|
+
color: plastic,
|
|
422
|
+
name: "strain-relief"
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return meshes;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// lib/cables/sweep-round-cable.ts
|
|
430
|
+
function sweepRoundCable({
|
|
431
|
+
frames,
|
|
432
|
+
diameter,
|
|
433
|
+
offset = 0,
|
|
434
|
+
radialSegments,
|
|
435
|
+
color,
|
|
436
|
+
name
|
|
437
|
+
}) {
|
|
438
|
+
const positions = [];
|
|
439
|
+
const indices = [];
|
|
440
|
+
for (const frame of frames) {
|
|
441
|
+
const center = add(frame.point, scale(frame.normal, offset));
|
|
442
|
+
for (let side = 0; side < radialSegments; side++) {
|
|
443
|
+
const angle = side * Math.PI * 2 / radialSegments;
|
|
444
|
+
positions.push(
|
|
445
|
+
...add(
|
|
446
|
+
center,
|
|
447
|
+
add(
|
|
448
|
+
scale(frame.normal, diameter * Math.cos(angle) / 2),
|
|
449
|
+
scale(frame.binormal, diameter * Math.sin(angle) / 2)
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (let ring = 0; ring < frames.length - 1; ring++) {
|
|
456
|
+
for (let side = 0; side < radialSegments; side++) {
|
|
457
|
+
const a = ring * radialSegments + side;
|
|
458
|
+
const b = ring * radialSegments + (side + 1) % radialSegments;
|
|
459
|
+
const c = a + radialSegments;
|
|
460
|
+
const d = b + radialSegments;
|
|
461
|
+
indices.push(a, b, c, b, d, c);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const startCenter = positions.length / 3;
|
|
465
|
+
positions.push(...add(frames[0].point, scale(frames[0].normal, offset)));
|
|
466
|
+
const endCenter = positions.length / 3;
|
|
467
|
+
positions.push(
|
|
468
|
+
...add(frames.at(-1).point, scale(frames.at(-1).normal, offset))
|
|
469
|
+
);
|
|
470
|
+
const lastRing = (frames.length - 1) * radialSegments;
|
|
471
|
+
for (let side = 0; side < radialSegments; side++) {
|
|
472
|
+
const next = (side + 1) % radialSegments;
|
|
473
|
+
indices.push(
|
|
474
|
+
startCenter,
|
|
475
|
+
next,
|
|
476
|
+
side,
|
|
477
|
+
endCenter,
|
|
478
|
+
lastRing + side,
|
|
479
|
+
lastRing + next
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return { name, positions, indices, color, smooth: true };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// lib/cables/create-cable-meshes.ts
|
|
486
|
+
function parseCableColor(color) {
|
|
487
|
+
if (!/^#[a-f\d]{6}$/i.test(color))
|
|
488
|
+
throw new Error("Cable colors must be six-digit hex strings");
|
|
489
|
+
return [
|
|
490
|
+
parseInt(color.slice(1, 3), 16) / 255,
|
|
491
|
+
parseInt(color.slice(3, 5), 16) / 255,
|
|
492
|
+
parseInt(color.slice(5, 7), 16) / 255,
|
|
493
|
+
1
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
function validateCableDefinition(definition) {
|
|
497
|
+
for (const connector of [definition.connectorA, definition.connectorB]) {
|
|
498
|
+
const dimensions = [
|
|
499
|
+
connector.bodyWidth,
|
|
500
|
+
connector.bodyHeight,
|
|
501
|
+
connector.bodyDepth
|
|
502
|
+
];
|
|
503
|
+
if (connector.kind === "usb_c_plug")
|
|
504
|
+
dimensions.push(
|
|
505
|
+
connector.shellWidth,
|
|
506
|
+
connector.shellHeight,
|
|
507
|
+
connector.shellDepth
|
|
508
|
+
);
|
|
509
|
+
if (dimensions.some(
|
|
510
|
+
(dimension) => !Number.isFinite(dimension) || dimension <= 0
|
|
511
|
+
))
|
|
512
|
+
throw new Error("Connector dimensions must be finite and positive");
|
|
513
|
+
if ("pinCount" in connector && (!Number.isInteger(connector.pinCount) || connector.pinCount < 2 || !Number.isFinite(connector.pitch) || connector.pitch <= 0))
|
|
514
|
+
throw new Error(
|
|
515
|
+
"JST connectors need a positive pitch and at least two contacts"
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const crossSection = definition.crossSection;
|
|
519
|
+
const diameters = crossSection.kind === "round_jacket" ? [crossSection.diameter] : crossSection.wires.map((wire) => wire.diameter);
|
|
520
|
+
if (diameters.length === 0 || diameters.some((diameter) => !Number.isFinite(diameter) || diameter <= 0))
|
|
521
|
+
throw new Error("Cable diameters must be finite and positive");
|
|
522
|
+
if (crossSection.kind === "wire_bundle") {
|
|
523
|
+
if (!Number.isFinite(crossSection.wirePitch) || crossSection.wires.some((wire) => wire.diameter > crossSection.wirePitch))
|
|
524
|
+
throw new Error("Insulated bundle wires must not overlap");
|
|
525
|
+
for (const connector of [definition.connectorA, definition.connectorB]) {
|
|
526
|
+
if (!("pinCount" in connector) || connector.pinCount !== crossSection.wires.length)
|
|
527
|
+
throw new Error("Bundle wire count must match connector contact count");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function createCableMeshes({
|
|
532
|
+
definition,
|
|
533
|
+
path,
|
|
534
|
+
radialSegments = 24
|
|
535
|
+
}) {
|
|
536
|
+
validateCableDefinition(definition);
|
|
537
|
+
if (!Number.isInteger(radialSegments) || radialSegments < 8 || radialSegments > 128)
|
|
538
|
+
throw new Error("radialSegments must be an integer from 8 to 128");
|
|
539
|
+
const frames = createCablePathFrames(path);
|
|
540
|
+
const crossSection = definition.crossSection;
|
|
541
|
+
const jacketDiameter = crossSection.kind === "round_jacket" ? crossSection.diameter : void 0;
|
|
542
|
+
const meshes = crossSection.kind === "round_jacket" ? [
|
|
543
|
+
sweepRoundCable({
|
|
544
|
+
frames,
|
|
545
|
+
diameter: crossSection.diameter,
|
|
546
|
+
radialSegments,
|
|
547
|
+
color: parseCableColor(crossSection.color),
|
|
548
|
+
name: "jacket"
|
|
549
|
+
})
|
|
550
|
+
] : crossSection.wires.map(
|
|
551
|
+
(wire, index) => sweepRoundCable({
|
|
552
|
+
frames,
|
|
553
|
+
diameter: wire.diameter,
|
|
554
|
+
offset: (index - (crossSection.wires.length - 1) / 2) * crossSection.wirePitch,
|
|
555
|
+
radialSegments,
|
|
556
|
+
color: parseCableColor(wire.color),
|
|
557
|
+
name: `wire-${index + 1}`
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
for (const [index, connector] of [
|
|
561
|
+
definition.connectorA,
|
|
562
|
+
definition.connectorB
|
|
563
|
+
].entries()) {
|
|
564
|
+
const frame = index === 0 ? frames[0] : frames.at(-1);
|
|
565
|
+
const parts = createConnectorMeshes({ connector, jacketDiameter });
|
|
566
|
+
meshes.push(
|
|
567
|
+
...parts.map(
|
|
568
|
+
(mesh) => placeConnectorMesh({
|
|
569
|
+
mesh: { ...mesh, name: `${index === 0 ? "A" : "B"}-${mesh.name}` },
|
|
570
|
+
frame,
|
|
571
|
+
isEnd: index === 1,
|
|
572
|
+
wireExitDepth: connectorWireExitDepth(connector)
|
|
573
|
+
})
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
return meshes;
|
|
578
|
+
}
|
|
579
|
+
export {
|
|
580
|
+
createCableMeshes
|
|
581
|
+
};
|
|
582
|
+
//# sourceMappingURL=cables.js.map
|