cyclecad 3.6.0 → 3.7.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.
@@ -0,0 +1,1755 @@
1
+ /**
2
+ * Smart Parts Library with AI Search
3
+ *
4
+ * Provides access to 200+ standard parts with:
5
+ * - AI-powered natural language search
6
+ * - 3D parametric geometry generation
7
+ * - Supplier part number cross-reference
8
+ * - BOM management and export
9
+ * - Real-time pricing estimation
10
+ *
11
+ * @namespace CycleCAD.SmartParts
12
+ * @requires THREE.js
13
+ */
14
+
15
+ window.CycleCAD = window.CycleCAD || {};
16
+
17
+ window.CycleCAD.SmartParts = (() => {
18
+ 'use strict';
19
+
20
+ // ============================================================================
21
+ // PART CATALOG DATABASE
22
+ // ============================================================================
23
+
24
+ const partCatalog = {
25
+ // Fasteners: ISO 4762 Socket Head Cap Screws
26
+ 'fastener_shcs_m3_8': {
27
+ id: 'fastener_shcs_m3_8',
28
+ name: 'Socket Head Cap Screw M3×8',
29
+ category: 'Fasteners',
30
+ subcategory: 'Socket Head Cap Screws',
31
+ standard: 'ISO 4762',
32
+ dimensions: { dia: 3, length: 8, headDia: 5.5, headHeight: 3 },
33
+ material: 'Steel',
34
+ finish: 'Zinc Plated',
35
+ weight: 0.5,
36
+ supplier: { mcmaster: '91251A030', misumi: 'SHCB-M3-8', digi: null },
37
+ price: { usd: 0.25, eur: 0.22 },
38
+ tags: ['screw', 'socket', 'head', 'cap', 'iso4762', 'fastener', 'm3']
39
+ },
40
+ 'fastener_shcs_m3_12': {
41
+ id: 'fastener_shcs_m3_12',
42
+ name: 'Socket Head Cap Screw M3×12',
43
+ category: 'Fasteners',
44
+ subcategory: 'Socket Head Cap Screws',
45
+ standard: 'ISO 4762',
46
+ dimensions: { dia: 3, length: 12, headDia: 5.5, headHeight: 3 },
47
+ material: 'Steel',
48
+ finish: 'Zinc Plated',
49
+ weight: 0.7,
50
+ supplier: { mcmaster: '91251A031', misumi: 'SHCB-M3-12', digi: null },
51
+ price: { usd: 0.30, eur: 0.27 },
52
+ tags: ['screw', 'socket', 'head', 'cap', 'iso4762', 'fastener', 'm3']
53
+ },
54
+ 'fastener_shcs_m4_10': {
55
+ id: 'fastener_shcs_m4_10',
56
+ name: 'Socket Head Cap Screw M4×10',
57
+ category: 'Fasteners',
58
+ subcategory: 'Socket Head Cap Screws',
59
+ standard: 'ISO 4762',
60
+ dimensions: { dia: 4, length: 10, headDia: 7, headHeight: 4 },
61
+ material: 'Steel',
62
+ finish: 'Zinc Plated',
63
+ weight: 1.0,
64
+ supplier: { mcmaster: '91251A003', misumi: 'SHCB-M4-10', digi: null },
65
+ price: { usd: 0.35, eur: 0.30 },
66
+ tags: ['screw', 'socket', 'head', 'cap', 'iso4762', 'fastener', 'm4']
67
+ },
68
+ 'fastener_shcs_m5_16': {
69
+ id: 'fastener_shcs_m5_16',
70
+ name: 'Socket Head Cap Screw M5×16',
71
+ category: 'Fasteners',
72
+ subcategory: 'Socket Head Cap Screws',
73
+ standard: 'ISO 4762',
74
+ dimensions: { dia: 5, length: 16, headDia: 8.5, headHeight: 5 },
75
+ material: 'Steel',
76
+ finish: 'Zinc Plated',
77
+ weight: 1.5,
78
+ supplier: { mcmaster: '91251A007', misumi: 'SHCB-M5-16', digi: null },
79
+ price: { usd: 0.45, eur: 0.40 },
80
+ tags: ['screw', 'socket', 'head', 'cap', 'iso4762', 'fastener', 'm5']
81
+ },
82
+ 'fastener_shcs_m6_20': {
83
+ id: 'fastener_shcs_m6_20',
84
+ name: 'Socket Head Cap Screw M6×20',
85
+ category: 'Fasteners',
86
+ subcategory: 'Socket Head Cap Screws',
87
+ standard: 'ISO 4762',
88
+ dimensions: { dia: 6, length: 20, headDia: 10, headHeight: 6 },
89
+ material: 'Steel',
90
+ finish: 'Zinc Plated',
91
+ weight: 2.2,
92
+ supplier: { mcmaster: '91251A010', misumi: 'SHCB-M6-20', digi: null },
93
+ price: { usd: 0.55, eur: 0.50 },
94
+ tags: ['screw', 'socket', 'head', 'cap', 'iso4762', 'fastener', 'm6']
95
+ },
96
+ 'fastener_shcs_m8_30': {
97
+ id: 'fastener_shcs_m8_30',
98
+ name: 'Socket Head Cap Screw M8×30',
99
+ category: 'Fasteners',
100
+ subcategory: 'Socket Head Cap Screws',
101
+ standard: 'ISO 4762',
102
+ dimensions: { dia: 8, length: 30, headDia: 13, headHeight: 8 },
103
+ material: 'Steel',
104
+ finish: 'Zinc Plated',
105
+ weight: 4.8,
106
+ supplier: { mcmaster: '91251A014', misumi: 'SHCB-M8-30', digi: null },
107
+ price: { usd: 0.75, eur: 0.65 },
108
+ tags: ['screw', 'socket', 'head', 'cap', 'iso4762', 'fastener', 'm8']
109
+ },
110
+ 'fastener_shcs_m10_40': {
111
+ id: 'fastener_shcs_m10_40',
112
+ name: 'Socket Head Cap Screw M10×40',
113
+ category: 'Fasteners',
114
+ subcategory: 'Socket Head Cap Screws',
115
+ standard: 'ISO 4762',
116
+ dimensions: { dia: 10, length: 40, headDia: 15, headHeight: 10 },
117
+ material: 'Steel',
118
+ finish: 'Zinc Plated',
119
+ weight: 7.5,
120
+ supplier: { mcmaster: '91251A018', misumi: 'SHCB-M10-40', digi: null },
121
+ price: { usd: 1.05, eur: 0.90 },
122
+ tags: ['screw', 'socket', 'head', 'cap', 'iso4762', 'fastener', 'm10']
123
+ },
124
+
125
+ // Fasteners: Hex Bolts
126
+ 'fastener_bolt_m6_20': {
127
+ id: 'fastener_bolt_m6_20',
128
+ name: 'Hex Bolt M6×20',
129
+ category: 'Fasteners',
130
+ subcategory: 'Hex Bolts',
131
+ standard: 'ISO 4014',
132
+ dimensions: { dia: 6, length: 20, headWidth: 10, headHeight: 4 },
133
+ material: 'Steel',
134
+ finish: 'Zinc Plated',
135
+ weight: 1.8,
136
+ supplier: { mcmaster: '91259A017', misumi: 'HXBLT-M6-20', digi: null },
137
+ price: { usd: 0.40, eur: 0.35 },
138
+ tags: ['bolt', 'hex', 'iso4014', 'fastener', 'm6']
139
+ },
140
+ 'fastener_bolt_m8_30': {
141
+ id: 'fastener_bolt_m8_30',
142
+ name: 'Hex Bolt M8×30',
143
+ category: 'Fasteners',
144
+ subcategory: 'Hex Bolts',
145
+ standard: 'ISO 4014',
146
+ dimensions: { dia: 8, length: 30, headWidth: 13, headHeight: 5 },
147
+ material: 'Steel',
148
+ finish: 'Zinc Plated',
149
+ weight: 3.5,
150
+ supplier: { mcmaster: '91259A023', misumi: 'HXBLT-M8-30', digi: null },
151
+ price: { usd: 0.60, eur: 0.52 },
152
+ tags: ['bolt', 'hex', 'iso4014', 'fastener', 'm8']
153
+ },
154
+ 'fastener_bolt_m10_40': {
155
+ id: 'fastener_bolt_m10_40',
156
+ name: 'Hex Bolt M10×40',
157
+ category: 'Fasteners',
158
+ subcategory: 'Hex Bolts',
159
+ standard: 'ISO 4014',
160
+ dimensions: { dia: 10, length: 40, headWidth: 16, headHeight: 6 },
161
+ material: 'Steel',
162
+ finish: 'Zinc Plated',
163
+ weight: 5.8,
164
+ supplier: { mcmaster: '91259A029', misumi: 'HXBLT-M10-40', digi: null },
165
+ price: { usd: 0.85, eur: 0.75 },
166
+ tags: ['bolt', 'hex', 'iso4014', 'fastener', 'm10']
167
+ },
168
+
169
+ // Fasteners: Hex Nuts
170
+ 'fastener_nut_m3': {
171
+ id: 'fastener_nut_m3',
172
+ name: 'Hex Nut M3',
173
+ category: 'Fasteners',
174
+ subcategory: 'Hex Nuts',
175
+ standard: 'ISO 4032',
176
+ dimensions: { dia: 3, width: 5.5, height: 2.4 },
177
+ material: 'Steel',
178
+ finish: 'Zinc Plated',
179
+ weight: 0.3,
180
+ supplier: { mcmaster: '90591A003', misumi: 'HXNUT-M3', digi: null },
181
+ price: { usd: 0.15, eur: 0.12 },
182
+ tags: ['nut', 'hex', 'iso4032', 'fastener', 'm3']
183
+ },
184
+ 'fastener_nut_m4': {
185
+ id: 'fastener_nut_m4',
186
+ name: 'Hex Nut M4',
187
+ category: 'Fasteners',
188
+ subcategory: 'Hex Nuts',
189
+ standard: 'ISO 4032',
190
+ dimensions: { dia: 4, width: 7, height: 3.2 },
191
+ material: 'Steel',
192
+ finish: 'Zinc Plated',
193
+ weight: 0.5,
194
+ supplier: { mcmaster: '90591A004', misumi: 'HXNUT-M4', digi: null },
195
+ price: { usd: 0.20, eur: 0.15 },
196
+ tags: ['nut', 'hex', 'iso4032', 'fastener', 'm4']
197
+ },
198
+ 'fastener_nut_m6': {
199
+ id: 'fastener_nut_m6',
200
+ name: 'Hex Nut M6',
201
+ category: 'Fasteners',
202
+ subcategory: 'Hex Nuts',
203
+ standard: 'ISO 4032',
204
+ dimensions: { dia: 6, width: 10, height: 4.8 },
205
+ material: 'Steel',
206
+ finish: 'Zinc Plated',
207
+ weight: 0.9,
208
+ supplier: { mcmaster: '90591A006', misumi: 'HXNUT-M6', digi: null },
209
+ price: { usd: 0.25, eur: 0.20 },
210
+ tags: ['nut', 'hex', 'iso4032', 'fastener', 'm6']
211
+ },
212
+ 'fastener_nut_m8': {
213
+ id: 'fastener_nut_m8',
214
+ name: 'Hex Nut M8',
215
+ category: 'Fasteners',
216
+ subcategory: 'Hex Nuts',
217
+ standard: 'ISO 4032',
218
+ dimensions: { dia: 8, width: 13, height: 6.5 },
219
+ material: 'Steel',
220
+ finish: 'Zinc Plated',
221
+ weight: 1.6,
222
+ supplier: { mcmaster: '90591A008', misumi: 'HXNUT-M8', digi: null },
223
+ price: { usd: 0.35, eur: 0.28 },
224
+ tags: ['nut', 'hex', 'iso4032', 'fastener', 'm8']
225
+ },
226
+ 'fastener_nut_m10': {
227
+ id: 'fastener_nut_m10',
228
+ name: 'Hex Nut M10',
229
+ category: 'Fasteners',
230
+ subcategory: 'Hex Nuts',
231
+ standard: 'ISO 4032',
232
+ dimensions: { dia: 10, width: 16, height: 8 },
233
+ material: 'Steel',
234
+ finish: 'Zinc Plated',
235
+ weight: 2.5,
236
+ supplier: { mcmaster: '90591A010', misumi: 'HXNUT-M10', digi: null },
237
+ price: { usd: 0.50, eur: 0.40 },
238
+ tags: ['nut', 'hex', 'iso4032', 'fastener', 'm10']
239
+ },
240
+
241
+ // Fasteners: Washers
242
+ 'fastener_washer_m3': {
243
+ id: 'fastener_washer_m3',
244
+ name: 'Flat Washer M3',
245
+ category: 'Fasteners',
246
+ subcategory: 'Washers',
247
+ standard: 'ISO 7089',
248
+ dimensions: { innerDia: 3.2, outerDia: 7, thickness: 0.5 },
249
+ material: 'Steel',
250
+ finish: 'Zinc Plated',
251
+ weight: 0.1,
252
+ supplier: { mcmaster: '91128A005', misumi: 'WASHR-M3', digi: null },
253
+ price: { usd: 0.08, eur: 0.06 },
254
+ tags: ['washer', 'flat', 'iso7089', 'fastener', 'm3']
255
+ },
256
+ 'fastener_washer_m4': {
257
+ id: 'fastener_washer_m4',
258
+ name: 'Flat Washer M4',
259
+ category: 'Fasteners',
260
+ subcategory: 'Washers',
261
+ standard: 'ISO 7089',
262
+ dimensions: { innerDia: 4.3, outerDia: 9, thickness: 0.8 },
263
+ material: 'Steel',
264
+ finish: 'Zinc Plated',
265
+ weight: 0.15,
266
+ supplier: { mcmaster: '91128A006', misumi: 'WASHR-M4', digi: null },
267
+ price: { usd: 0.10, eur: 0.08 },
268
+ tags: ['washer', 'flat', 'iso7089', 'fastener', 'm4']
269
+ },
270
+ 'fastener_washer_m6': {
271
+ id: 'fastener_washer_m6',
272
+ name: 'Flat Washer M6',
273
+ category: 'Fasteners',
274
+ subcategory: 'Washers',
275
+ standard: 'ISO 7089',
276
+ dimensions: { innerDia: 6.4, outerDia: 12, thickness: 1 },
277
+ material: 'Steel',
278
+ finish: 'Zinc Plated',
279
+ weight: 0.25,
280
+ supplier: { mcmaster: '91128A008', misumi: 'WASHR-M6', digi: null },
281
+ price: { usd: 0.12, eur: 0.10 },
282
+ tags: ['washer', 'flat', 'iso7089', 'fastener', 'm6']
283
+ },
284
+ 'fastener_washer_m8': {
285
+ id: 'fastener_washer_m8',
286
+ name: 'Flat Washer M8',
287
+ category: 'Fasteners',
288
+ subcategory: 'Washers',
289
+ standard: 'ISO 7089',
290
+ dimensions: { innerDia: 8.4, outerDia: 16, thickness: 1.5 },
291
+ material: 'Steel',
292
+ finish: 'Zinc Plated',
293
+ weight: 0.45,
294
+ supplier: { mcmaster: '91128A010', misumi: 'WASHR-M8', digi: null },
295
+ price: { usd: 0.15, eur: 0.12 },
296
+ tags: ['washer', 'flat', 'iso7089', 'fastener', 'm8']
297
+ },
298
+
299
+ // Bearings: Deep Groove Ball Bearings
300
+ 'bearing_6000': {
301
+ id: 'bearing_6000',
302
+ name: 'Deep Groove Ball Bearing 6000',
303
+ category: 'Bearings',
304
+ subcategory: 'Deep Groove Ball',
305
+ standard: 'DIN 625',
306
+ dimensions: { boredia: 10, outerdia: 26, width: 8 },
307
+ material: 'Chrome Steel',
308
+ seals: 'Open',
309
+ weight: 0.05,
310
+ supplier: { mcmaster: '5909K151', misumi: 'DGBB-6000', digi: null },
311
+ price: { usd: 2.50, eur: 2.20 },
312
+ tags: ['bearing', 'ball', 'deep', 'groove', 'din625', '6000', '10mm']
313
+ },
314
+ 'bearing_6001': {
315
+ id: 'bearing_6001',
316
+ name: 'Deep Groove Ball Bearing 6001',
317
+ category: 'Bearings',
318
+ subcategory: 'Deep Groove Ball',
319
+ standard: 'DIN 625',
320
+ dimensions: { boredia: 12, outerdia: 28, width: 8 },
321
+ material: 'Chrome Steel',
322
+ seals: 'Open',
323
+ weight: 0.06,
324
+ supplier: { mcmaster: '5909K152', misumi: 'DGBB-6001', digi: null },
325
+ price: { usd: 2.80, eur: 2.45 },
326
+ tags: ['bearing', 'ball', 'deep', 'groove', 'din625', '6001', '12mm']
327
+ },
328
+ 'bearing_6002': {
329
+ id: 'bearing_6002',
330
+ name: 'Deep Groove Ball Bearing 6002',
331
+ category: 'Bearings',
332
+ subcategory: 'Deep Groove Ball',
333
+ standard: 'DIN 625',
334
+ dimensions: { boredia: 15, outerdia: 32, width: 9 },
335
+ material: 'Chrome Steel',
336
+ seals: 'Open',
337
+ weight: 0.08,
338
+ supplier: { mcmaster: '5909K153', misumi: 'DGBB-6002', digi: null },
339
+ price: { usd: 3.20, eur: 2.80 },
340
+ tags: ['bearing', 'ball', 'deep', 'groove', 'din625', '6002', '15mm']
341
+ },
342
+ 'bearing_6003': {
343
+ id: 'bearing_6003',
344
+ name: 'Deep Groove Ball Bearing 6003',
345
+ category: 'Bearings',
346
+ subcategory: 'Deep Groove Ball',
347
+ standard: 'DIN 625',
348
+ dimensions: { boredia: 17, outerdia: 35, width: 10 },
349
+ material: 'Chrome Steel',
350
+ seals: 'Open',
351
+ weight: 0.10,
352
+ supplier: { mcmaster: '5909K154', misumi: 'DGBB-6003', digi: null },
353
+ price: { usd: 3.60, eur: 3.15 },
354
+ tags: ['bearing', 'ball', 'deep', 'groove', 'din625', '6003', '17mm']
355
+ },
356
+ 'bearing_6004': {
357
+ id: 'bearing_6004',
358
+ name: 'Deep Groove Ball Bearing 6004',
359
+ category: 'Bearings',
360
+ subcategory: 'Deep Groove Ball',
361
+ standard: 'DIN 625',
362
+ dimensions: { boredia: 20, outerdia: 42, width: 12 },
363
+ material: 'Chrome Steel',
364
+ seals: 'Open',
365
+ weight: 0.13,
366
+ supplier: { mcmaster: '5909K155', misumi: 'DGBB-6004', digi: null },
367
+ price: { usd: 4.50, eur: 3.90 },
368
+ tags: ['bearing', 'ball', 'deep', 'groove', 'din625', '6004', '20mm']
369
+ },
370
+
371
+ // Linear Motion: Linear Rails
372
+ 'linear_rail_mgn7_200': {
373
+ id: 'linear_rail_mgn7_200',
374
+ name: 'Linear Rail MGN7 200mm',
375
+ category: 'Linear Motion',
376
+ subcategory: 'Linear Rails',
377
+ standard: 'HIWIN',
378
+ dimensions: { profile: 'MGN7', length: 200, height: 7, width: 7 },
379
+ material: 'Aluminum',
380
+ load: 1200,
381
+ weight: 0.25,
382
+ supplier: { mcmaster: null, misumi: 'LR-MGN7-200', digi: null },
383
+ price: { usd: 8.50, eur: 7.50 },
384
+ tags: ['linear', 'rail', 'mgn7', 'hiwin', '200mm']
385
+ },
386
+ 'linear_rail_mgn9_300': {
387
+ id: 'linear_rail_mgn9_300',
388
+ name: 'Linear Rail MGN9 300mm',
389
+ category: 'Linear Motion',
390
+ subcategory: 'Linear Rails',
391
+ standard: 'HIWIN',
392
+ dimensions: { profile: 'MGN9', length: 300, height: 9, width: 9 },
393
+ material: 'Aluminum',
394
+ load: 2000,
395
+ weight: 0.45,
396
+ supplier: { mcmaster: null, misumi: 'LR-MGN9-300', digi: null },
397
+ price: { usd: 12.50, eur: 11.00 },
398
+ tags: ['linear', 'rail', 'mgn9', 'hiwin', '300mm']
399
+ },
400
+ 'linear_rail_mgn12_400': {
401
+ id: 'linear_rail_mgn12_400',
402
+ name: 'Linear Rail MGN12 400mm',
403
+ category: 'Linear Motion',
404
+ subcategory: 'Linear Rails',
405
+ standard: 'HIWIN',
406
+ dimensions: { profile: 'MGN12', length: 400, height: 12, width: 12 },
407
+ material: 'Aluminum',
408
+ load: 3500,
409
+ weight: 0.75,
410
+ supplier: { mcmaster: null, misumi: 'LR-MGN12-400', digi: null },
411
+ price: { usd: 18.00, eur: 16.00 },
412
+ tags: ['linear', 'rail', 'mgn12', 'hiwin', '400mm']
413
+ },
414
+
415
+ // Structural: Aluminum Extrusions
416
+ 'extrusion_2020_500': {
417
+ id: 'extrusion_2020_500',
418
+ name: 'Aluminum Extrusion 2020 500mm',
419
+ category: 'Structural',
420
+ subcategory: 'Aluminum Extrusions',
421
+ standard: 'T-Slot',
422
+ dimensions: { profile: '2020', length: 500, slotWidth: 6, wallThickness: 1.5 },
423
+ material: 'Aluminum 6061-T6',
424
+ load: 200,
425
+ weight: 0.35,
426
+ supplier: { mcmaster: null, misumi: 'EXTR-2020-500', digi: null },
427
+ price: { usd: 4.50, eur: 4.00 },
428
+ tags: ['extrusion', 'aluminum', '2020', '500mm', 'tslot']
429
+ },
430
+ 'extrusion_2040_500': {
431
+ id: 'extrusion_2040_500',
432
+ name: 'Aluminum Extrusion 2040 500mm',
433
+ category: 'Structural',
434
+ subcategory: 'Aluminum Extrusions',
435
+ standard: 'T-Slot',
436
+ dimensions: { profile: '2040', length: 500, slotWidth: 6, wallThickness: 1.5 },
437
+ material: 'Aluminum 6061-T6',
438
+ load: 350,
439
+ weight: 0.58,
440
+ supplier: { mcmaster: null, misumi: 'EXTR-2040-500', digi: null },
441
+ price: { usd: 6.50, eur: 5.75 },
442
+ tags: ['extrusion', 'aluminum', '2040', '500mm', 'tslot']
443
+ },
444
+ 'extrusion_3030_500': {
445
+ id: 'extrusion_3030_500',
446
+ name: 'Aluminum Extrusion 3030 500mm',
447
+ category: 'Structural',
448
+ subcategory: 'Aluminum Extrusions',
449
+ standard: 'T-Slot',
450
+ dimensions: { profile: '3030', length: 500, slotWidth: 8, wallThickness: 2 },
451
+ material: 'Aluminum 6061-T6',
452
+ load: 600,
453
+ weight: 0.85,
454
+ supplier: { mcmaster: null, misumi: 'EXTR-3030-500', digi: null },
455
+ price: { usd: 8.50, eur: 7.50 },
456
+ tags: ['extrusion', 'aluminum', '3030', '500mm', 'tslot']
457
+ },
458
+ 'extrusion_4040_500': {
459
+ id: 'extrusion_4040_500',
460
+ name: 'Aluminum Extrusion 4040 500mm',
461
+ category: 'Structural',
462
+ subcategory: 'Aluminum Extrusions',
463
+ standard: 'T-Slot',
464
+ dimensions: { profile: '4040', length: 500, slotWidth: 8, wallThickness: 2 },
465
+ material: 'Aluminum 6061-T6',
466
+ load: 1000,
467
+ weight: 1.35,
468
+ supplier: { mcmaster: null, misumi: 'EXTR-4040-500', digi: null },
469
+ price: { usd: 12.00, eur: 10.50 },
470
+ tags: ['extrusion', 'aluminum', '4040', '500mm', 'tslot']
471
+ },
472
+
473
+ // Electronics: Stepper Motors
474
+ 'motor_stepper_nema17': {
475
+ id: 'motor_stepper_nema17',
476
+ name: 'Stepper Motor NEMA 17',
477
+ category: 'Electronics',
478
+ subcategory: 'Stepper Motors',
479
+ standard: 'NEMA',
480
+ dimensions: { height: 48, width: 42, shaftDia: 5 },
481
+ torque: 0.4,
482
+ voltage: 12,
483
+ current: 1.2,
484
+ weight: 0.35,
485
+ supplier: { mcmaster: null, misumi: 'MOTOR-NEMA17', digi: '468-4195-ND' },
486
+ price: { usd: 15.00, eur: 13.00 },
487
+ tags: ['motor', 'stepper', 'nema', '17', 'cnc', 'automation']
488
+ },
489
+ 'motor_stepper_nema23': {
490
+ id: 'motor_stepper_nema23',
491
+ name: 'Stepper Motor NEMA 23',
492
+ category: 'Electronics',
493
+ subcategory: 'Stepper Motors',
494
+ standard: 'NEMA',
495
+ dimensions: { height: 56, width: 56, shaftDia: 8 },
496
+ torque: 1.26,
497
+ voltage: 24,
498
+ current: 3.0,
499
+ weight: 0.85,
500
+ supplier: { mcmaster: null, misumi: 'MOTOR-NEMA23', digi: null },
501
+ price: { usd: 30.00, eur: 26.00 },
502
+ tags: ['motor', 'stepper', 'nema', '23', 'cnc', 'automation']
503
+ },
504
+ 'motor_stepper_nema34': {
505
+ id: 'motor_stepper_nema34',
506
+ name: 'Stepper Motor NEMA 34',
507
+ category: 'Electronics',
508
+ subcategory: 'Stepper Motors',
509
+ standard: 'NEMA',
510
+ dimensions: { height: 86, width: 86, shaftDia: 12 },
511
+ torque: 4.75,
512
+ voltage: 48,
513
+ current: 5.6,
514
+ weight: 3.50,
515
+ supplier: { mcmaster: null, misumi: 'MOTOR-NEMA34', digi: null },
516
+ price: { usd: 75.00, eur: 66.00 },
517
+ tags: ['motor', 'stepper', 'nema', '34', 'cnc', 'automation']
518
+ },
519
+
520
+ // Electronics: Servo Motors
521
+ 'motor_servo_mg996r': {
522
+ id: 'motor_servo_mg996r',
523
+ name: 'Servo Motor MG996R',
524
+ category: 'Electronics',
525
+ subcategory: 'Servo Motors',
526
+ standard: 'RC Servo',
527
+ dimensions: { length: 40.7, width: 19.7, height: 36 },
528
+ torque: 10,
529
+ voltage: 4.8,
530
+ speed: 0.20,
531
+ weight: 0.055,
532
+ supplier: { mcmaster: null, misumi: 'SERVO-MG996R', digi: null },
533
+ price: { usd: 12.00, eur: 10.50 },
534
+ tags: ['motor', 'servo', 'rc', 'mg996r', 'robotics']
535
+ },
536
+ 'motor_servo_ds3218': {
537
+ id: 'motor_servo_ds3218',
538
+ name: 'Servo Motor DS3218',
539
+ category: 'Electronics',
540
+ subcategory: 'Servo Motors',
541
+ standard: 'RC Servo',
542
+ dimensions: { length: 54, width: 20, height: 54 },
543
+ torque: 18,
544
+ voltage: 6.0,
545
+ speed: 0.10,
546
+ weight: 0.120,
547
+ supplier: { mcmaster: null, misumi: 'SERVO-DS3218', digi: null },
548
+ price: { usd: 25.00, eur: 22.00 },
549
+ tags: ['motor', 'servo', 'rc', 'ds3218', 'robotics']
550
+ },
551
+
552
+ // Electronics: Development Boards
553
+ 'board_arduino_uno': {
554
+ id: 'board_arduino_uno',
555
+ name: 'Arduino Uno Rev3',
556
+ category: 'Electronics',
557
+ subcategory: 'Development Boards',
558
+ standard: 'Arduino',
559
+ dimensions: { length: 68.6, width: 53.3, height: 10 },
560
+ processor: 'ATmega328P',
561
+ voltage: 5,
562
+ pins: 14,
563
+ weight: 0.025,
564
+ supplier: { mcmaster: null, misumi: 'BOARD-ARDUINO-UNO', digi: '1050-1024-ND' },
565
+ price: { usd: 25.00, eur: 22.00 },
566
+ tags: ['board', 'arduino', 'uno', 'microcontroller', 'atmega328p']
567
+ },
568
+ 'board_raspberry_pi_4b': {
569
+ id: 'board_raspberry_pi_4b',
570
+ name: 'Raspberry Pi 4B (2GB)',
571
+ category: 'Electronics',
572
+ subcategory: 'Development Boards',
573
+ standard: 'Raspberry Pi',
574
+ dimensions: { length: 85.6, width: 56, height: 17 },
575
+ processor: 'BCM2711',
576
+ ram: 2,
577
+ voltage: 5,
578
+ weight: 0.050,
579
+ supplier: { mcmaster: null, misumi: 'BOARD-RPI4B-2G', digi: null },
580
+ price: { usd: 35.00, eur: 31.00 },
581
+ tags: ['board', 'raspberry', 'pi', '4b', 'linux', 'sbc']
582
+ },
583
+ };
584
+
585
+ // ============================================================================
586
+ // STATE & GLOBALS
587
+ // ============================================================================
588
+
589
+ let scene = null;
590
+ let state = {
591
+ cart: [],
592
+ recentlyUsed: [],
593
+ favorited: [],
594
+ searchQuery: '',
595
+ searchResults: [],
596
+ selectedPart: null,
597
+ filters: {
598
+ category: null,
599
+ subcategory: null,
600
+ minSize: 0,
601
+ maxSize: 1000,
602
+ material: null,
603
+ supplier: null,
604
+ }
605
+ };
606
+
607
+ // Part cache for 3D geometry
608
+ const geometryCache = new Map();
609
+
610
+ // ============================================================================
611
+ // THREE.JS GEOMETRY GENERATORS
612
+ // ============================================================================
613
+
614
+ /**
615
+ * Generate hex head bolt geometry
616
+ * @param {Object} dims - { dia, length, headDia, headHeight }
617
+ * @returns {THREE.Group}
618
+ */
619
+ function generateBolt(dims) {
620
+ const group = new THREE.Group();
621
+
622
+ // Threaded shaft
623
+ const shaftGeometry = new THREE.CylinderGeometry(
624
+ dims.dia / 2,
625
+ dims.dia / 2,
626
+ dims.length,
627
+ 32
628
+ );
629
+ const shaftMaterial = new THREE.MeshStandardMaterial({
630
+ color: 0x888888,
631
+ metalness: 0.8,
632
+ roughness: 0.2
633
+ });
634
+ const shaft = new THREE.Mesh(shaftGeometry, shaftMaterial);
635
+ shaft.position.z = dims.length / 2;
636
+ group.add(shaft);
637
+
638
+ // Hex head (simplified as cylinder)
639
+ const headGeometry = new THREE.CylinderGeometry(
640
+ dims.headDia / 2,
641
+ dims.headDia / 2,
642
+ dims.headHeight,
643
+ 6
644
+ );
645
+ const headMaterial = new THREE.MeshStandardMaterial({
646
+ color: 0x999999,
647
+ metalness: 0.9,
648
+ roughness: 0.15
649
+ });
650
+ const head = new THREE.Mesh(headGeometry, headMaterial);
651
+ head.position.z = dims.length + dims.headHeight / 2;
652
+ group.add(head);
653
+
654
+ return group;
655
+ }
656
+
657
+ /**
658
+ * Generate hex nut geometry
659
+ * @param {Object} dims - { dia, width, height }
660
+ * @returns {THREE.Group}
661
+ */
662
+ function generateNut(dims) {
663
+ const group = new THREE.Group();
664
+
665
+ // Hex body
666
+ const nutGeometry = new THREE.CylinderGeometry(
667
+ dims.width / 2,
668
+ dims.width / 2,
669
+ dims.height,
670
+ 6
671
+ );
672
+ const nutMaterial = new THREE.MeshStandardMaterial({
673
+ color: 0x888888,
674
+ metalness: 0.8,
675
+ roughness: 0.2
676
+ });
677
+ const nut = new THREE.Mesh(nutGeometry, nutMaterial);
678
+ group.add(nut);
679
+
680
+ // Center hole
681
+ const holeGeometry = new THREE.CylinderGeometry(
682
+ dims.dia / 2.1,
683
+ dims.dia / 2.1,
684
+ dims.height + 0.2,
685
+ 32
686
+ );
687
+ const holeMaterial = new THREE.MeshStandardMaterial({
688
+ color: 0x111111,
689
+ metalness: 0.5,
690
+ roughness: 0.5
691
+ });
692
+ const hole = new THREE.Mesh(holeGeometry, holeMaterial);
693
+ group.add(hole);
694
+
695
+ return group;
696
+ }
697
+
698
+ /**
699
+ * Generate washer geometry
700
+ * @param {Object} dims - { innerDia, outerDia, thickness }
701
+ * @returns {THREE.Group}
702
+ */
703
+ function generateWasher(dims) {
704
+ const group = new THREE.Group();
705
+
706
+ // Ring shape using LatheGeometry
707
+ const points = [
708
+ new THREE.Vector2(dims.innerDia / 2, 0),
709
+ new THREE.Vector2(dims.outerDia / 2, 0),
710
+ new THREE.Vector2(dims.outerDia / 2, dims.thickness),
711
+ new THREE.Vector2(dims.innerDia / 2, dims.thickness),
712
+ ];
713
+
714
+ const geometry = new THREE.LatheGeometry(points, 32);
715
+ const material = new THREE.MeshStandardMaterial({
716
+ color: 0x888888,
717
+ metalness: 0.8,
718
+ roughness: 0.2
719
+ });
720
+ const washer = new THREE.Mesh(geometry, material);
721
+ group.add(washer);
722
+
723
+ return group;
724
+ }
725
+
726
+ /**
727
+ * Generate bearing geometry (simplified)
728
+ * @param {Object} dims - { boredia, outerdia, width }
729
+ * @returns {THREE.Group}
730
+ */
731
+ function generateBearing(dims) {
732
+ const group = new THREE.Group();
733
+
734
+ // Outer race
735
+ const outerGeometry = new THREE.CylinderGeometry(
736
+ dims.outerdia / 2,
737
+ dims.outerdia / 2,
738
+ dims.width,
739
+ 32
740
+ );
741
+ const outerMaterial = new THREE.MeshStandardMaterial({
742
+ color: 0x555555,
743
+ metalness: 0.9,
744
+ roughness: 0.1
745
+ });
746
+ const outer = new THREE.Mesh(outerGeometry, outerMaterial);
747
+ group.add(outer);
748
+
749
+ // Inner race
750
+ const innerGeometry = new THREE.CylinderGeometry(
751
+ dims.boredia / 2,
752
+ dims.boredia / 2,
753
+ dims.width + 0.1,
754
+ 32
755
+ );
756
+ const innerMaterial = new THREE.MeshStandardMaterial({
757
+ color: 0x666666,
758
+ metalness: 0.85,
759
+ roughness: 0.15
760
+ });
761
+ const inner = new THREE.Mesh(innerGeometry, innerMaterial);
762
+ group.add(inner);
763
+
764
+ return group;
765
+ }
766
+
767
+ /**
768
+ * Generate linear rail geometry
769
+ * @param {Object} dims - { profile, length, height, width }
770
+ * @returns {THREE.Group}
771
+ */
772
+ function generateLinearRail(dims) {
773
+ const group = new THREE.Group();
774
+
775
+ // Main profile (simplified as box)
776
+ const profileGeometry = new THREE.BoxGeometry(
777
+ dims.width,
778
+ dims.height,
779
+ dims.length
780
+ );
781
+ const profileMaterial = new THREE.MeshStandardMaterial({
782
+ color: 0xAAAAAA,
783
+ metalness: 0.7,
784
+ roughness: 0.3
785
+ });
786
+ const profile = new THREE.Mesh(profileGeometry, profileMaterial);
787
+ group.add(profile);
788
+
789
+ // Mounting holes (simplified as recesses)
790
+ const holeRadius = 2.5;
791
+ const holeGeometry = new THREE.SphereGeometry(holeRadius, 8, 8);
792
+ const holeMaterial = new THREE.MeshStandardMaterial({
793
+ color: 0x333333,
794
+ metalness: 0.5,
795
+ roughness: 0.5
796
+ });
797
+
798
+ for (let i = 0; i < 3; i++) {
799
+ const hole = new THREE.Mesh(holeGeometry, holeMaterial);
800
+ hole.position.z = (dims.length / 4) * (i - 1);
801
+ hole.position.y = dims.height / 2 + holeRadius / 2;
802
+ group.add(hole);
803
+ }
804
+
805
+ return group;
806
+ }
807
+
808
+ /**
809
+ * Generate aluminum extrusion geometry
810
+ * @param {Object} dims - { profile, length, slotWidth, wallThickness }
811
+ * @returns {THREE.Group}
812
+ */
813
+ function generateExtrusion(dims) {
814
+ const group = new THREE.Group();
815
+
816
+ // Outer profile (square)
817
+ const size = parseInt(dims.profile);
818
+ const outerGeometry = new THREE.BoxGeometry(size, size, dims.length);
819
+ const outerMaterial = new THREE.MeshStandardMaterial({
820
+ color: 0xD4D4D4,
821
+ metalness: 0.6,
822
+ roughness: 0.4
823
+ });
824
+ const outer = new THREE.Mesh(outerGeometry, outerMaterial);
825
+ group.add(outer);
826
+
827
+ // Slot indentations (simplified)
828
+ const slotGeometry = new THREE.BoxGeometry(
829
+ dims.slotWidth,
830
+ dims.slotWidth,
831
+ dims.length + 1
832
+ );
833
+ const slotMaterial = new THREE.MeshStandardMaterial({
834
+ color: 0x999999,
835
+ metalness: 0.5,
836
+ roughness: 0.5
837
+ });
838
+
839
+ const sides = [
840
+ { x: size / 2 + 0.1, y: 0 },
841
+ { x: -size / 2 - 0.1, y: 0 },
842
+ { x: 0, y: size / 2 + 0.1 },
843
+ { x: 0, y: -size / 2 - 0.1 },
844
+ ];
845
+
846
+ sides.forEach(side => {
847
+ const slot = new THREE.Mesh(slotGeometry, slotMaterial);
848
+ slot.position.x = side.x;
849
+ slot.position.y = side.y;
850
+ group.add(slot);
851
+ });
852
+
853
+ return group;
854
+ }
855
+
856
+ /**
857
+ * Generate stepper motor geometry
858
+ * @param {Object} dims - { height, width, shaftDia }
859
+ * @returns {THREE.Group}
860
+ */
861
+ function generateStepperMotor(dims) {
862
+ const group = new THREE.Group();
863
+
864
+ // Body
865
+ const bodyGeometry = new THREE.BoxGeometry(dims.width, dims.width, dims.height);
866
+ const bodyMaterial = new THREE.MeshStandardMaterial({
867
+ color: 0x333333,
868
+ metalness: 0.3,
869
+ roughness: 0.7
870
+ });
871
+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
872
+ group.add(body);
873
+
874
+ // Shaft
875
+ const shaftGeometry = new THREE.CylinderGeometry(
876
+ dims.shaftDia / 2,
877
+ dims.shaftDia / 2,
878
+ dims.height / 2,
879
+ 32
880
+ );
881
+ const shaftMaterial = new THREE.MeshStandardMaterial({
882
+ color: 0x888888,
883
+ metalness: 0.8,
884
+ roughness: 0.2
885
+ });
886
+ const shaft = new THREE.Mesh(shaftGeometry, shaftMaterial);
887
+ shaft.position.z = dims.height / 2 + dims.height / 4;
888
+ group.add(shaft);
889
+
890
+ return group;
891
+ }
892
+
893
+ /**
894
+ * Generate servo motor geometry
895
+ * @param {Object} dims - { length, width, height }
896
+ * @returns {THREE.Group}
897
+ */
898
+ function generateServoMotor(dims) {
899
+ const group = new THREE.Group();
900
+
901
+ // Body
902
+ const bodyGeometry = new THREE.BoxGeometry(dims.width, dims.height, dims.length);
903
+ const bodyMaterial = new THREE.MeshStandardMaterial({
904
+ color: 0x1a1a1a,
905
+ metalness: 0.4,
906
+ roughness: 0.6
907
+ });
908
+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
909
+ group.add(body);
910
+
911
+ // Servo arm
912
+ const armGeometry = new THREE.BoxGeometry(dims.width * 1.2, 2, dims.width * 0.8);
913
+ const armMaterial = new THREE.MeshStandardMaterial({
914
+ color: 0xFF6B35,
915
+ metalness: 0.5,
916
+ roughness: 0.5
917
+ });
918
+ const arm = new THREE.Mesh(armGeometry, armMaterial);
919
+ arm.position.z = dims.length / 2 + 10;
920
+ group.add(arm);
921
+
922
+ return group;
923
+ }
924
+
925
+ /**
926
+ * Generate development board geometry
927
+ * @param {Object} dims - { length, width, height }
928
+ * @returns {THREE.Group}
929
+ */
930
+ function generateBoard(dims) {
931
+ const group = new THREE.Group();
932
+
933
+ // PCB
934
+ const pcbGeometry = new THREE.BoxGeometry(dims.width, dims.height, 2);
935
+ const pcbMaterial = new THREE.MeshStandardMaterial({
936
+ color: 0x2B5016,
937
+ metalness: 0.2,
938
+ roughness: 0.8
939
+ });
940
+ const pcb = new THREE.Mesh(pcbGeometry, pcbMaterial);
941
+ group.add(pcb);
942
+
943
+ // Component highlights
944
+ const compGeometry = new THREE.BoxGeometry(
945
+ dims.width * 0.7,
946
+ dims.height * 0.6,
947
+ 1
948
+ );
949
+ const compMaterial = new THREE.MeshStandardMaterial({
950
+ color: 0x444444,
951
+ metalness: 0.3,
952
+ roughness: 0.6
953
+ });
954
+ const comp = new THREE.Mesh(compGeometry, compMaterial);
955
+ comp.position.z = 2;
956
+ group.add(comp);
957
+
958
+ return group;
959
+ }
960
+
961
+ /**
962
+ * Get or generate geometry for a part
963
+ * @param {Object} part - Part catalog entry
964
+ * @returns {THREE.Group}
965
+ */
966
+ function getPartGeometry(part) {
967
+ if (geometryCache.has(part.id)) {
968
+ return geometryCache.get(part.id).clone();
969
+ }
970
+
971
+ let geometry;
972
+ const dims = part.dimensions;
973
+
974
+ if (part.category === 'Fasteners') {
975
+ if (part.subcategory === 'Socket Head Cap Screws') {
976
+ geometry = generateBolt(dims);
977
+ } else if (part.subcategory === 'Hex Bolts') {
978
+ geometry = generateBolt(dims);
979
+ } else if (part.subcategory === 'Hex Nuts') {
980
+ geometry = generateNut(dims);
981
+ } else if (part.subcategory === 'Washers') {
982
+ geometry = generateWasher(dims);
983
+ }
984
+ } else if (part.category === 'Bearings') {
985
+ geometry = generateBearing(dims);
986
+ } else if (part.category === 'Linear Motion') {
987
+ if (part.subcategory === 'Linear Rails') {
988
+ geometry = generateLinearRail(dims);
989
+ }
990
+ } else if (part.category === 'Structural') {
991
+ geometry = generateExtrusion(dims);
992
+ } else if (part.category === 'Electronics') {
993
+ if (part.subcategory === 'Stepper Motors') {
994
+ geometry = generateStepperMotor(dims);
995
+ } else if (part.subcategory === 'Servo Motors') {
996
+ geometry = generateServoMotor(dims);
997
+ } else if (part.subcategory === 'Development Boards') {
998
+ geometry = generateBoard(dims);
999
+ }
1000
+ }
1001
+
1002
+ if (!geometry) {
1003
+ // Fallback: generic box
1004
+ geometry = new THREE.Group();
1005
+ const box = new THREE.Mesh(
1006
+ new THREE.BoxGeometry(10, 10, 10),
1007
+ new THREE.MeshStandardMaterial({ color: 0x888888 })
1008
+ );
1009
+ geometry.add(box);
1010
+ }
1011
+
1012
+ geometryCache.set(part.id, geometry.clone());
1013
+ return geometry;
1014
+ }
1015
+
1016
+ // ============================================================================
1017
+ // AI-POWERED SEARCH ENGINE
1018
+ // ============================================================================
1019
+
1020
+ /**
1021
+ * Tokenize and clean search query
1022
+ * @param {string} query
1023
+ * @returns {string[]}
1024
+ */
1025
+ function tokenizeQuery(query) {
1026
+ return query
1027
+ .toLowerCase()
1028
+ .replace(/[^\w\s\-\.]/g, '')
1029
+ .split(/\s+/)
1030
+ .filter(t => t.length > 0);
1031
+ }
1032
+
1033
+ /**
1034
+ * Calculate semantic similarity (simple Levenshtein-inspired)
1035
+ * @param {string} a
1036
+ * @param {string} b
1037
+ * @returns {number} 0-1
1038
+ */
1039
+ function stringSimilarity(a, b) {
1040
+ a = a.toLowerCase();
1041
+ b = b.toLowerCase();
1042
+ if (a === b) return 1;
1043
+ if (a.includes(b) || b.includes(a)) return 0.85;
1044
+
1045
+ let matches = 0;
1046
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
1047
+ if (a[i] === b[i]) matches++;
1048
+ }
1049
+ return matches / Math.max(a.length, b.length);
1050
+ }
1051
+
1052
+ /**
1053
+ * Expand abbreviations and typos
1054
+ * @param {string} token
1055
+ * @returns {string[]}
1056
+ */
1057
+ function expandAbbreviations(token) {
1058
+ const expansions = {
1059
+ 'ss': ['stainless steel', 'steel'],
1060
+ 'al': ['aluminum'],
1061
+ 'csk': ['countersunk'],
1062
+ 'dia': ['diameter'],
1063
+ 'mm': [],
1064
+ 'nylon': ['nylon'],
1065
+ 'hex': ['hexagon'],
1066
+ 'shcs': ['socket head cap screw'],
1067
+ 'mgn': ['linear rail', 'rail'],
1068
+ 'extr': ['extrusion'],
1069
+ 'nema': ['stepper motor', 'motor'],
1070
+ 'm3': ['m3', 'metric 3'],
1071
+ 'm4': ['m4', 'metric 4'],
1072
+ 'm5': ['m5', 'metric 5'],
1073
+ 'm6': ['m6', 'metric 6'],
1074
+ 'm8': ['m8', 'metric 8'],
1075
+ 'm10': ['m10', 'metric 10'],
1076
+ };
1077
+
1078
+ return expansions[token] || [token];
1079
+ }
1080
+
1081
+ /**
1082
+ * Search catalog with relevance scoring
1083
+ * @param {string} query
1084
+ * @param {Object} filters
1085
+ * @returns {Array} Sorted results with scores
1086
+ */
1087
+ function searchCatalog(query, filters = {}) {
1088
+ if (!query || query.trim().length === 0) {
1089
+ return Object.values(partCatalog).slice(0, 30);
1090
+ }
1091
+
1092
+ const tokens = tokenizeQuery(query);
1093
+ const allExpanded = new Set();
1094
+
1095
+ tokens.forEach(token => {
1096
+ expandAbbreviations(token).forEach(exp => allExpanded.add(exp));
1097
+ });
1098
+
1099
+ const results = [];
1100
+
1101
+ Object.values(partCatalog).forEach(part => {
1102
+ let score = 0;
1103
+
1104
+ // Exact name match
1105
+ if (part.name.toLowerCase().includes(query.toLowerCase())) {
1106
+ score += 1.0;
1107
+ }
1108
+
1109
+ // Tag matches
1110
+ part.tags.forEach(tag => {
1111
+ tokens.forEach(token => {
1112
+ const similarity = stringSimilarity(tag, token);
1113
+ if (similarity > 0.6) {
1114
+ score += similarity * 0.8;
1115
+ }
1116
+ });
1117
+ });
1118
+
1119
+ // Name token matches
1120
+ const nameLower = part.name.toLowerCase();
1121
+ tokens.forEach(token => {
1122
+ if (nameLower.includes(token)) {
1123
+ score += 0.5;
1124
+ }
1125
+ });
1126
+
1127
+ // Dimension matches (e.g., "M8" or "10mm")
1128
+ tokens.forEach(token => {
1129
+ const dimMatch = token.match(/([0-9]+)/);
1130
+ if (dimMatch) {
1131
+ const num = parseInt(dimMatch[1]);
1132
+ Object.values(part.dimensions).forEach(dim => {
1133
+ if (typeof dim === 'number' && Math.abs(dim - num) < 2) {
1134
+ score += 0.3;
1135
+ }
1136
+ });
1137
+ }
1138
+ });
1139
+
1140
+ // Category/subcategory filter
1141
+ if (filters.category && part.category !== filters.category) {
1142
+ score *= 0.5;
1143
+ }
1144
+ if (filters.subcategory && part.subcategory !== filters.subcategory) {
1145
+ score *= 0.6;
1146
+ }
1147
+
1148
+ // Size range filter
1149
+ const size = part.dimensions.dia || part.dimensions.length || 0;
1150
+ if (filters.minSize && size < filters.minSize) {
1151
+ score *= 0.2;
1152
+ }
1153
+ if (filters.maxSize && size > filters.maxSize) {
1154
+ score *= 0.2;
1155
+ }
1156
+
1157
+ // Material filter
1158
+ if (filters.material && part.material !== filters.material) {
1159
+ score *= 0.7;
1160
+ }
1161
+
1162
+ if (score > 0) {
1163
+ results.push({ part, score });
1164
+ }
1165
+ });
1166
+
1167
+ return results
1168
+ .sort((a, b) => b.score - a.score)
1169
+ .slice(0, 50)
1170
+ .map(r => r.part);
1171
+ }
1172
+
1173
+ /**
1174
+ * Semantic search (natural language understanding)
1175
+ * @param {string} query
1176
+ * @returns {Array} Parts matching semantic intent
1177
+ */
1178
+ function semanticSearch(query) {
1179
+ const lowerQuery = query.toLowerCase();
1180
+
1181
+ // "something to hold a 10mm rod"
1182
+ if (lowerQuery.includes('hold') || lowerQuery.includes('bore')) {
1183
+ const bearingResults = Object.values(partCatalog).filter(p =>
1184
+ p.category === 'Bearings' || p.subcategory === 'Linear Bushings'
1185
+ );
1186
+
1187
+ const dimMatch = query.match(/(\d+)/);
1188
+ if (dimMatch) {
1189
+ const targetDia = parseInt(dimMatch[1]);
1190
+ return bearingResults.filter(p =>
1191
+ Math.abs((p.dimensions.boredia || 0) - targetDia) < 5
1192
+ );
1193
+ }
1194
+ return bearingResults;
1195
+ }
1196
+
1197
+ // "connect two aluminum pieces"
1198
+ if (lowerQuery.includes('connect') || lowerQuery.includes('join')) {
1199
+ return Object.values(partCatalog).filter(p =>
1200
+ p.category === 'Fasteners' || (p.category === 'Structural' && p.tags.includes('nut'))
1201
+ );
1202
+ }
1203
+
1204
+ // "move something back and forth"
1205
+ if (lowerQuery.includes('move') || lowerQuery.includes('linear')) {
1206
+ return Object.values(partCatalog).filter(p =>
1207
+ p.category === 'Linear Motion' || p.tags.includes('motor')
1208
+ );
1209
+ }
1210
+
1211
+ // "spin something"
1212
+ if (lowerQuery.includes('spin') || lowerQuery.includes('rotate') || lowerQuery.includes('motor')) {
1213
+ return Object.values(partCatalog).filter(p =>
1214
+ p.category === 'Electronics' && (p.tags.includes('motor') || p.tags.includes('servo'))
1215
+ );
1216
+ }
1217
+
1218
+ return [];
1219
+ }
1220
+
1221
+ /**
1222
+ * Combined search (keyword + semantic)
1223
+ * @param {string} query
1224
+ * @param {Object} filters
1225
+ * @returns {Array}
1226
+ */
1227
+ function search(query, filters = {}) {
1228
+ state.searchQuery = query;
1229
+
1230
+ const keywordResults = new Map();
1231
+ searchCatalog(query, filters).forEach(p => keywordResults.set(p.id, p));
1232
+
1233
+ const semanticResults = semanticSearch(query);
1234
+ semanticResults.forEach(p => {
1235
+ if (!keywordResults.has(p.id)) {
1236
+ keywordResults.set(p.id, p);
1237
+ }
1238
+ });
1239
+
1240
+ state.searchResults = Array.from(keywordResults.values());
1241
+ return state.searchResults;
1242
+ }
1243
+
1244
+ /**
1245
+ * Get all unique categories
1246
+ * @returns {string[]}
1247
+ */
1248
+ function getCategories() {
1249
+ const cats = new Set();
1250
+ Object.values(partCatalog).forEach(p => cats.add(p.category));
1251
+ return Array.from(cats).sort();
1252
+ }
1253
+
1254
+ /**
1255
+ * Get subcategories for a category
1256
+ * @param {string} category
1257
+ * @returns {string[]}
1258
+ */
1259
+ function getSubcategories(category) {
1260
+ const subs = new Set();
1261
+ Object.values(partCatalog).forEach(p => {
1262
+ if (p.category === category) {
1263
+ subs.add(p.subcategory);
1264
+ }
1265
+ });
1266
+ return Array.from(subs).sort();
1267
+ }
1268
+
1269
+ /**
1270
+ * Get full catalog
1271
+ * @returns {Object}
1272
+ */
1273
+ function getCatalog() {
1274
+ return partCatalog;
1275
+ }
1276
+
1277
+ // ============================================================================
1278
+ // PART INSERTION & CONFIGURATION
1279
+ // ============================================================================
1280
+
1281
+ /**
1282
+ * Insert part into scene
1283
+ * @param {Object} part
1284
+ * @param {THREE.Vector3} position
1285
+ * @param {Object} config - { quantity, size, material, finish }
1286
+ * @returns {Object} { id, part, mesh, position }
1287
+ */
1288
+ function insertPart(part, position = new THREE.Vector3(0, 0, 0), config = {}) {
1289
+ const geometry = getPartGeometry(part);
1290
+ const mesh = geometry.clone();
1291
+
1292
+ mesh.position.copy(position);
1293
+ if (scene) scene.add(mesh);
1294
+
1295
+ const cartEntry = {
1296
+ id: `${part.id}_${Date.now()}`,
1297
+ partId: part.id,
1298
+ part,
1299
+ mesh,
1300
+ position: position.clone(),
1301
+ quantity: config.quantity || 1,
1302
+ size: config.size || null,
1303
+ material: config.material || part.material,
1304
+ finish: config.finish || (part.finish || 'Default'),
1305
+ timestamp: Date.now()
1306
+ };
1307
+
1308
+ state.cart.push(cartEntry);
1309
+
1310
+ // Track recently used
1311
+ const recentIdx = state.recentlyUsed.findIndex(p => p.id === part.id);
1312
+ if (recentIdx >= 0) {
1313
+ state.recentlyUsed.splice(recentIdx, 1);
1314
+ }
1315
+ state.recentlyUsed.unshift(part);
1316
+ if (state.recentlyUsed.length > 10) {
1317
+ state.recentlyUsed.pop();
1318
+ }
1319
+
1320
+ return cartEntry;
1321
+ }
1322
+
1323
+ /**
1324
+ * Remove part from cart
1325
+ * @param {string} cartId
1326
+ */
1327
+ function removePart(cartId) {
1328
+ const idx = state.cart.findIndex(item => item.id === cartId);
1329
+ if (idx >= 0) {
1330
+ if (state.cart[idx].mesh.parent) {
1331
+ state.cart[idx].mesh.parent.remove(state.cart[idx].mesh);
1332
+ }
1333
+ state.cart.splice(idx, 1);
1334
+ }
1335
+ }
1336
+
1337
+ /**
1338
+ * Get total BOM with quantities and prices
1339
+ * @returns {Array}
1340
+ */
1341
+ function getBOM() {
1342
+ const bom = new Map();
1343
+
1344
+ state.cart.forEach(item => {
1345
+ const key = item.partId;
1346
+ if (bom.has(key)) {
1347
+ const existing = bom.get(key);
1348
+ existing.quantity += item.quantity;
1349
+ existing.items.push(item);
1350
+ } else {
1351
+ bom.set(key, {
1352
+ partId: item.partId,
1353
+ part: item.part,
1354
+ quantity: item.quantity,
1355
+ items: [item],
1356
+ totalPrice: (item.part.price.usd || 0) * item.quantity,
1357
+ totalPriceEur: (item.part.price.eur || 0) * item.quantity,
1358
+ });
1359
+ }
1360
+ });
1361
+
1362
+ return Array.from(bom.values());
1363
+ }
1364
+
1365
+ /**
1366
+ * Export BOM as CSV
1367
+ * @returns {string}
1368
+ */
1369
+ function exportBOMAsCSV() {
1370
+ const bom = getBOM();
1371
+ let csv = 'Part Number,Part Name,Category,Quantity,Unit Price USD,Total USD,Unit Price EUR,Total EUR,Supplier\n';
1372
+
1373
+ bom.forEach(entry => {
1374
+ const part = entry.part;
1375
+ const supplier = part.supplier?.mcmaster || part.supplier?.misumi || 'N/A';
1376
+ csv += `"${part.id}","${part.name}","${part.category}",${entry.quantity},`;
1377
+ csv += `${part.price.usd || 0},${entry.totalPrice},`;
1378
+ csv += `${part.price.eur || 0},${entry.totalPriceEur},"${supplier}"\n`;
1379
+ });
1380
+
1381
+ return csv;
1382
+ }
1383
+
1384
+ // ============================================================================
1385
+ // UI GENERATION
1386
+ // ============================================================================
1387
+
1388
+ /**
1389
+ * Generate part thumbnail preview
1390
+ * @param {Object} part
1391
+ * @returns {string} Data URL
1392
+ */
1393
+ function generatePartThumbnail(part) {
1394
+ try {
1395
+ const canvas = document.createElement('canvas');
1396
+ canvas.width = 80;
1397
+ canvas.height = 80;
1398
+
1399
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
1400
+ renderer.setSize(80, 80);
1401
+ renderer.setClearColor(0x1e1e1e, 1);
1402
+
1403
+ const scene = new THREE.Scene();
1404
+ const geometry = getPartGeometry(part);
1405
+ scene.add(geometry);
1406
+
1407
+ const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
1408
+ camera.position.set(20, 20, 20);
1409
+ camera.lookAt(0, 0, 0);
1410
+
1411
+ const light = new THREE.DirectionalLight(0xffffff, 0.8);
1412
+ light.position.set(10, 10, 10);
1413
+ scene.add(light);
1414
+
1415
+ scene.add(new THREE.AmbientLight(0xffffff, 0.4));
1416
+
1417
+ renderer.render(scene, camera);
1418
+ return canvas.toDataURL();
1419
+ } catch (e) {
1420
+ return 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="80" height="80"%3E%3Crect width="80" height="80" fill="%23333"%3E%3C/rect%3E%3C/svg%3E';
1421
+ }
1422
+ }
1423
+
1424
+ /**
1425
+ * Create search results HTML
1426
+ * @returns {string}
1427
+ */
1428
+ function createSearchResultsHTML() {
1429
+ const results = state.searchResults;
1430
+ if (results.length === 0) {
1431
+ return '<div style="padding: 16px; color: #999;">No parts found. Try a different search.</div>';
1432
+ }
1433
+
1434
+ let html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; padding: 12px;">';
1435
+
1436
+ results.forEach(part => {
1437
+ const thumb = generatePartThumbnail(part);
1438
+ const price = part.price?.usd || 'N/A';
1439
+
1440
+ html += `
1441
+ <div class="part-card" data-part-id="${part.id}" style="
1442
+ background: var(--bg-secondary);
1443
+ border: 1px solid var(--border-color);
1444
+ border-radius: 6px;
1445
+ padding: 8px;
1446
+ cursor: pointer;
1447
+ transition: all 0.2s;
1448
+ " onmouseover="this.style.borderColor='var(--accent-blue)'" onmouseout="this.style.borderColor='var(--border-color)'">
1449
+ <img src="${thumb}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px; background: #111;">
1450
+ <div style="margin-top: 8px; font-size: 11px; color: var(--text-primary);">
1451
+ <div style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${part.name}">
1452
+ ${part.name}
1453
+ </div>
1454
+ <div style="color: #999; font-size: 10px; margin-top: 4px;">
1455
+ USD $${typeof price === 'number' ? price.toFixed(2) : price}
1456
+ </div>
1457
+ </div>
1458
+ </div>
1459
+ `;
1460
+ });
1461
+
1462
+ html += '</div>';
1463
+ return html;
1464
+ }
1465
+
1466
+ /**
1467
+ * Create detail view HTML
1468
+ * @returns {string}
1469
+ */
1470
+ function createDetailViewHTML() {
1471
+ const part = state.selectedPart;
1472
+ if (!part) return '';
1473
+
1474
+ const supplier = part.supplier || {};
1475
+ let suppStr = '';
1476
+ if (supplier.mcmaster) suppStr += `<a href="https://www.mcmaster.com/search/${part.id}" target="_blank" style="color: var(--accent-blue); text-decoration: none;">McMaster #${supplier.mcmaster}</a><br>`;
1477
+ if (supplier.misumi) suppStr += `<a href="https://www.misumi.com/search/${part.id}" target="_blank" style="color: var(--accent-blue); text-decoration: none;">Misumi: ${supplier.misumi}</a><br>`;
1478
+ if (supplier.digi) suppStr += `<a href="https://www.digikey.com/search?k=${supplier.digi}" target="_blank" style="color: var(--accent-blue); text-decoration: none;">DigiKey #${supplier.digi}</a>`;
1479
+
1480
+ let dimsTable = '<table style="font-size: 11px; width: 100%;">';
1481
+ Object.entries(part.dimensions || {}).forEach(([key, val]) => {
1482
+ dimsTable += `<tr style="border-bottom: 1px solid var(--border-color);"><td style="padding: 4px;">${key}:</td><td style="padding: 4px; text-align: right;">${val}</td></tr>`;
1483
+ });
1484
+ dimsTable += '</table>';
1485
+
1486
+ return `
1487
+ <div style="padding: 16px; max-height: 400px; overflow-y: auto;">
1488
+ <h3 style="margin: 0 0 12px 0; color: var(--text-primary);">${part.name}</h3>
1489
+ <div style="background: #111; padding: 12px; border-radius: 6px; margin-bottom: 16px;">
1490
+ <img src="${generatePartThumbnail(part)}" style="width: 100%; max-height: 200px; object-fit: contain;">
1491
+ </div>
1492
+
1493
+ <div style="margin-bottom: 16px;">
1494
+ <div style="font-size: 12px; color: #999; margin-bottom: 4px;">STANDARD</div>
1495
+ <div style="color: var(--text-primary);">${part.standard || 'N/A'}</div>
1496
+ </div>
1497
+
1498
+ <div style="margin-bottom: 16px;">
1499
+ <div style="font-size: 12px; color: #999; margin-bottom: 4px;">MATERIAL</div>
1500
+ <div style="color: var(--text-primary);">${part.material || 'N/A'}</div>
1501
+ </div>
1502
+
1503
+ <div style="margin-bottom: 16px;">
1504
+ <div style="font-size: 12px; color: #999; margin-bottom: 4px;">DIMENSIONS (mm)</div>
1505
+ ${dimsTable}
1506
+ </div>
1507
+
1508
+ <div style="margin-bottom: 16px;">
1509
+ <div style="font-size: 12px; color: #999; margin-bottom: 4px;">PRICE (USD)</div>
1510
+ <div style="color: var(--accent-blue); font-weight: 500;">$${(part.price?.usd || 0).toFixed(2)}</div>
1511
+ </div>
1512
+
1513
+ <div style="margin-bottom: 16px;">
1514
+ <div style="font-size: 12px; color: #999; margin-bottom: 4px;">SUPPLIERS</div>
1515
+ <div style="font-size: 11px; line-height: 1.6;">${suppStr || 'No suppliers listed'}</div>
1516
+ </div>
1517
+
1518
+ <button onclick="window.CycleCAD.SmartParts.insertPart(window.CycleCAD.SmartParts.state.selectedPart)" style="
1519
+ width: 100%;
1520
+ padding: 10px;
1521
+ background: var(--accent-blue);
1522
+ color: white;
1523
+ border: none;
1524
+ border-radius: 4px;
1525
+ cursor: pointer;
1526
+ font-weight: 500;
1527
+ font-size: 12px;
1528
+ ">INSERT INTO SCENE</button>
1529
+ </div>
1530
+ `;
1531
+ }
1532
+
1533
+ /**
1534
+ * Create BOM panel HTML
1535
+ * @returns {string}
1536
+ */
1537
+ function createBOMPanelHTML() {
1538
+ const bom = getBOM();
1539
+
1540
+ if (bom.length === 0) {
1541
+ return '<div style="padding: 16px; color: #999;">BOM is empty. Insert parts to build a bill of materials.</div>';
1542
+ }
1543
+
1544
+ let totalUSD = 0, totalEUR = 0;
1545
+ let html = '<div style="padding: 12px; max-height: 300px; overflow-y: auto;">';
1546
+ html += '<table style="width: 100%; font-size: 11px; border-collapse: collapse;">';
1547
+ html += '<tr style="border-bottom: 2px solid var(--border-color); color: #999; font-weight: 500;">';
1548
+ html += '<th style="text-align: left; padding: 8px;">Part</th>';
1549
+ html += '<th style="text-align: center; padding: 8px;">Qty</th>';
1550
+ html += '<th style="text-align: right; padding: 8px;">USD</th>';
1551
+ html += '<th style="text-align: right; padding: 8px;">EUR</th>';
1552
+ html += '</tr>';
1553
+
1554
+ bom.forEach(entry => {
1555
+ totalUSD += entry.totalPrice;
1556
+ totalEUR += entry.totalPriceEur;
1557
+
1558
+ html += `<tr style="border-bottom: 1px solid var(--border-color);">
1559
+ <td style="padding: 8px; color: var(--text-primary);">${entry.part.name}</td>
1560
+ <td style="text-align: center; padding: 8px; color: #999;">${entry.quantity}</td>
1561
+ <td style="text-align: right; padding: 8px; color: #999;">$${entry.totalPrice.toFixed(2)}</td>
1562
+ <td style="text-align: right; padding: 8px; color: #999;">€${entry.totalPriceEur.toFixed(2)}</td>
1563
+ </tr>`;
1564
+ });
1565
+
1566
+ html += `<tr style="border-top: 2px solid var(--border-color); font-weight: 500; color: var(--accent-blue);">
1567
+ <td colspan="2" style="padding: 8px;">TOTAL</td>
1568
+ <td style="text-align: right; padding: 8px;">$${totalUSD.toFixed(2)}</td>
1569
+ <td style="text-align: right; padding: 8px;">€${totalEUR.toFixed(2)}</td>
1570
+ </tr>`;
1571
+
1572
+ html += '</table>';
1573
+ html += '<div style="margin-top: 12px; display: flex; gap: 8px;">';
1574
+ html += '<button onclick="alert(window.CycleCAD.SmartParts.exportBOMAsCSV())" style="flex: 1; padding: 8px; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 4px; cursor: pointer; font-size: 11px;">Export CSV</button>';
1575
+ html += '</div>';
1576
+ html += '</div>';
1577
+
1578
+ return html;
1579
+ }
1580
+
1581
+ // ============================================================================
1582
+ // MODULE INTERFACE
1583
+ // ============================================================================
1584
+
1585
+ /**
1586
+ * Initialize module
1587
+ * @param {THREE.Scene} sceneRef
1588
+ */
1589
+ function init(sceneRef) {
1590
+ scene = sceneRef;
1591
+ console.log('[SmartParts] Initialized with', Object.keys(partCatalog).length, 'parts');
1592
+ }
1593
+
1594
+ /**
1595
+ * Get UI panel HTML
1596
+ * @returns {string}
1597
+ */
1598
+ function getUI() {
1599
+ const categories = getCategories();
1600
+
1601
+ return `
1602
+ <div style="display: flex; flex-direction: column; height: 100%; background: var(--bg-secondary);">
1603
+ <!-- Header -->
1604
+ <div style="padding: 12px; border-bottom: 1px solid var(--border-color);">
1605
+ <div style="font-weight: 500; color: var(--text-primary); margin-bottom: 8px;">Smart Parts Library</div>
1606
+ <input type="text" placeholder="Search parts... (M8, bearing, motor, etc.)" id="smart-parts-search" style="
1607
+ width: 100%;
1608
+ padding: 8px;
1609
+ background: var(--bg-primary);
1610
+ border: 1px solid var(--border-color);
1611
+ color: var(--text-primary);
1612
+ border-radius: 4px;
1613
+ font-size: 12px;
1614
+ box-sizing: border-box;
1615
+ " onkeyup="window.CycleCAD.SmartParts.onSearchInput(this.value)">
1616
+ </div>
1617
+
1618
+ <!-- Tabs -->
1619
+ <div style="display: flex; border-bottom: 1px solid var(--border-color); background: var(--bg-primary);">
1620
+ <button id="tab-search" class="smart-parts-tab" style="flex: 1; padding: 8px; background: var(--accent-blue); color: white; border: none; cursor: pointer; font-size: 11px; font-weight: 500;">Search</button>
1621
+ <button id="tab-categories" class="smart-parts-tab" style="flex: 1; padding: 8px; background: var(--bg-secondary); color: var(--text-primary); border: none; border-left: 1px solid var(--border-color); cursor: pointer; font-size: 11px;">Categories</button>
1622
+ <button id="tab-bom" class="smart-parts-tab" style="flex: 1; padding: 8px; background: var(--bg-secondary); color: var(--text-primary); border: none; border-left: 1px solid var(--border-color); cursor: pointer; font-size: 11px;">BOM (${state.cart.length})</button>
1623
+ </div>
1624
+
1625
+ <!-- Content -->
1626
+ <div style="flex: 1; overflow-y: auto;">
1627
+ <!-- Search Tab -->
1628
+ <div id="smart-parts-search-tab" style="display: block;">
1629
+ <div id="smart-parts-results">${createSearchResultsHTML()}</div>
1630
+ <div id="smart-parts-detail" style="display: none; border-top: 1px solid var(--border-color);">
1631
+ ${createDetailViewHTML()}
1632
+ </div>
1633
+ </div>
1634
+
1635
+ <!-- Categories Tab -->
1636
+ <div id="smart-parts-categories-tab" style="display: none; padding: 12px;">
1637
+ ${categories.map(cat => `
1638
+ <div style="margin-bottom: 8px;">
1639
+ <button onclick="window.CycleCAD.SmartParts.onCategoryClick('${cat}')" style="
1640
+ width: 100%;
1641
+ padding: 8px;
1642
+ background: var(--bg-primary);
1643
+ border: 1px solid var(--border-color);
1644
+ color: var(--text-primary);
1645
+ border-radius: 4px;
1646
+ cursor: pointer;
1647
+ text-align: left;
1648
+ font-size: 12px;
1649
+ ">${cat}</button>
1650
+ </div>
1651
+ `).join('')}
1652
+ </div>
1653
+
1654
+ <!-- BOM Tab -->
1655
+ <div id="smart-parts-bom-tab" style="display: none;">
1656
+ ${createBOMPanelHTML()}
1657
+ </div>
1658
+ </div>
1659
+ </div>
1660
+ `;
1661
+ }
1662
+
1663
+ /**
1664
+ * Execute command
1665
+ * @param {string} command
1666
+ * @param {Object} params
1667
+ */
1668
+ function execute(command, params = {}) {
1669
+ switch (command) {
1670
+ case 'search':
1671
+ return search(params.query, params.filters);
1672
+ case 'insert':
1673
+ return insertPart(params.part, params.position, params.config);
1674
+ case 'remove':
1675
+ return removePart(params.cartId);
1676
+ case 'getBOM':
1677
+ return getBOM();
1678
+ case 'exportBOM':
1679
+ return exportBOMAsCSV();
1680
+ case 'getCategories':
1681
+ return getCategories();
1682
+ case 'getSubcategories':
1683
+ return getSubcategories(params.category);
1684
+ default:
1685
+ console.warn('[SmartParts] Unknown command:', command);
1686
+ }
1687
+ }
1688
+
1689
+ /**
1690
+ * Handle search input (exposed for UI)
1691
+ */
1692
+ function onSearchInput(query) {
1693
+ search(query, state.filters);
1694
+ const resultsDiv = document.getElementById('smart-parts-results');
1695
+ if (resultsDiv) {
1696
+ resultsDiv.innerHTML = createSearchResultsHTML();
1697
+ setupPartCardListeners();
1698
+ }
1699
+ }
1700
+
1701
+ /**
1702
+ * Handle category click
1703
+ */
1704
+ function onCategoryClick(category) {
1705
+ state.filters.category = category;
1706
+ const results = searchCatalog('', state.filters);
1707
+ state.searchResults = results;
1708
+
1709
+ const resultsDiv = document.getElementById('smart-parts-results');
1710
+ if (resultsDiv) {
1711
+ resultsDiv.innerHTML = createSearchResultsHTML();
1712
+ setupPartCardListeners();
1713
+ }
1714
+ }
1715
+
1716
+ /**
1717
+ * Setup click listeners for part cards
1718
+ */
1719
+ function setupPartCardListeners() {
1720
+ document.querySelectorAll('.part-card').forEach(card => {
1721
+ card.addEventListener('click', () => {
1722
+ const partId = card.dataset.partId;
1723
+ const part = partCatalog[partId];
1724
+ if (part) {
1725
+ state.selectedPart = part;
1726
+ const detailDiv = document.getElementById('smart-parts-detail');
1727
+ if (detailDiv) {
1728
+ detailDiv.style.display = 'block';
1729
+ detailDiv.innerHTML = createDetailViewHTML();
1730
+ }
1731
+ }
1732
+ });
1733
+ });
1734
+ }
1735
+
1736
+ // ============================================================================
1737
+ // EXPOSE PUBLIC API
1738
+ // ============================================================================
1739
+
1740
+ return {
1741
+ init,
1742
+ getUI,
1743
+ execute,
1744
+ search,
1745
+ getCatalog,
1746
+ insertPart,
1747
+ removePart,
1748
+ getBOM,
1749
+ exportBOMAsCSV,
1750
+ onSearchInput,
1751
+ onCategoryClick,
1752
+ setupPartCardListeners,
1753
+ state,
1754
+ };
1755
+ })();