cyclecad 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,523 @@
1
+ /**
2
+ * params.js - Parameter editor panel for cycleCAD
3
+ * Manages feature parameters, materials, and property display
4
+ */
5
+
6
+ const MATERIALS = {
7
+ Steel: {
8
+ density: 7.85, // g/cm³
9
+ color: 0x8899aa,
10
+ label: 'Steel',
11
+ },
12
+ Aluminum: {
13
+ density: 2.7,
14
+ color: 0xb0b8c4,
15
+ label: 'Aluminum',
16
+ },
17
+ ABS: {
18
+ density: 1.05,
19
+ color: 0x2a2a2e,
20
+ label: 'ABS',
21
+ },
22
+ Brass: {
23
+ density: 8.5,
24
+ color: 0xc4a54a,
25
+ label: 'Brass',
26
+ },
27
+ Titanium: {
28
+ density: 4.5,
29
+ color: 0x8a8a90,
30
+ label: 'Titanium',
31
+ },
32
+ Nylon: {
33
+ density: 1.14,
34
+ color: 0xe8e0d0,
35
+ label: 'Nylon',
36
+ },
37
+ };
38
+
39
+ let paramsState = {
40
+ containerEl: null,
41
+ currentFeature: null,
42
+ onParamChangeCallback: null,
43
+ onMaterialChangeCallback: null,
44
+ };
45
+
46
+ /**
47
+ * Initialize the parameters panel
48
+ * @param {HTMLElement} containerEl - Container for the panel
49
+ */
50
+ export function initParams(containerEl) {
51
+ paramsState.containerEl = containerEl;
52
+ paramsState.currentFeature = null;
53
+
54
+ // Create panel structure
55
+ containerEl.innerHTML = `
56
+ <div class="params-panel">
57
+ <div class="params-header">
58
+ <h3>Properties</h3>
59
+ </div>
60
+ <div class="params-content" id="params-content">
61
+ <div class="params-empty">
62
+ <p>Select a feature to see parameters</p>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ `;
67
+
68
+ // Add styles if not already present
69
+ if (!document.getElementById('params-styles')) {
70
+ const style = document.createElement('style');
71
+ style.id = 'params-styles';
72
+ style.textContent = `
73
+ .params-panel {
74
+ display: flex;
75
+ flex-direction: column;
76
+ height: 100%;
77
+ background: var(--surface, #1e1e1e);
78
+ color: var(--text, #e0e0e0);
79
+ border-left: 1px solid var(--border, #333);
80
+ }
81
+
82
+ .params-header {
83
+ padding: 16px;
84
+ border-bottom: 1px solid var(--border, #333);
85
+ }
86
+
87
+ .params-header h3 {
88
+ margin: 0;
89
+ font-size: 14px;
90
+ font-weight: 600;
91
+ color: var(--text, #e0e0e0);
92
+ }
93
+
94
+ .params-content {
95
+ flex: 1;
96
+ overflow-y: auto;
97
+ min-height: 0;
98
+ padding: 16px;
99
+ }
100
+
101
+ .params-content::-webkit-scrollbar {
102
+ width: 8px;
103
+ }
104
+
105
+ .params-content::-webkit-scrollbar-track {
106
+ background: transparent;
107
+ }
108
+
109
+ .params-content::-webkit-scrollbar-thumb {
110
+ background: var(--border, #333);
111
+ border-radius: 4px;
112
+ }
113
+
114
+ .params-content::-webkit-scrollbar-thumb:hover {
115
+ background: var(--text2, #a0a0a0);
116
+ }
117
+
118
+ .params-empty {
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ height: 100%;
123
+ color: var(--text2, #a0a0a0);
124
+ font-size: 13px;
125
+ }
126
+
127
+ .params-group {
128
+ margin-bottom: 20px;
129
+ }
130
+
131
+ .params-group-title {
132
+ font-size: 12px;
133
+ font-weight: 600;
134
+ color: var(--text2, #a0a0a0);
135
+ text-transform: uppercase;
136
+ margin-bottom: 12px;
137
+ letter-spacing: 0.5px;
138
+ }
139
+
140
+ .param-row {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 8px;
144
+ margin-bottom: 12px;
145
+ }
146
+
147
+ .param-label {
148
+ flex: 1;
149
+ font-size: 13px;
150
+ color: var(--text, #e0e0e0);
151
+ white-space: nowrap;
152
+ overflow: hidden;
153
+ text-overflow: ellipsis;
154
+ }
155
+
156
+ .param-input-wrapper {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 4px;
160
+ }
161
+
162
+ .param-input {
163
+ width: 80px;
164
+ padding: 6px 8px;
165
+ background: var(--bg, #141414);
166
+ border: 1px solid var(--border, #333);
167
+ border-radius: 3px;
168
+ color: var(--text, #e0e0e0);
169
+ font-size: 12px;
170
+ font-family: monospace;
171
+ transition: border-color 0.15s, background 0.15s;
172
+ }
173
+
174
+ .param-input:focus {
175
+ outline: none;
176
+ border-color: var(--accent, #6496ff);
177
+ background: var(--bg, #1a1a1a);
178
+ }
179
+
180
+ .param-unit {
181
+ font-size: 12px;
182
+ color: var(--text2, #a0a0a0);
183
+ min-width: 24px;
184
+ text-align: right;
185
+ }
186
+
187
+ .material-section {
188
+ margin-bottom: 20px;
189
+ }
190
+
191
+ .material-label {
192
+ font-size: 12px;
193
+ font-weight: 600;
194
+ color: var(--text2, #a0a0a0);
195
+ text-transform: uppercase;
196
+ margin-bottom: 8px;
197
+ letter-spacing: 0.5px;
198
+ }
199
+
200
+ .material-select {
201
+ width: 100%;
202
+ padding: 8px 10px;
203
+ background: var(--bg, #141414);
204
+ border: 1px solid var(--border, #333);
205
+ border-radius: 3px;
206
+ color: var(--text, #e0e0e0);
207
+ font-size: 13px;
208
+ cursor: pointer;
209
+ transition: border-color 0.15s, background 0.15s;
210
+ }
211
+
212
+ .material-select:focus {
213
+ outline: none;
214
+ border-color: var(--accent, #6496ff);
215
+ background: var(--bg, #1a1a1a);
216
+ }
217
+
218
+ .material-select option {
219
+ background: var(--surface, #1e1e1e);
220
+ color: var(--text, #e0e0e0);
221
+ }
222
+
223
+ .material-info {
224
+ margin-top: 12px;
225
+ padding: 10px 12px;
226
+ background: rgba(100, 150, 255, 0.08);
227
+ border-left: 3px solid var(--accent, #6496ff);
228
+ border-radius: 2px;
229
+ font-size: 12px;
230
+ color: var(--text, #e0e0e0);
231
+ }
232
+
233
+ .material-info-row {
234
+ display: flex;
235
+ justify-content: space-between;
236
+ margin-bottom: 4px;
237
+ }
238
+
239
+ .material-info-row:last-child {
240
+ margin-bottom: 0;
241
+ }
242
+
243
+ .material-info-label {
244
+ color: var(--text2, #a0a0a0);
245
+ }
246
+
247
+ .material-color-preview {
248
+ display: inline-block;
249
+ width: 16px;
250
+ height: 16px;
251
+ border-radius: 2px;
252
+ border: 1px solid var(--border, #333);
253
+ vertical-align: middle;
254
+ margin-right: 6px;
255
+ }
256
+ `;
257
+ document.head.appendChild(style);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Show parameters for a feature
263
+ * @param {Object} feature - Feature object with params
264
+ */
265
+ export function showParams(feature) {
266
+ if (!feature) {
267
+ clearParams();
268
+ return;
269
+ }
270
+
271
+ paramsState.currentFeature = feature;
272
+ const content = document.getElementById('params-content');
273
+ if (!content) return;
274
+
275
+ let html = '';
276
+
277
+ // Feature info
278
+ html += `
279
+ <div class="params-group">
280
+ <div class="params-group-title">Feature</div>
281
+ <div class="param-row">
282
+ <span class="param-label">Type:</span>
283
+ <span class="param-unit">${feature.type}</span>
284
+ </div>
285
+ <div class="param-row">
286
+ <span class="param-label">Name:</span>
287
+ <span class="param-unit">${feature.name}</span>
288
+ </div>
289
+ </div>
290
+ `;
291
+
292
+ // Parameters section
293
+ if (feature.params && Object.keys(feature.params).length > 0) {
294
+ html += `<div class="params-group">`;
295
+ html += `<div class="params-group-title">Parameters</div>`;
296
+
297
+ for (const [paramName, paramValue] of Object.entries(feature.params)) {
298
+ const displayName = paramName
299
+ .replace(/([A-Z])/g, ' $1')
300
+ .replace(/^./, (str) => str.toUpperCase())
301
+ .trim();
302
+
303
+ html += `
304
+ <div class="param-row">
305
+ <label class="param-label" for="param-${paramName}">${displayName}:</label>
306
+ <div class="param-input-wrapper">
307
+ <input
308
+ type="number"
309
+ id="param-${paramName}"
310
+ class="param-input"
311
+ data-param="${paramName}"
312
+ value="${typeof paramValue === 'number' ? paramValue.toFixed(2) : paramValue}"
313
+ step="0.1"
314
+ />
315
+ <span class="param-unit">mm</span>
316
+ </div>
317
+ </div>
318
+ `;
319
+ }
320
+
321
+ html += `</div>`;
322
+ }
323
+
324
+ // Material section
325
+ html += `
326
+ <div class="material-section">
327
+ <div class="material-label">Material</div>
328
+ <select class="material-select" id="material-select">
329
+ `;
330
+
331
+ for (const [key, mat] of Object.entries(MATERIALS)) {
332
+ const selected = feature.material === key ? 'selected' : '';
333
+ html += `<option value="${key}" ${selected}>${mat.label}</option>`;
334
+ }
335
+
336
+ html += `
337
+ </select>
338
+ <div class="material-info" id="material-info">
339
+ ${getMaterialInfoHtml(feature.material || 'Steel')}
340
+ </div>
341
+ </div>
342
+ `;
343
+
344
+ content.innerHTML = html;
345
+
346
+ // Attach event listeners
347
+ attachParamListeners();
348
+ attachMaterialListener();
349
+ }
350
+
351
+ /**
352
+ * Show only material selector (for non-parameter features)
353
+ * @param {Object} feature - Feature object
354
+ */
355
+ export function showMaterial(feature) {
356
+ if (!feature) {
357
+ clearParams();
358
+ return;
359
+ }
360
+
361
+ paramsState.currentFeature = feature;
362
+ const content = document.getElementById('params-content');
363
+ if (!content) return;
364
+
365
+ const html = `
366
+ <div class="material-section">
367
+ <div class="material-label">Material</div>
368
+ <select class="material-select" id="material-select">
369
+ ${Object.entries(MATERIALS)
370
+ .map(([key, mat]) => {
371
+ const selected = feature.material === key ? 'selected' : '';
372
+ return `<option value="${key}" ${selected}>${mat.label}</option>`;
373
+ })
374
+ .join('')}
375
+ </select>
376
+ <div class="material-info" id="material-info">
377
+ ${getMaterialInfoHtml(feature.material || 'Steel')}
378
+ </div>
379
+ </div>
380
+ `;
381
+
382
+ content.innerHTML = html;
383
+ attachMaterialListener();
384
+ }
385
+
386
+ /**
387
+ * Clear the parameters display
388
+ */
389
+ export function clearParams() {
390
+ paramsState.currentFeature = null;
391
+ const content = document.getElementById('params-content');
392
+ if (content) {
393
+ content.innerHTML = `
394
+ <div class="params-empty">
395
+ <p>Select a feature to see parameters</p>
396
+ </div>
397
+ `;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Register callback for parameter changes
403
+ * @param {Function} callback - Called with (feature, paramName, newValue)
404
+ */
405
+ export function onParamChange(callback) {
406
+ paramsState.onParamChangeCallback = callback;
407
+ }
408
+
409
+ /**
410
+ * Register callback for material changes
411
+ * @param {Function} callback - Called with (feature, material)
412
+ */
413
+ export function onMaterialChange(callback) {
414
+ paramsState.onMaterialChangeCallback = callback;
415
+ }
416
+
417
+ /**
418
+ * Get material info (density, color)
419
+ * @param {string} materialName - Material key
420
+ * @returns {Object} Material object
421
+ */
422
+ export function getMaterial(materialName) {
423
+ return MATERIALS[materialName] || MATERIALS.Steel;
424
+ }
425
+
426
+ /**
427
+ * Get all materials
428
+ * @returns {Object} All materials
429
+ */
430
+ export function getMaterials() {
431
+ return { ...MATERIALS };
432
+ }
433
+
434
+ /**
435
+ * Internal: Generate material info HTML
436
+ */
437
+ function getMaterialInfoHtml(materialName) {
438
+ const mat = MATERIALS[materialName] || MATERIALS.Steel;
439
+ const colorHex = mat.color.toString(16).padStart(6, '0');
440
+
441
+ return `
442
+ <div class="material-info-row">
443
+ <span class="material-info-label">Density:</span>
444
+ <span>${mat.density} g/cm³</span>
445
+ </div>
446
+ <div class="material-info-row">
447
+ <span class="material-info-label">Color:</span>
448
+ <span>
449
+ <div class="material-color-preview" style="background-color: #${colorHex};"></div>
450
+ #${colorHex}
451
+ </span>
452
+ </div>
453
+ `;
454
+ }
455
+
456
+ /**
457
+ * Internal: Attach parameter input listeners
458
+ */
459
+ function attachParamListeners() {
460
+ const paramInputs = document.querySelectorAll('.param-input');
461
+
462
+ paramInputs.forEach((input) => {
463
+ input.addEventListener('change', () => {
464
+ if (!paramsState.currentFeature) return;
465
+
466
+ const paramName = input.dataset.param;
467
+ const newValue = parseFloat(input.value);
468
+
469
+ if (!isNaN(newValue)) {
470
+ paramsState.currentFeature.params[paramName] = newValue;
471
+
472
+ if (paramsState.onParamChangeCallback) {
473
+ paramsState.onParamChangeCallback(
474
+ paramsState.currentFeature,
475
+ paramName,
476
+ newValue
477
+ );
478
+ }
479
+ }
480
+ });
481
+
482
+ // Real-time preview on input (optional)
483
+ input.addEventListener('input', () => {
484
+ // Could trigger a live preview here
485
+ });
486
+ });
487
+ }
488
+
489
+ /**
490
+ * Internal: Attach material select listener
491
+ */
492
+ function attachMaterialListener() {
493
+ const materialSelect = document.getElementById('material-select');
494
+ if (!materialSelect) return;
495
+
496
+ materialSelect.addEventListener('change', () => {
497
+ if (!paramsState.currentFeature) return;
498
+
499
+ const materialName = materialSelect.value;
500
+ paramsState.currentFeature.material = materialName;
501
+
502
+ // Update material info display
503
+ const materialInfo = document.getElementById('material-info');
504
+ if (materialInfo) {
505
+ materialInfo.innerHTML = getMaterialInfoHtml(materialName);
506
+ }
507
+
508
+ if (paramsState.onMaterialChangeCallback) {
509
+ paramsState.onMaterialChangeCallback(paramsState.currentFeature, materialName);
510
+ }
511
+ });
512
+ }
513
+
514
+ export default {
515
+ initParams,
516
+ showParams,
517
+ showMaterial,
518
+ clearParams,
519
+ onParamChange,
520
+ onMaterialChange,
521
+ getMaterial,
522
+ getMaterials,
523
+ };