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.
- package/app/HELP-QUICK-START.md +207 -0
- package/app/HELP-SYSTEM-README.md +287 -0
- package/app/help-viewer.html +805 -0
- package/app/index.html +48 -0
- package/app/js/killer-features-help.json +310 -391
- package/app/js/modules/generative-design.js +1102 -0
- package/app/js/modules/manufacturability.js +170 -3
- package/app/js/modules/multi-physics.js +1404 -0
- package/app/js/modules/photo-to-cad.js +200 -10
- package/app/js/modules/smart-parts.js +1925 -0
- package/app/js/modules/text-to-cad.js +242 -33
- package/app/tests/KILLER_FEATURES_BATCH2_README.md +214 -0
- package/app/tests/KILLER_FEATURES_TEST_GUIDE.md +324 -0
- package/app/tests/index.html +24 -7
- package/app/tests/killer-features-batch2-tests.html +849 -0
- package/app/tests/killer-features-visual-test.html +1362 -0
- package/docs/KILLER-FEATURES-GUIDE.md +2728 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +1663 -5
- package/package.json +1 -1
|
@@ -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
|
+
})();
|