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.
- package/app/index.html +48 -0
- package/app/js/modules/generative-design.js +949 -0
- package/app/js/modules/multi-physics.js +1244 -0
- package/app/js/modules/smart-parts.js +1755 -0
- package/app/tests/KILLER_FEATURES_BATCH2_README.md +214 -0
- package/app/tests/killer-features-batch2-tests.html +849 -0
- package/package.json +1 -1
|
@@ -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
|
+
})();
|