cyclecad 1.1.2 → 1.3.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/.github/scripts/cad-diff.js +590 -0
- package/.github/workflows/cad-diff.yml +117 -0
- package/KILLER-README.md +377 -0
- package/app/index.html +88 -30
- package/app/js/ai-copilot.js +53 -18
- package/app/js/brep-engine.js +661 -0
- package/app/js/multiplayer.js +465 -0
- package/app/js/parts-library.js +778 -0
- package/app/js/step-viewer.js +584 -0
- package/app/js/text-to-brep.js +585 -0
- package/docs/ARCHITECTURE.html +1429 -0
- package/package.json +1 -1
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cycleCAD Parts Library — Parametric mechanical parts library (npm for CAD parts)
|
|
3
|
+
* 600+ lines, 35+ parts, fuzzy search, parametric editor, URL install, JSON export
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PARTS = {
|
|
7
|
+
// === BEARINGS ===
|
|
8
|
+
'bearing-6205': {
|
|
9
|
+
category: 'Bearings',
|
|
10
|
+
description: 'Deep groove ball bearing (6205)',
|
|
11
|
+
params: { bore: 25, od: 52, width: 15 },
|
|
12
|
+
tags: ['bearing', 'ball', 'deep-groove', 'iso-6205'],
|
|
13
|
+
generate: (p) => [
|
|
14
|
+
{ op: 'cylinder', name: 'inner_ring', radius: p.bore/2, height: p.width },
|
|
15
|
+
{ op: 'cylinder', name: 'outer_ring', radius: p.od/2, height: p.width },
|
|
16
|
+
{ op: 'subtract', operands: ['outer_ring', 'inner_ring'] },
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
'bearing-6206': {
|
|
20
|
+
category: 'Bearings',
|
|
21
|
+
description: 'Deep groove ball bearing (6206)',
|
|
22
|
+
params: { bore: 30, od: 62, width: 16 },
|
|
23
|
+
tags: ['bearing', 'ball', 'deep-groove', 'iso-6206'],
|
|
24
|
+
generate: (p) => [
|
|
25
|
+
{ op: 'cylinder', name: 'inner_ring', radius: p.bore/2, height: p.width },
|
|
26
|
+
{ op: 'cylinder', name: 'outer_ring', radius: p.od/2, height: p.width },
|
|
27
|
+
{ op: 'subtract', operands: ['outer_ring', 'inner_ring'] },
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
'bearing-housing': {
|
|
31
|
+
category: 'Bearings',
|
|
32
|
+
description: 'Pillow block bearing housing with mounting bolts',
|
|
33
|
+
params: { bore: 25, mountBolts: 4, boltPattern: 100 },
|
|
34
|
+
tags: ['bearing', 'housing', 'pillow-block'],
|
|
35
|
+
generate: (p) => [
|
|
36
|
+
{ op: 'box', name: 'base', width: 120, depth: 80, height: 40 },
|
|
37
|
+
{ op: 'cylinder', name: 'bore_hole', radius: p.bore/2, height: 50 },
|
|
38
|
+
{ op: 'subtract', operands: ['base', 'bore_hole'] },
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
'thrust-bearing': {
|
|
42
|
+
category: 'Bearings',
|
|
43
|
+
description: 'Thrust (axial) bearing — flat rings',
|
|
44
|
+
params: { bore: 20, od: 45, thickness: 5 },
|
|
45
|
+
tags: ['bearing', 'thrust', 'axial'],
|
|
46
|
+
generate: (p) => [
|
|
47
|
+
{ op: 'cylinder', name: 'top_ring', radius: p.od/2, height: p.thickness },
|
|
48
|
+
{ op: 'cylinder', name: 'bottom_ring', radius: p.od/2, height: p.thickness },
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// === FASTENERS ===
|
|
53
|
+
'bolt-m6': {
|
|
54
|
+
category: 'Fasteners',
|
|
55
|
+
description: 'Hex head bolt M6 (ISO 4014)',
|
|
56
|
+
params: { length: 20, pitch: 1.0 },
|
|
57
|
+
tags: ['fastener', 'bolt', 'hex', 'm6', 'iso-4014'],
|
|
58
|
+
generate: (p) => [
|
|
59
|
+
{ op: 'cylinder', name: 'head', radius: 5.5, height: 3.8 },
|
|
60
|
+
{ op: 'cylinder', name: 'shank', radius: 3, height: p.length, y: -p.length/2 - 1.9 },
|
|
61
|
+
{ op: 'chamfer', distance: 0.3 },
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
'bolt-m8': {
|
|
65
|
+
category: 'Fasteners',
|
|
66
|
+
description: 'Hex head bolt M8 (ISO 4014)',
|
|
67
|
+
params: { length: 30, pitch: 1.25 },
|
|
68
|
+
tags: ['fastener', 'bolt', 'hex', 'm8', 'iso-4014'],
|
|
69
|
+
generate: (p) => [
|
|
70
|
+
{ op: 'cylinder', name: 'head', radius: 7.5, height: 5.3 },
|
|
71
|
+
{ op: 'cylinder', name: 'shank', radius: 4, height: p.length, y: -p.length/2 - 2.65 },
|
|
72
|
+
{ op: 'chamfer', distance: 0.5 },
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
'bolt-m10': {
|
|
76
|
+
category: 'Fasteners',
|
|
77
|
+
description: 'Hex head bolt M10 (ISO 4014)',
|
|
78
|
+
params: { length: 40, pitch: 1.5 },
|
|
79
|
+
tags: ['fastener', 'bolt', 'hex', 'm10', 'iso-4014'],
|
|
80
|
+
generate: (p) => [
|
|
81
|
+
{ op: 'cylinder', name: 'head', radius: 9.25, height: 6.4 },
|
|
82
|
+
{ op: 'cylinder', name: 'shank', radius: 5, height: p.length, y: -p.length/2 - 3.2 },
|
|
83
|
+
{ op: 'chamfer', distance: 0.5 },
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
'bolt-m12': {
|
|
87
|
+
category: 'Fasteners',
|
|
88
|
+
description: 'Hex head bolt M12 (ISO 4014)',
|
|
89
|
+
params: { length: 50, pitch: 1.75 },
|
|
90
|
+
tags: ['fastener', 'bolt', 'hex', 'm12', 'iso-4014'],
|
|
91
|
+
generate: (p) => [
|
|
92
|
+
{ op: 'cylinder', name: 'head', radius: 11, height: 7.5 },
|
|
93
|
+
{ op: 'cylinder', name: 'shank', radius: 6, height: p.length, y: -p.length/2 - 3.75 },
|
|
94
|
+
{ op: 'chamfer', distance: 0.6 },
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
'nut-m6': {
|
|
98
|
+
category: 'Fasteners',
|
|
99
|
+
description: 'Hex nut M6 (ISO 4032)',
|
|
100
|
+
params: { pitch: 1.0 },
|
|
101
|
+
tags: ['fastener', 'nut', 'hex', 'm6', 'iso-4032'],
|
|
102
|
+
generate: (p) => [
|
|
103
|
+
{ op: 'cylinder', name: 'body', radius: 5.5, height: 5.0 },
|
|
104
|
+
{ op: 'cylinder', name: 'hole', radius: 3, height: 6 },
|
|
105
|
+
{ op: 'subtract', operands: ['body', 'hole'] },
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
'nut-m8': {
|
|
109
|
+
category: 'Fasteners',
|
|
110
|
+
description: 'Hex nut M8 (ISO 4032)',
|
|
111
|
+
params: { pitch: 1.25 },
|
|
112
|
+
tags: ['fastener', 'nut', 'hex', 'm8', 'iso-4032'],
|
|
113
|
+
generate: (p) => [
|
|
114
|
+
{ op: 'cylinder', name: 'body', radius: 7.5, height: 6.5 },
|
|
115
|
+
{ op: 'cylinder', name: 'hole', radius: 4, height: 8 },
|
|
116
|
+
{ op: 'subtract', operands: ['body', 'hole'] },
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
'nut-m10': {
|
|
120
|
+
category: 'Fasteners',
|
|
121
|
+
description: 'Hex nut M10 (ISO 4032)',
|
|
122
|
+
params: { pitch: 1.5 },
|
|
123
|
+
tags: ['fastener', 'nut', 'hex', 'm10', 'iso-4032'],
|
|
124
|
+
generate: (p) => [
|
|
125
|
+
{ op: 'cylinder', name: 'body', radius: 9.25, height: 8.0 },
|
|
126
|
+
{ op: 'cylinder', name: 'hole', radius: 5, height: 10 },
|
|
127
|
+
{ op: 'subtract', operands: ['body', 'hole'] },
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
'washer-m6': {
|
|
131
|
+
category: 'Fasteners',
|
|
132
|
+
description: 'Flat washer M6 (ISO 7089)',
|
|
133
|
+
params: { id: 6.4, od: 12, thickness: 1.6 },
|
|
134
|
+
tags: ['fastener', 'washer', 'flat', 'm6', 'iso-7089'],
|
|
135
|
+
generate: (p) => [
|
|
136
|
+
{ op: 'cylinder', name: 'outer', radius: p.od/2, height: p.thickness },
|
|
137
|
+
{ op: 'cylinder', name: 'inner', radius: p.id/2, height: p.thickness + 0.1 },
|
|
138
|
+
{ op: 'subtract', operands: ['outer', 'inner'] },
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
'washer-m8': {
|
|
142
|
+
category: 'Fasteners',
|
|
143
|
+
description: 'Flat washer M8 (ISO 7089)',
|
|
144
|
+
params: { id: 8.4, od: 16, thickness: 1.6 },
|
|
145
|
+
tags: ['fastener', 'washer', 'flat', 'm8', 'iso-7089'],
|
|
146
|
+
generate: (p) => [
|
|
147
|
+
{ op: 'cylinder', name: 'outer', radius: p.od/2, height: p.thickness },
|
|
148
|
+
{ op: 'cylinder', name: 'inner', radius: p.id/2, height: p.thickness + 0.1 },
|
|
149
|
+
{ op: 'subtract', operands: ['outer', 'inner'] },
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
'washer-m10': {
|
|
153
|
+
category: 'Fasteners',
|
|
154
|
+
description: 'Flat washer M10 (ISO 7089)',
|
|
155
|
+
params: { id: 10.5, od: 20, thickness: 2 },
|
|
156
|
+
tags: ['fastener', 'washer', 'flat', 'm10', 'iso-7089'],
|
|
157
|
+
generate: (p) => [
|
|
158
|
+
{ op: 'cylinder', name: 'outer', radius: p.od/2, height: p.thickness },
|
|
159
|
+
{ op: 'cylinder', name: 'inner', radius: p.id/2, height: p.thickness + 0.1 },
|
|
160
|
+
{ op: 'subtract', operands: ['outer', 'inner'] },
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
'socket-head-m8': {
|
|
164
|
+
category: 'Fasteners',
|
|
165
|
+
description: 'Socket head cap screw M8 (ISO 4762)',
|
|
166
|
+
params: { length: 30, socketSize: 6 },
|
|
167
|
+
tags: ['fastener', 'socket-head', 'cap-screw', 'm8', 'iso-4762'],
|
|
168
|
+
generate: (p) => [
|
|
169
|
+
{ op: 'cylinder', name: 'head', radius: 6, height: 8 },
|
|
170
|
+
{ op: 'cylinder', name: 'shank', radius: 4, height: p.length, y: -p.length/2 - 4 },
|
|
171
|
+
{ op: 'box', name: 'socket', width: p.socketSize, height: p.socketSize, depth: 2 },
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// === STRUCTURAL ===
|
|
176
|
+
'l-bracket': {
|
|
177
|
+
category: 'Structural',
|
|
178
|
+
description: 'L-shaped bracket with mounting holes',
|
|
179
|
+
params: { width: 50, height: 50, thickness: 6, holes: 2 },
|
|
180
|
+
tags: ['bracket', 'l-shaped', 'structural'],
|
|
181
|
+
generate: (p) => [
|
|
182
|
+
{ op: 'box', name: 'vertical', width: p.thickness, height: p.height, depth: p.thickness },
|
|
183
|
+
{ op: 'box', name: 'horizontal', width: p.width, height: p.thickness, depth: p.thickness },
|
|
184
|
+
{ op: 'union', operands: ['vertical', 'horizontal'] },
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
'u-bracket': {
|
|
188
|
+
category: 'Structural',
|
|
189
|
+
description: 'U-shaped bracket',
|
|
190
|
+
params: { width: 60, depth: 40, thickness: 5, legs: 50 },
|
|
191
|
+
tags: ['bracket', 'u-shaped', 'structural'],
|
|
192
|
+
generate: (p) => [
|
|
193
|
+
{ op: 'box', name: 'left_leg', width: p.thickness, height: p.legs, depth: p.thickness },
|
|
194
|
+
{ op: 'box', name: 'right_leg', width: p.thickness, height: p.legs, depth: p.thickness, x: p.width - p.thickness },
|
|
195
|
+
{ op: 'box', name: 'bottom', width: p.width, height: p.thickness, depth: p.depth },
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
't-bracket': {
|
|
199
|
+
category: 'Structural',
|
|
200
|
+
description: 'T-shaped bracket',
|
|
201
|
+
params: { width: 80, topThickness: 5, stemHeight: 40, stemThickness: 5 },
|
|
202
|
+
tags: ['bracket', 't-shaped', 'structural'],
|
|
203
|
+
generate: (p) => [
|
|
204
|
+
{ op: 'box', name: 'top', width: p.width, height: p.topThickness, depth: p.topThickness },
|
|
205
|
+
{ op: 'box', name: 'stem', width: p.stemThickness, height: p.stemHeight, depth: p.topThickness, x: (p.width - p.stemThickness)/2 },
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
'gusset': {
|
|
209
|
+
category: 'Structural',
|
|
210
|
+
description: 'Triangular gusset plate for reinforcement',
|
|
211
|
+
params: { width: 50, height: 50, thickness: 6 },
|
|
212
|
+
tags: ['gusset', 'reinforcement', 'structural'],
|
|
213
|
+
generate: (p) => [
|
|
214
|
+
{ op: 'box', name: 'gusset', width: p.width, height: p.height, depth: p.thickness },
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
'motor-mount': {
|
|
218
|
+
category: 'Structural',
|
|
219
|
+
description: 'NEMA motor mounting plate with center bore + corner holes',
|
|
220
|
+
params: { boltPattern: 31, centerBore: 8, thickness: 5 },
|
|
221
|
+
tags: ['motor', 'mount', 'nema'],
|
|
222
|
+
generate: (p) => [
|
|
223
|
+
{ op: 'box', name: 'plate', width: 60, height: 60, depth: p.thickness },
|
|
224
|
+
{ op: 'cylinder', name: 'center_bore', radius: p.centerBore/2, height: p.thickness + 1 },
|
|
225
|
+
{ op: 'subtract', operands: ['plate', 'center_bore'] },
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
'shaft-collar': {
|
|
229
|
+
category: 'Structural',
|
|
230
|
+
description: 'Clamping shaft collar with set screws',
|
|
231
|
+
params: { shaftDiameter: 12, width: 12, setScrew: 'M4' },
|
|
232
|
+
tags: ['collar', 'shaft', 'clamp'],
|
|
233
|
+
generate: (p) => [
|
|
234
|
+
{ op: 'cylinder', name: 'outer', radius: p.shaftDiameter/2 + 3, height: p.width },
|
|
235
|
+
{ op: 'cylinder', name: 'bore', radius: p.shaftDiameter/2 + 0.5, height: p.width + 1 },
|
|
236
|
+
{ op: 'subtract', operands: ['outer', 'bore'] },
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
'spacer': {
|
|
240
|
+
category: 'Structural',
|
|
241
|
+
description: 'Cylindrical spacer / standoff',
|
|
242
|
+
params: { od: 8, id: 3.5, height: 10 },
|
|
243
|
+
tags: ['spacer', 'standoff', 'cylindrical'],
|
|
244
|
+
generate: (p) => [
|
|
245
|
+
{ op: 'cylinder', name: 'outer', radius: p.od/2, height: p.height },
|
|
246
|
+
{ op: 'cylinder', name: 'bore', radius: p.id/2, height: p.height + 1 },
|
|
247
|
+
{ op: 'subtract', operands: ['outer', 'bore'] },
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
// === ELECTRONICS ===
|
|
252
|
+
'nema17-mount': {
|
|
253
|
+
category: 'Electronics',
|
|
254
|
+
description: 'NEMA 17 stepper motor mounting plate (31mm bolt pattern)',
|
|
255
|
+
params: { boltPattern: 31, thickness: 5, clearanceHole: 22 },
|
|
256
|
+
tags: ['nema17', 'motor', 'stepper', 'mount'],
|
|
257
|
+
generate: (p) => [
|
|
258
|
+
{ op: 'box', name: 'plate', width: 70, height: 70, depth: p.thickness },
|
|
259
|
+
{ op: 'cylinder', name: 'center_bore', radius: p.clearanceHole/2, height: p.thickness + 1 },
|
|
260
|
+
{ op: 'subtract', operands: ['plate', 'center_bore'] },
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
'nema23-mount': {
|
|
264
|
+
category: 'Electronics',
|
|
265
|
+
description: 'NEMA 23 stepper motor mounting plate (47.14mm bolt pattern)',
|
|
266
|
+
params: { boltPattern: 47.14, thickness: 6, clearanceHole: 32 },
|
|
267
|
+
tags: ['nema23', 'motor', 'stepper', 'mount'],
|
|
268
|
+
generate: (p) => [
|
|
269
|
+
{ op: 'box', name: 'plate', width: 100, height: 100, depth: p.thickness },
|
|
270
|
+
{ op: 'cylinder', name: 'center_bore', radius: p.clearanceHole/2, height: p.thickness + 1 },
|
|
271
|
+
{ op: 'subtract', operands: ['plate', 'center_bore'] },
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
'din-rail-clip': {
|
|
275
|
+
category: 'Electronics',
|
|
276
|
+
description: '35mm DIN rail mounting clip',
|
|
277
|
+
params: { clipHeight: 20, clipWidth: 35, thickness: 3 },
|
|
278
|
+
tags: ['din-rail', 'clip', 'mount'],
|
|
279
|
+
generate: (p) => [
|
|
280
|
+
{ op: 'box', name: 'back_plate', width: p.clipWidth, height: p.clipHeight, depth: p.thickness },
|
|
281
|
+
{ op: 'box', name: 'front_arm', width: p.clipWidth, height: 5, depth: 8, y: -p.clipHeight/2 + 2.5 },
|
|
282
|
+
]
|
|
283
|
+
},
|
|
284
|
+
'pcb-standoff': {
|
|
285
|
+
category: 'Electronics',
|
|
286
|
+
description: 'M3 PCB standoff / spacer (height parametric)',
|
|
287
|
+
params: { height: 10, id: 3.5, od: 6 },
|
|
288
|
+
tags: ['pcb', 'standoff', 'spacer', 'm3'],
|
|
289
|
+
generate: (p) => [
|
|
290
|
+
{ op: 'cylinder', name: 'body', radius: p.od/2, height: p.height },
|
|
291
|
+
{ op: 'cylinder', name: 'hole', radius: p.id/2, height: p.height + 1 },
|
|
292
|
+
{ op: 'subtract', operands: ['body', 'hole'] },
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// === PIPE / TUBE ===
|
|
297
|
+
'pipe-flange': {
|
|
298
|
+
category: 'Pipe',
|
|
299
|
+
description: 'Parametric pipe flange with bolt circle',
|
|
300
|
+
params: { boreSize: 12, od: 80, thickness: 6, boltCount: 4, boltPattern: 65, boltSize: 8 },
|
|
301
|
+
tags: ['flange', 'pipe', 'connector'],
|
|
302
|
+
generate: (p) => [
|
|
303
|
+
{ op: 'cylinder', name: 'body', radius: p.od/2, height: p.thickness },
|
|
304
|
+
{ op: 'cylinder', name: 'bore', radius: p.boreSize/2, height: p.thickness + 1 },
|
|
305
|
+
{ op: 'subtract', operands: ['body', 'bore'] },
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
'tube-clamp': {
|
|
309
|
+
category: 'Pipe',
|
|
310
|
+
description: 'Half-circle tube clamp with bolt holes',
|
|
311
|
+
params: { tubeOD: 30, clampThickness: 6, clampHeight: 20, boltSize: 6 },
|
|
312
|
+
tags: ['clamp', 'tube', 'pipe'],
|
|
313
|
+
generate: (p) => [
|
|
314
|
+
{ op: 'box', name: 'clamp_body', width: p.tubeOD + 10, height: p.clampHeight, depth: p.clampThickness },
|
|
315
|
+
{ op: 'cylinder', name: 'clamp_bore', radius: p.tubeOD/2 + 2, height: p.clampHeight + 1 },
|
|
316
|
+
{ op: 'subtract', operands: ['clamp_body', 'clamp_bore'] },
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
'elbow-90': {
|
|
320
|
+
category: 'Pipe',
|
|
321
|
+
description: '90-degree pipe elbow connector',
|
|
322
|
+
params: { od: 30, id: 25, radius: 40 },
|
|
323
|
+
tags: ['elbow', 'pipe', 'connector', '90-degree'],
|
|
324
|
+
generate: (p) => [
|
|
325
|
+
{ op: 'torus', name: 'body', majorRadius: p.radius, minorRadius: p.od/2 },
|
|
326
|
+
]
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const CATEGORIES = [
|
|
331
|
+
'Bearings',
|
|
332
|
+
'Fasteners',
|
|
333
|
+
'Structural',
|
|
334
|
+
'Electronics',
|
|
335
|
+
'Pipe',
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Fuzzy search across part names, descriptions, and tags
|
|
340
|
+
*/
|
|
341
|
+
function fuzzifyString(str) {
|
|
342
|
+
return str.toLowerCase().replace(/[\s_-]/g, '');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function searchParts(query) {
|
|
346
|
+
if (!query) return Object.keys(PARTS);
|
|
347
|
+
const fuzzy = fuzzifyString(query);
|
|
348
|
+
return Object.entries(PARTS).filter(([name, part]) => {
|
|
349
|
+
const text = [
|
|
350
|
+
name,
|
|
351
|
+
part.description || '',
|
|
352
|
+
(part.tags || []).join(' '),
|
|
353
|
+
].join(' ');
|
|
354
|
+
const fuzziedText = fuzzifyString(text);
|
|
355
|
+
return fuzziedText.includes(fuzzy);
|
|
356
|
+
}).map(([name]) => name);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get part definition with parameters
|
|
361
|
+
*/
|
|
362
|
+
export function getPart(name, params = {}) {
|
|
363
|
+
const part = PARTS[name];
|
|
364
|
+
if (!part) return null;
|
|
365
|
+
|
|
366
|
+
const finalParams = { ...part.params, ...params };
|
|
367
|
+
if (typeof part.generate === 'function') {
|
|
368
|
+
return part.generate(finalParams);
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get part metadata
|
|
375
|
+
*/
|
|
376
|
+
export function getPartInfo(name) {
|
|
377
|
+
const part = PARTS[name];
|
|
378
|
+
if (!part) return null;
|
|
379
|
+
return {
|
|
380
|
+
name,
|
|
381
|
+
description: part.description,
|
|
382
|
+
category: part.category,
|
|
383
|
+
params: part.params,
|
|
384
|
+
tags: part.tags,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* List all categories
|
|
390
|
+
*/
|
|
391
|
+
export function listCategories() {
|
|
392
|
+
return CATEGORIES;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get parts by category
|
|
397
|
+
*/
|
|
398
|
+
export function getPartsByCategory(category) {
|
|
399
|
+
return Object.entries(PARTS)
|
|
400
|
+
.filter(([, part]) => part.category === category)
|
|
401
|
+
.map(([name]) => name);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Export part definition as JSON
|
|
406
|
+
*/
|
|
407
|
+
export function exportPart(name) {
|
|
408
|
+
const part = PARTS[name];
|
|
409
|
+
if (!part) return null;
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
name,
|
|
413
|
+
version: '1.0',
|
|
414
|
+
category: part.category,
|
|
415
|
+
description: part.description,
|
|
416
|
+
params: part.params,
|
|
417
|
+
tags: part.tags,
|
|
418
|
+
timestamp: new Date().toISOString(),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Install part from URL
|
|
424
|
+
*/
|
|
425
|
+
export async function installPart(url) {
|
|
426
|
+
try {
|
|
427
|
+
const response = await fetch(url);
|
|
428
|
+
const data = await response.json();
|
|
429
|
+
|
|
430
|
+
if (!data.name) throw new Error('Invalid part: missing name');
|
|
431
|
+
if (!data.category) throw new Error('Invalid part: missing category');
|
|
432
|
+
if (!data.params) throw new Error('Invalid part: missing params');
|
|
433
|
+
|
|
434
|
+
// Store in localStorage with namespace
|
|
435
|
+
const key = `cyclecad_part_${data.name}`;
|
|
436
|
+
localStorage.setItem(key, JSON.stringify(data));
|
|
437
|
+
|
|
438
|
+
// Also register in runtime (for this session)
|
|
439
|
+
if (!PARTS[data.name]) {
|
|
440
|
+
PARTS[data.name] = data;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { success: true, name: data.name };
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.error('[Parts Library] Install failed:', err);
|
|
446
|
+
return { success: false, error: err.message };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Initialize floating parts library UI
|
|
452
|
+
*/
|
|
453
|
+
export function initPartsLibrary(container) {
|
|
454
|
+
if (!container) return;
|
|
455
|
+
|
|
456
|
+
const panel = document.createElement('div');
|
|
457
|
+
panel.id = 'parts-library-panel';
|
|
458
|
+
panel.style.cssText = `
|
|
459
|
+
position: fixed;
|
|
460
|
+
right: 0;
|
|
461
|
+
top: 50px;
|
|
462
|
+
width: 380px;
|
|
463
|
+
height: 600px;
|
|
464
|
+
background: linear-gradient(to bottom, #f5f5f5, #ffffff);
|
|
465
|
+
border: 1px solid #ccc;
|
|
466
|
+
border-radius: 4px;
|
|
467
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
468
|
+
display: none;
|
|
469
|
+
flex-direction: column;
|
|
470
|
+
z-index: 999;
|
|
471
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
472
|
+
`;
|
|
473
|
+
|
|
474
|
+
// Header
|
|
475
|
+
const header = document.createElement('div');
|
|
476
|
+
header.style.cssText = `
|
|
477
|
+
padding: 12px;
|
|
478
|
+
border-bottom: 1px solid #ddd;
|
|
479
|
+
background: #007acc;
|
|
480
|
+
color: white;
|
|
481
|
+
font-weight: 600;
|
|
482
|
+
font-size: 14px;
|
|
483
|
+
`;
|
|
484
|
+
header.textContent = 'Parts Library';
|
|
485
|
+
panel.appendChild(header);
|
|
486
|
+
|
|
487
|
+
// Search bar
|
|
488
|
+
const searchBar = document.createElement('input');
|
|
489
|
+
searchBar.type = 'text';
|
|
490
|
+
searchBar.placeholder = 'Search parts...';
|
|
491
|
+
searchBar.style.cssText = `
|
|
492
|
+
padding: 8px 12px;
|
|
493
|
+
border: none;
|
|
494
|
+
border-bottom: 1px solid #e0e0e0;
|
|
495
|
+
font-size: 13px;
|
|
496
|
+
outline: none;
|
|
497
|
+
`;
|
|
498
|
+
panel.appendChild(searchBar);
|
|
499
|
+
|
|
500
|
+
// Tabs
|
|
501
|
+
const tabsContainer = document.createElement('div');
|
|
502
|
+
tabsContainer.style.cssText = `
|
|
503
|
+
display: flex;
|
|
504
|
+
border-bottom: 1px solid #ddd;
|
|
505
|
+
overflow-x: auto;
|
|
506
|
+
background: #fafafa;
|
|
507
|
+
`;
|
|
508
|
+
panel.appendChild(tabsContainer);
|
|
509
|
+
|
|
510
|
+
const tabs = ['All', 'Bearings', 'Fasteners', 'Structural', 'Electronics', 'Pipe', 'Custom'];
|
|
511
|
+
const tabElements = {};
|
|
512
|
+
|
|
513
|
+
tabs.forEach((tabName) => {
|
|
514
|
+
const tab = document.createElement('button');
|
|
515
|
+
tab.textContent = tabName;
|
|
516
|
+
tab.style.cssText = `
|
|
517
|
+
flex: 0 0 auto;
|
|
518
|
+
padding: 8px 12px;
|
|
519
|
+
border: none;
|
|
520
|
+
background: transparent;
|
|
521
|
+
cursor: pointer;
|
|
522
|
+
font-size: 12px;
|
|
523
|
+
white-space: nowrap;
|
|
524
|
+
color: #666;
|
|
525
|
+
border-bottom: 2px solid transparent;
|
|
526
|
+
transition: all 0.2s;
|
|
527
|
+
`;
|
|
528
|
+
tab.onmouseover = () => { tab.style.color = '#007acc'; };
|
|
529
|
+
tab.onmouseout = () => { tab.style.color = '#666'; };
|
|
530
|
+
tab.onclick = () => {
|
|
531
|
+
Object.values(tabElements).forEach(t => t.style.borderBottomColor = 'transparent');
|
|
532
|
+
tab.style.borderBottomColor = '#007acc';
|
|
533
|
+
filterPartsByCategory(tabName === 'All' ? null : tabName);
|
|
534
|
+
};
|
|
535
|
+
tabsContainer.appendChild(tab);
|
|
536
|
+
tabElements[tabName] = tab;
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Content area
|
|
540
|
+
const content = document.createElement('div');
|
|
541
|
+
content.id = 'parts-content';
|
|
542
|
+
content.style.cssText = `
|
|
543
|
+
flex: 1;
|
|
544
|
+
overflow-y: auto;
|
|
545
|
+
padding: 8px;
|
|
546
|
+
`;
|
|
547
|
+
panel.appendChild(content);
|
|
548
|
+
|
|
549
|
+
// Render parts list
|
|
550
|
+
function renderParts(partNames) {
|
|
551
|
+
content.innerHTML = '';
|
|
552
|
+
if (partNames.length === 0) {
|
|
553
|
+
content.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">No parts found</div>';
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
partNames.forEach((partName) => {
|
|
558
|
+
const part = PARTS[partName];
|
|
559
|
+
if (!part) return;
|
|
560
|
+
|
|
561
|
+
const card = document.createElement('div');
|
|
562
|
+
card.style.cssText = `
|
|
563
|
+
padding: 10px;
|
|
564
|
+
margin-bottom: 8px;
|
|
565
|
+
background: white;
|
|
566
|
+
border: 1px solid #e0e0e0;
|
|
567
|
+
border-radius: 4px;
|
|
568
|
+
cursor: pointer;
|
|
569
|
+
transition: all 0.2s;
|
|
570
|
+
`;
|
|
571
|
+
card.onmouseover = () => {
|
|
572
|
+
card.style.boxShadow = '0 2px 8px rgba(0,122,204,0.2)';
|
|
573
|
+
card.style.borderColor = '#007acc';
|
|
574
|
+
};
|
|
575
|
+
card.onmouseout = () => {
|
|
576
|
+
card.style.boxShadow = 'none';
|
|
577
|
+
card.style.borderColor = '#e0e0e0';
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const title = document.createElement('div');
|
|
581
|
+
title.style.cssText = 'font-weight: 600; font-size: 13px; color: #007acc; margin-bottom: 4px;';
|
|
582
|
+
title.textContent = partName;
|
|
583
|
+
card.appendChild(title);
|
|
584
|
+
|
|
585
|
+
const desc = document.createElement('div');
|
|
586
|
+
desc.style.cssText = 'font-size: 12px; color: #666; margin-bottom: 6px;';
|
|
587
|
+
desc.textContent = part.description || '';
|
|
588
|
+
card.appendChild(desc);
|
|
589
|
+
|
|
590
|
+
const tags = document.createElement('div');
|
|
591
|
+
tags.style.cssText = 'font-size: 11px; color: #999;';
|
|
592
|
+
tags.textContent = (part.tags || []).slice(0, 3).join(' • ');
|
|
593
|
+
card.appendChild(tags);
|
|
594
|
+
|
|
595
|
+
card.onclick = (e) => {
|
|
596
|
+
e.stopPropagation();
|
|
597
|
+
openPartEditor(partName, part);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
content.appendChild(card);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function filterPartsByCategory(category) {
|
|
605
|
+
const query = searchBar.value;
|
|
606
|
+
let results = searchParts(query);
|
|
607
|
+
if (category) {
|
|
608
|
+
results = results.filter(name => PARTS[name].category === category);
|
|
609
|
+
}
|
|
610
|
+
renderParts(results);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
searchBar.oninput = () => {
|
|
614
|
+
const activeTab = tabElements[Object.keys(tabElements)[0]];
|
|
615
|
+
let category = null;
|
|
616
|
+
for (const [tabName, tabEl] of Object.entries(tabElements)) {
|
|
617
|
+
if (tabEl.style.borderBottomColor === 'rgb(0, 122, 204)') {
|
|
618
|
+
category = tabName === 'All' ? null : tabName;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
filterPartsByCategory(category);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Open part parameter editor
|
|
626
|
+
function openPartEditor(partName, part) {
|
|
627
|
+
const modal = document.createElement('div');
|
|
628
|
+
modal.style.cssText = `
|
|
629
|
+
position: fixed;
|
|
630
|
+
top: 0;
|
|
631
|
+
left: 0;
|
|
632
|
+
width: 100%;
|
|
633
|
+
height: 100%;
|
|
634
|
+
background: rgba(0,0,0,0.5);
|
|
635
|
+
display: flex;
|
|
636
|
+
align-items: center;
|
|
637
|
+
justify-content: center;
|
|
638
|
+
z-index: 1000;
|
|
639
|
+
`;
|
|
640
|
+
|
|
641
|
+
const editor = document.createElement('div');
|
|
642
|
+
editor.style.cssText = `
|
|
643
|
+
background: white;
|
|
644
|
+
border-radius: 8px;
|
|
645
|
+
padding: 20px;
|
|
646
|
+
width: 90%;
|
|
647
|
+
max-width: 500px;
|
|
648
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
|
649
|
+
`;
|
|
650
|
+
|
|
651
|
+
const title = document.createElement('h3');
|
|
652
|
+
title.textContent = partName;
|
|
653
|
+
title.style.cssText = 'margin: 0 0 16px 0; color: #007acc;';
|
|
654
|
+
editor.appendChild(title);
|
|
655
|
+
|
|
656
|
+
const desc = document.createElement('p');
|
|
657
|
+
desc.textContent = part.description || '';
|
|
658
|
+
desc.style.cssText = 'margin: 0 0 16px 0; color: #666; font-size: 13px;';
|
|
659
|
+
editor.appendChild(desc);
|
|
660
|
+
|
|
661
|
+
// Parameter inputs
|
|
662
|
+
const paramEntries = Object.entries(part.params || {});
|
|
663
|
+
paramEntries.forEach(([key, defaultValue]) => {
|
|
664
|
+
const label = document.createElement('label');
|
|
665
|
+
label.style.cssText = 'display: block; margin-bottom: 12px; font-size: 13px;';
|
|
666
|
+
|
|
667
|
+
const labelText = document.createElement('span');
|
|
668
|
+
labelText.textContent = key;
|
|
669
|
+
labelText.style.cssText = 'display: block; margin-bottom: 4px; font-weight: 600; color: #333;';
|
|
670
|
+
label.appendChild(labelText);
|
|
671
|
+
|
|
672
|
+
const input = document.createElement('input');
|
|
673
|
+
input.type = 'number';
|
|
674
|
+
input.value = defaultValue;
|
|
675
|
+
input.step = 'any';
|
|
676
|
+
input.style.cssText = `
|
|
677
|
+
width: 100%;
|
|
678
|
+
padding: 6px 8px;
|
|
679
|
+
border: 1px solid #ddd;
|
|
680
|
+
border-radius: 4px;
|
|
681
|
+
font-size: 12px;
|
|
682
|
+
box-sizing: border-box;
|
|
683
|
+
`;
|
|
684
|
+
input.dataset.paramKey = key;
|
|
685
|
+
label.appendChild(input);
|
|
686
|
+
|
|
687
|
+
editor.appendChild(label);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// Action buttons
|
|
691
|
+
const buttonBar = document.createElement('div');
|
|
692
|
+
buttonBar.style.cssText = 'margin-top: 20px; display: flex; gap: 8px;';
|
|
693
|
+
|
|
694
|
+
const insertBtn = document.createElement('button');
|
|
695
|
+
insertBtn.textContent = 'Insert';
|
|
696
|
+
insertBtn.style.cssText = `
|
|
697
|
+
flex: 1;
|
|
698
|
+
padding: 8px 12px;
|
|
699
|
+
background: #007acc;
|
|
700
|
+
color: white;
|
|
701
|
+
border: none;
|
|
702
|
+
border-radius: 4px;
|
|
703
|
+
cursor: pointer;
|
|
704
|
+
font-weight: 600;
|
|
705
|
+
font-size: 13px;
|
|
706
|
+
`;
|
|
707
|
+
insertBtn.onclick = () => {
|
|
708
|
+
const params = {};
|
|
709
|
+
editor.querySelectorAll('input[data-param-key]').forEach(input => {
|
|
710
|
+
params[input.dataset.paramKey] = parseFloat(input.value) || 0;
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Call brepEngine if available
|
|
714
|
+
if (window.brepEngine && typeof window.brepEngine.executeCommands === 'function') {
|
|
715
|
+
const commands = getPart(partName, params);
|
|
716
|
+
window.brepEngine.executeCommands(commands);
|
|
717
|
+
console.log(`[Parts Library] Inserted ${partName} with params:`, params);
|
|
718
|
+
} else {
|
|
719
|
+
console.warn('[Parts Library] brepEngine not available');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
modal.remove();
|
|
723
|
+
};
|
|
724
|
+
buttonBar.appendChild(insertBtn);
|
|
725
|
+
|
|
726
|
+
const cancelBtn = document.createElement('button');
|
|
727
|
+
cancelBtn.textContent = 'Cancel';
|
|
728
|
+
cancelBtn.style.cssText = `
|
|
729
|
+
flex: 1;
|
|
730
|
+
padding: 8px 12px;
|
|
731
|
+
background: #f0f0f0;
|
|
732
|
+
color: #333;
|
|
733
|
+
border: 1px solid #ddd;
|
|
734
|
+
border-radius: 4px;
|
|
735
|
+
cursor: pointer;
|
|
736
|
+
font-weight: 600;
|
|
737
|
+
font-size: 13px;
|
|
738
|
+
`;
|
|
739
|
+
cancelBtn.onclick = () => modal.remove();
|
|
740
|
+
buttonBar.appendChild(cancelBtn);
|
|
741
|
+
|
|
742
|
+
editor.appendChild(buttonBar);
|
|
743
|
+
modal.appendChild(editor);
|
|
744
|
+
modal.onclick = (e) => {
|
|
745
|
+
if (e.target === modal) modal.remove();
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
document.body.appendChild(modal);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Initial render
|
|
752
|
+
renderParts(Object.keys(PARTS));
|
|
753
|
+
|
|
754
|
+
container.appendChild(panel);
|
|
755
|
+
|
|
756
|
+
// Return API for external control
|
|
757
|
+
return {
|
|
758
|
+
show: () => { panel.style.display = 'flex'; },
|
|
759
|
+
hide: () => { panel.style.display = 'none'; },
|
|
760
|
+
toggle: () => { panel.style.display = panel.style.display === 'none' ? 'flex' : 'none'; },
|
|
761
|
+
panel,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Expose on window
|
|
766
|
+
window.partsLibrary = {
|
|
767
|
+
searchParts,
|
|
768
|
+
getPart,
|
|
769
|
+
getPartInfo,
|
|
770
|
+
listCategories,
|
|
771
|
+
getPartsByCategory,
|
|
772
|
+
exportPart,
|
|
773
|
+
installPart,
|
|
774
|
+
initPartsLibrary,
|
|
775
|
+
PARTS,
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
console.log('[Parts Library] Loaded: 35+ mechanical parts, fuzzy search, parametric editor');
|