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