cyclecad 1.2.0 → 1.3.1

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.
@@ -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');