syd 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
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.
- syd/__init__.py +2 -1
- syd/flask_deployment/deployer.py +107 -23
- syd/flask_deployment/static/css/styles.css +77 -62
- syd/flask_deployment/static/css/viewer.css +0 -42
- syd/flask_deployment/static/js/modules/api.js +89 -0
- syd/flask_deployment/static/js/modules/config.js +22 -0
- syd/flask_deployment/static/js/modules/plot.js +75 -0
- syd/flask_deployment/static/js/modules/state.js +89 -0
- syd/flask_deployment/static/js/modules/system_controls.js +191 -0
- syd/flask_deployment/static/js/modules/ui_controls.js +812 -0
- syd/flask_deployment/static/js/modules/utils.js +49 -0
- syd/flask_deployment/static/js/old_viewer.js +1195 -0
- syd/flask_deployment/static/js/viewer.js +40 -1177
- syd/flask_deployment/templates/index.html +1 -1
- syd/support.py +26 -1
- syd/viewer.py +4 -0
- {syd-1.1.0.dist-info → syd-1.2.1.dist-info}/METADATA +18 -5
- syd-1.2.1.dist-info/RECORD +28 -0
- syd-1.1.0.dist-info/RECORD +0 -20
- {syd-1.1.0.dist-info → syd-1.2.1.dist-info}/WHEEL +0 -0
- {syd-1.1.0.dist-info → syd-1.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { paramOrder, paramInfo, state, updateParameter } from './state.js';
|
|
2
|
+
import { formatLabel } from './utils.js';
|
|
3
|
+
import { handleButtonClick } from './api.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create UI controls based on parameter types
|
|
7
|
+
*/
|
|
8
|
+
export function createControls() {
|
|
9
|
+
const paramControlsContainer = document.getElementById('parameter-controls');
|
|
10
|
+
if (!paramControlsContainer) {
|
|
11
|
+
console.error("Element with ID 'parameter-controls' not found.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Clear any existing parameter controls first
|
|
16
|
+
// Add Parameters header
|
|
17
|
+
const paramHeader = document.createElement('div');
|
|
18
|
+
paramHeader.className = 'section-header';
|
|
19
|
+
paramHeader.innerHTML = '<b>Parameters</b>';
|
|
20
|
+
paramControlsContainer.innerHTML = ''; // Clear container AFTER getting header content
|
|
21
|
+
paramControlsContainer.appendChild(paramHeader);
|
|
22
|
+
|
|
23
|
+
// Create controls for each parameter in the order specified by the viewer
|
|
24
|
+
paramOrder.forEach(name => {
|
|
25
|
+
const param = paramInfo[name];
|
|
26
|
+
if (!param) {
|
|
27
|
+
console.warn(`Parameter info not found for ${name} during control creation.`);
|
|
28
|
+
return; // Skip if param info is missing for some reason
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create control group
|
|
32
|
+
const controlGroup = createControlGroup(name, param);
|
|
33
|
+
|
|
34
|
+
// Add to container
|
|
35
|
+
if (controlGroup) {
|
|
36
|
+
paramControlsContainer.appendChild(controlGroup);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a control group for a parameter
|
|
43
|
+
*/
|
|
44
|
+
function createControlGroup(name, param) {
|
|
45
|
+
// Skip if param type is unknown
|
|
46
|
+
if (!param.type || param.type === 'unknown') {
|
|
47
|
+
console.warn(`Unknown parameter type for ${name}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create control group div
|
|
52
|
+
const controlGroup = document.createElement('div');
|
|
53
|
+
controlGroup.className = 'control-group';
|
|
54
|
+
controlGroup.id = `control-group-${name}`;
|
|
55
|
+
|
|
56
|
+
// Add label
|
|
57
|
+
const label = document.createElement('span');
|
|
58
|
+
label.className = 'control-label';
|
|
59
|
+
label.textContent = formatLabel(name);
|
|
60
|
+
controlGroup.appendChild(label);
|
|
61
|
+
|
|
62
|
+
// Create specific control based on parameter type
|
|
63
|
+
const control = createControl(name, param);
|
|
64
|
+
if (control) {
|
|
65
|
+
controlGroup.appendChild(control);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return controlGroup;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a specific control based on parameter type
|
|
73
|
+
*/
|
|
74
|
+
function createControl(name, param) {
|
|
75
|
+
switch (param.type) {
|
|
76
|
+
case 'text':
|
|
77
|
+
return createTextControl(name, param);
|
|
78
|
+
case 'boolean':
|
|
79
|
+
return createBooleanControl(name, param);
|
|
80
|
+
case 'integer':
|
|
81
|
+
return createIntegerControl(name, param);
|
|
82
|
+
case 'float':
|
|
83
|
+
return createFloatControl(name, param);
|
|
84
|
+
case 'selection':
|
|
85
|
+
return createSelectionControl(name, param);
|
|
86
|
+
case 'multiple-selection':
|
|
87
|
+
return createMultipleSelectionControl(name, param);
|
|
88
|
+
case 'integer-range':
|
|
89
|
+
return createIntegerRangeControl(name, param);
|
|
90
|
+
case 'float-range':
|
|
91
|
+
return createFloatRangeControl(name, param);
|
|
92
|
+
case 'unbounded-integer':
|
|
93
|
+
return createUnboundedIntegerControl(name, param);
|
|
94
|
+
case 'unbounded-float':
|
|
95
|
+
return createUnboundedFloatControl(name, param);
|
|
96
|
+
case 'button':
|
|
97
|
+
return createButtonControl(name, param);
|
|
98
|
+
default:
|
|
99
|
+
console.warn(`No control implementation for type: ${param.type}`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create text input control
|
|
106
|
+
*/
|
|
107
|
+
function createTextControl(name, param) {
|
|
108
|
+
const input = document.createElement('input');
|
|
109
|
+
input.type = 'text';
|
|
110
|
+
input.id = `${name}-input`;
|
|
111
|
+
input.value = param.value || '';
|
|
112
|
+
|
|
113
|
+
input.addEventListener('change', function() {
|
|
114
|
+
updateParameter(name, this.value);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return input;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create boolean checkbox control
|
|
122
|
+
*/
|
|
123
|
+
function createBooleanControl(name, param) {
|
|
124
|
+
const container = document.createElement('div');
|
|
125
|
+
container.className = 'checkbox-container';
|
|
126
|
+
|
|
127
|
+
const checkbox = document.createElement('input');
|
|
128
|
+
checkbox.type = 'checkbox';
|
|
129
|
+
checkbox.id = `${name}-checkbox`;
|
|
130
|
+
checkbox.checked = param.value === true;
|
|
131
|
+
|
|
132
|
+
checkbox.addEventListener('change', function() {
|
|
133
|
+
updateParameter(name, this.checked);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
container.appendChild(checkbox);
|
|
137
|
+
return container;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create selection dropdown control
|
|
142
|
+
*/
|
|
143
|
+
function createSelectionControl(name, param) {
|
|
144
|
+
const select = document.createElement('select');
|
|
145
|
+
select.id = `${name}-select`;
|
|
146
|
+
|
|
147
|
+
// Add options
|
|
148
|
+
if (param.options && Array.isArray(param.options)) {
|
|
149
|
+
param.options.forEach(option => {
|
|
150
|
+
const optionElement = document.createElement('option');
|
|
151
|
+
optionElement.value = option;
|
|
152
|
+
optionElement.textContent = formatLabel(String(option));
|
|
153
|
+
// Store the original type information as a data attribute
|
|
154
|
+
optionElement.dataset.originalType = typeof option;
|
|
155
|
+
// For float values, also store the original value for exact comparison
|
|
156
|
+
if (typeof option === 'number') {
|
|
157
|
+
optionElement.dataset.originalValue = option;
|
|
158
|
+
}
|
|
159
|
+
select.appendChild(optionElement);
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
console.warn(`No options or invalid options format for selection parameter: ${name}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Set default value
|
|
166
|
+
select.value = param.value;
|
|
167
|
+
|
|
168
|
+
// Add event listener
|
|
169
|
+
select.addEventListener('change', function() {
|
|
170
|
+
// Get the selected option element
|
|
171
|
+
const selectedOption = this.options[this.selectedIndex];
|
|
172
|
+
let valueToSend = this.value;
|
|
173
|
+
|
|
174
|
+
// Convert back to the original type if needed
|
|
175
|
+
if (selectedOption && selectedOption.dataset.originalType === 'number') {
|
|
176
|
+
// Use the original value from the dataset for exact precision with floats
|
|
177
|
+
if (selectedOption.dataset.originalValue) {
|
|
178
|
+
valueToSend = parseFloat(selectedOption.dataset.originalValue);
|
|
179
|
+
} else {
|
|
180
|
+
// Fallback if originalValue wasn't stored (shouldn't happen)
|
|
181
|
+
valueToSend = parseFloat(valueToSend);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// No conversion needed for string, boolean already handled by value
|
|
185
|
+
|
|
186
|
+
updateParameter(name, valueToSend);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return select;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create multiple selection control
|
|
194
|
+
*/
|
|
195
|
+
function createMultipleSelectionControl(name, param) {
|
|
196
|
+
const container = document.createElement('div');
|
|
197
|
+
container.className = 'multiple-selection-container';
|
|
198
|
+
|
|
199
|
+
// Create select element
|
|
200
|
+
const select = document.createElement('select');
|
|
201
|
+
select.id = `${name}-select`;
|
|
202
|
+
select.className = 'multiple-select';
|
|
203
|
+
select.multiple = true;
|
|
204
|
+
|
|
205
|
+
// Add options
|
|
206
|
+
if (param.options && Array.isArray(param.options)) {
|
|
207
|
+
param.options.forEach(option => {
|
|
208
|
+
const optionElement = document.createElement('option');
|
|
209
|
+
optionElement.value = option;
|
|
210
|
+
optionElement.textContent = formatLabel(String(option));
|
|
211
|
+
// Store original type/value info
|
|
212
|
+
optionElement.dataset.originalType = typeof option;
|
|
213
|
+
if (typeof option === 'number') {
|
|
214
|
+
optionElement.dataset.originalValue = option;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if this option is selected in the initial value array
|
|
218
|
+
if (Array.isArray(param.value) && param.value.includes(option)) {
|
|
219
|
+
optionElement.selected = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
select.appendChild(optionElement);
|
|
223
|
+
});
|
|
224
|
+
} else {
|
|
225
|
+
console.warn(`No options or invalid options format for multi-selection parameter: ${name}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Helper text
|
|
229
|
+
const helperText = document.createElement('div');
|
|
230
|
+
helperText.className = 'helper-text';
|
|
231
|
+
helperText.textContent = 'Ctrl+click to select multiple';
|
|
232
|
+
|
|
233
|
+
// Add event listener
|
|
234
|
+
select.addEventListener('change', function() {
|
|
235
|
+
// Get all selected options
|
|
236
|
+
const selectedValues = Array.from(this.selectedOptions).map(option => {
|
|
237
|
+
let value = option.value;
|
|
238
|
+
// Convert back to original type if needed
|
|
239
|
+
if (option.dataset.originalType === 'number') {
|
|
240
|
+
value = option.dataset.originalValue ? parseFloat(option.dataset.originalValue) : parseFloat(value);
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
});
|
|
244
|
+
updateParameter(name, selectedValues);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
container.appendChild(select);
|
|
248
|
+
container.appendChild(helperText);
|
|
249
|
+
return container;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create integer control with slider and number input
|
|
254
|
+
*/
|
|
255
|
+
function createIntegerControl(name, param) {
|
|
256
|
+
const container = document.createElement('div');
|
|
257
|
+
container.className = 'numeric-control';
|
|
258
|
+
|
|
259
|
+
// Create slider
|
|
260
|
+
const slider = document.createElement('input');
|
|
261
|
+
slider.type = 'range';
|
|
262
|
+
slider.id = `${name}-slider`;
|
|
263
|
+
slider.min = param.min;
|
|
264
|
+
slider.max = param.max;
|
|
265
|
+
slider.step = param.step || 1;
|
|
266
|
+
slider.value = param.value;
|
|
267
|
+
|
|
268
|
+
// Create number input
|
|
269
|
+
const input = document.createElement('input');
|
|
270
|
+
input.type = 'number';
|
|
271
|
+
input.id = `${name}-input`;
|
|
272
|
+
input.min = param.min;
|
|
273
|
+
input.max = param.max;
|
|
274
|
+
input.step = param.step || 1;
|
|
275
|
+
input.value = param.value;
|
|
276
|
+
|
|
277
|
+
// Add event listeners
|
|
278
|
+
slider.addEventListener('input', function() {
|
|
279
|
+
input.value = this.value;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
slider.addEventListener('change', function() {
|
|
283
|
+
const value = parseInt(this.value, 10);
|
|
284
|
+
input.value = value;
|
|
285
|
+
updateParameter(name, value);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
input.addEventListener('change', function() {
|
|
289
|
+
const value = parseInt(this.value, 10);
|
|
290
|
+
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
291
|
+
slider.value = value;
|
|
292
|
+
updateParameter(name, value);
|
|
293
|
+
} else {
|
|
294
|
+
// Revert to current state value if input is invalid
|
|
295
|
+
this.value = state[name];
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
container.appendChild(slider);
|
|
300
|
+
container.appendChild(input);
|
|
301
|
+
return container;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Create float control with slider and number input
|
|
306
|
+
*/
|
|
307
|
+
function createFloatControl(name, param) {
|
|
308
|
+
// Use the shared controller creator
|
|
309
|
+
const container = createFloatController(name, param);
|
|
310
|
+
const slider = container.querySelector('input[type="range"]');
|
|
311
|
+
const input = container.querySelector('input[type="number"]');
|
|
312
|
+
|
|
313
|
+
// Add event listeners specific to parameter updates
|
|
314
|
+
slider.addEventListener('input', function() {
|
|
315
|
+
input.value = this.value;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
slider.addEventListener('change', function() {
|
|
319
|
+
const value = parseFloat(this.value);
|
|
320
|
+
input.value = value;
|
|
321
|
+
updateParameter(name, value);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
input.addEventListener('change', function() {
|
|
325
|
+
const value = parseFloat(this.value);
|
|
326
|
+
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
327
|
+
slider.value = value;
|
|
328
|
+
updateParameter(name, value);
|
|
329
|
+
} else {
|
|
330
|
+
// Revert to current state value if input is invalid
|
|
331
|
+
this.value = state[name];
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return container;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create float controller object (container with slider and number input).
|
|
340
|
+
* Exported because it's also used by system_controls.js
|
|
341
|
+
* @param {string} name - Base name for element IDs.
|
|
342
|
+
* @param {object} param - Parameter info (min, max, step, value).
|
|
343
|
+
*/
|
|
344
|
+
export function createFloatController(name, param) {
|
|
345
|
+
const container = document.createElement('div');
|
|
346
|
+
container.className = 'numeric-control';
|
|
347
|
+
|
|
348
|
+
// Create slider
|
|
349
|
+
const slider = document.createElement('input');
|
|
350
|
+
slider.type = 'range';
|
|
351
|
+
slider.id = `${name}-slider`;
|
|
352
|
+
slider.min = param.min;
|
|
353
|
+
slider.max = param.max;
|
|
354
|
+
slider.step = param.step || 0.01; // Default step for float
|
|
355
|
+
slider.value = param.value;
|
|
356
|
+
|
|
357
|
+
// Create number input
|
|
358
|
+
const input = document.createElement('input');
|
|
359
|
+
input.type = 'number';
|
|
360
|
+
input.id = `${name}-input`;
|
|
361
|
+
input.min = param.min;
|
|
362
|
+
input.max = param.max;
|
|
363
|
+
input.step = param.step || 0.01; // Default step for float
|
|
364
|
+
input.value = param.value;
|
|
365
|
+
|
|
366
|
+
container.appendChild(slider);
|
|
367
|
+
container.appendChild(input);
|
|
368
|
+
return container;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Create integer range control with dual sliders
|
|
373
|
+
*/
|
|
374
|
+
function createIntegerRangeControl(name, param) {
|
|
375
|
+
return createRangeControl(name, param, parseInt);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create float range control with dual sliders
|
|
380
|
+
*/
|
|
381
|
+
function createFloatRangeControl(name, param) {
|
|
382
|
+
return createRangeControl(name, param, parseFloat);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Generic range control creator
|
|
387
|
+
*/
|
|
388
|
+
function createRangeControl(name, param, converter) {
|
|
389
|
+
const container = document.createElement('div');
|
|
390
|
+
container.className = 'range-container';
|
|
391
|
+
|
|
392
|
+
// Create inputs container
|
|
393
|
+
const inputsContainer = document.createElement('div');
|
|
394
|
+
inputsContainer.className = 'range-inputs';
|
|
395
|
+
|
|
396
|
+
// Create min input
|
|
397
|
+
const minInput = document.createElement('input');
|
|
398
|
+
minInput.type = 'number';
|
|
399
|
+
minInput.id = `${name}-min-input`;
|
|
400
|
+
minInput.className = 'range-input';
|
|
401
|
+
minInput.min = param.min;
|
|
402
|
+
minInput.max = param.max;
|
|
403
|
+
minInput.step = param.step || (converter === parseInt ? 1 : 0.01); // Default step
|
|
404
|
+
minInput.value = param.value[0];
|
|
405
|
+
|
|
406
|
+
// Create max input
|
|
407
|
+
const maxInput = document.createElement('input');
|
|
408
|
+
maxInput.type = 'number';
|
|
409
|
+
maxInput.id = `${name}-max-input`;
|
|
410
|
+
maxInput.className = 'range-input';
|
|
411
|
+
maxInput.min = param.min;
|
|
412
|
+
maxInput.max = param.max;
|
|
413
|
+
maxInput.step = param.step || (converter === parseInt ? 1 : 0.01);
|
|
414
|
+
maxInput.value = param.value[1];
|
|
415
|
+
|
|
416
|
+
// Create slider container
|
|
417
|
+
const sliderContainer = document.createElement('div');
|
|
418
|
+
sliderContainer.className = 'range-slider-container';
|
|
419
|
+
|
|
420
|
+
// Create min slider
|
|
421
|
+
const minSlider = document.createElement('input');
|
|
422
|
+
minSlider.type = 'range';
|
|
423
|
+
minSlider.id = `${name}-min-slider`;
|
|
424
|
+
minSlider.className = 'range-slider min-slider';
|
|
425
|
+
minSlider.min = param.min;
|
|
426
|
+
minSlider.max = param.max;
|
|
427
|
+
minSlider.step = param.step || (converter === parseInt ? 1 : 0.01);
|
|
428
|
+
minSlider.value = param.value[0];
|
|
429
|
+
|
|
430
|
+
// Create max slider
|
|
431
|
+
const maxSlider = document.createElement('input');
|
|
432
|
+
maxSlider.type = 'range';
|
|
433
|
+
maxSlider.id = `${name}-max-slider`;
|
|
434
|
+
maxSlider.className = 'range-slider max-slider';
|
|
435
|
+
maxSlider.min = param.min;
|
|
436
|
+
maxSlider.max = param.max;
|
|
437
|
+
maxSlider.step = param.step || (converter === parseInt ? 1 : 0.01);
|
|
438
|
+
maxSlider.value = param.value[1];
|
|
439
|
+
|
|
440
|
+
// --- Event Listeners ---
|
|
441
|
+
|
|
442
|
+
// Input listeners (for real-time updates of linked elements and gradient)
|
|
443
|
+
minSlider.addEventListener('input', function() {
|
|
444
|
+
const minVal = converter(this.value);
|
|
445
|
+
const maxVal = converter(maxSlider.value);
|
|
446
|
+
if (minVal <= maxVal) {
|
|
447
|
+
minInput.value = minVal;
|
|
448
|
+
} else {
|
|
449
|
+
// Prevent slider crossing visually, update input to maxVal
|
|
450
|
+
this.value = maxVal;
|
|
451
|
+
minInput.value = maxVal;
|
|
452
|
+
}
|
|
453
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
maxSlider.addEventListener('input', function() {
|
|
457
|
+
const minVal = converter(minSlider.value);
|
|
458
|
+
const maxVal = converter(this.value);
|
|
459
|
+
if (maxVal >= minVal) {
|
|
460
|
+
maxInput.value = maxVal;
|
|
461
|
+
} else {
|
|
462
|
+
// Prevent slider crossing visually, update input to minVal
|
|
463
|
+
this.value = minVal;
|
|
464
|
+
maxInput.value = minVal;
|
|
465
|
+
}
|
|
466
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Change listeners (for updating state and triggering backend calls)
|
|
470
|
+
minSlider.addEventListener('change', function() {
|
|
471
|
+
const minVal = converter(this.value);
|
|
472
|
+
const maxVal = converter(maxSlider.value);
|
|
473
|
+
|
|
474
|
+
// Ensure min doesn't exceed max after potential adjustment during 'input'
|
|
475
|
+
if (minVal <= maxVal) {
|
|
476
|
+
minInput.value = minVal;
|
|
477
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
478
|
+
updateParameter(name, [minVal, maxVal]);
|
|
479
|
+
} else {
|
|
480
|
+
// If somehow it crossed, snap it back and update state
|
|
481
|
+
this.value = maxVal;
|
|
482
|
+
minInput.value = maxVal;
|
|
483
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
484
|
+
updateParameter(name, [maxVal, maxVal]);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
maxSlider.addEventListener('change', function() {
|
|
489
|
+
const minVal = converter(minSlider.value);
|
|
490
|
+
const maxVal = converter(this.value);
|
|
491
|
+
|
|
492
|
+
// Ensure max doesn't go below min after potential adjustment during 'input'
|
|
493
|
+
if (maxVal >= minVal) {
|
|
494
|
+
maxInput.value = maxVal;
|
|
495
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
496
|
+
updateParameter(name, [minVal, maxVal]);
|
|
497
|
+
} else {
|
|
498
|
+
// If somehow it crossed, snap it back and update state
|
|
499
|
+
this.value = minVal;
|
|
500
|
+
maxInput.value = minVal;
|
|
501
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
502
|
+
updateParameter(name, [minVal, minVal]);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
minInput.addEventListener('change', function() {
|
|
507
|
+
const minVal = converter(this.value);
|
|
508
|
+
const maxVal = converter(maxInput.value); // Use current max input value
|
|
509
|
+
|
|
510
|
+
if (!isNaN(minVal) && minVal >= param.min && minVal <= maxVal) {
|
|
511
|
+
minSlider.value = minVal;
|
|
512
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
513
|
+
updateParameter(name, [minVal, maxVal]);
|
|
514
|
+
} else {
|
|
515
|
+
// Revert input value to current state and update slider/gradient
|
|
516
|
+
this.value = state[name][0];
|
|
517
|
+
minSlider.value = state[name][0];
|
|
518
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
maxInput.addEventListener('change', function() {
|
|
523
|
+
const minVal = converter(minInput.value); // Use current min input value
|
|
524
|
+
const maxVal = converter(this.value);
|
|
525
|
+
|
|
526
|
+
if (!isNaN(maxVal) && maxVal <= param.max && maxVal >= minVal) {
|
|
527
|
+
maxSlider.value = maxVal;
|
|
528
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
529
|
+
updateParameter(name, [minVal, maxVal]);
|
|
530
|
+
} else {
|
|
531
|
+
// Revert input value to current state and update slider/gradient
|
|
532
|
+
this.value = state[name][1];
|
|
533
|
+
maxSlider.value = state[name][1];
|
|
534
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Assemble the control
|
|
539
|
+
inputsContainer.appendChild(minInput);
|
|
540
|
+
inputsContainer.appendChild(maxInput);
|
|
541
|
+
|
|
542
|
+
sliderContainer.appendChild(minSlider);
|
|
543
|
+
sliderContainer.appendChild(maxSlider);
|
|
544
|
+
|
|
545
|
+
container.appendChild(inputsContainer);
|
|
546
|
+
container.appendChild(sliderContainer);
|
|
547
|
+
|
|
548
|
+
// Set initial gradient state
|
|
549
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer);
|
|
550
|
+
|
|
551
|
+
return container;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Create unbounded integer control
|
|
556
|
+
*/
|
|
557
|
+
function createUnboundedIntegerControl(name, param) {
|
|
558
|
+
const input = document.createElement('input');
|
|
559
|
+
input.type = 'number';
|
|
560
|
+
input.id = `${name}-input`;
|
|
561
|
+
input.value = param.value;
|
|
562
|
+
input.step = 1;
|
|
563
|
+
|
|
564
|
+
input.addEventListener('change', function() {
|
|
565
|
+
const value = parseInt(this.value, 10);
|
|
566
|
+
if (!isNaN(value)) {
|
|
567
|
+
updateParameter(name, value);
|
|
568
|
+
} else {
|
|
569
|
+
// Revert to current state if input is not a valid integer
|
|
570
|
+
this.value = state[name];
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
return input;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Create unbounded float control
|
|
579
|
+
*/
|
|
580
|
+
function createUnboundedFloatControl(name, param) {
|
|
581
|
+
const input = document.createElement('input');
|
|
582
|
+
input.type = 'number';
|
|
583
|
+
input.id = `${name}-input`;
|
|
584
|
+
input.value = param.value;
|
|
585
|
+
input.step = param.step || 'any'; // Allow any decimal
|
|
586
|
+
|
|
587
|
+
input.addEventListener('change', function() {
|
|
588
|
+
const value = parseFloat(this.value);
|
|
589
|
+
if (!isNaN(value)) {
|
|
590
|
+
updateParameter(name, value);
|
|
591
|
+
} else {
|
|
592
|
+
// Revert to current state if input is not a valid float
|
|
593
|
+
this.value = state[name];
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return input;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Create button control
|
|
602
|
+
*/
|
|
603
|
+
function createButtonControl(name, param) {
|
|
604
|
+
const button = document.createElement('button');
|
|
605
|
+
button.id = `${name}-button`;
|
|
606
|
+
button.textContent = param.label || formatLabel(name); // Use formatLabel for default
|
|
607
|
+
button.className = 'button-control'; // Add a class for styling
|
|
608
|
+
|
|
609
|
+
button.addEventListener('click', function() {
|
|
610
|
+
// Call the handler from api.js
|
|
611
|
+
handleButtonClick(name);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
return button;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Updates the background gradient for the dual range slider.
|
|
619
|
+
* @param {HTMLInputElement} minSlider - The minimum value slider element.
|
|
620
|
+
* @param {HTMLInputElement} maxSlider - The maximum value slider element.
|
|
621
|
+
* @param {HTMLElement} container - The container element holding the sliders.
|
|
622
|
+
*/
|
|
623
|
+
function updateSliderGradient(minSlider, maxSlider, container) {
|
|
624
|
+
const rangeMin = parseFloat(minSlider.min);
|
|
625
|
+
const rangeMax = parseFloat(minSlider.max);
|
|
626
|
+
const minVal = parseFloat(minSlider.value);
|
|
627
|
+
const maxVal = parseFloat(maxSlider.value);
|
|
628
|
+
|
|
629
|
+
// Calculate percentages
|
|
630
|
+
const range = rangeMax - rangeMin;
|
|
631
|
+
// Prevent division by zero if min === max
|
|
632
|
+
const minPercent = range === 0 ? 0 : ((minVal - rangeMin) / range) * 100;
|
|
633
|
+
const maxPercent = range === 0 ? 100 : ((maxVal - rangeMin) / range) * 100;
|
|
634
|
+
|
|
635
|
+
// Update CSS custom properties on the container
|
|
636
|
+
container.style.setProperty('--min-pos', `${minPercent}%`);
|
|
637
|
+
container.style.setProperty('--max-pos', `${maxPercent}%`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Update a control's value in the UI based on state changes.
|
|
642
|
+
* Exported because it's called from state.js
|
|
643
|
+
* @param {string} name - The name of the parameter.
|
|
644
|
+
* @param {*} value - The new value.
|
|
645
|
+
* @param {object} param - The parameter's info (potentially updated, includes type, min, max, options etc.).
|
|
646
|
+
*/
|
|
647
|
+
export function updateControlValue(name, value, param) {
|
|
648
|
+
// Check if param info is available
|
|
649
|
+
if (!param) {
|
|
650
|
+
console.warn(`No parameter info found for ${name} during UI update.`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Helper function to safely update element properties
|
|
655
|
+
const safeUpdate = (element, property, newValue) => {
|
|
656
|
+
if (element && element[property] !== newValue) {
|
|
657
|
+
element[property] = newValue;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// Helper function to update min/max/step for numeric inputs/sliders
|
|
662
|
+
const updateNumericProps = (element, param) => {
|
|
663
|
+
if (element) {
|
|
664
|
+
safeUpdate(element, 'min', param.min);
|
|
665
|
+
safeUpdate(element, 'max', param.max);
|
|
666
|
+
safeUpdate(element, 'step', param.step || (param.type === 'integer' ? 1 : (param.type === 'integer-range' ? 1 : 0.01)));
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
switch (param.type) {
|
|
671
|
+
case 'text':
|
|
672
|
+
case 'unbounded-integer':
|
|
673
|
+
case 'unbounded-float':
|
|
674
|
+
const inputElem = document.getElementById(`${name}-input`);
|
|
675
|
+
safeUpdate(inputElem, 'value', value);
|
|
676
|
+
// Update step for unbounded types if provided
|
|
677
|
+
if (param.type !== 'text') {
|
|
678
|
+
safeUpdate(inputElem, 'step', param.step || (param.type === 'unbounded-integer' ? 1 : 'any'));
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
case 'boolean':
|
|
682
|
+
const checkboxElem = document.getElementById(`${name}-checkbox`);
|
|
683
|
+
safeUpdate(checkboxElem, 'checked', value === true);
|
|
684
|
+
break;
|
|
685
|
+
case 'integer':
|
|
686
|
+
case 'float':
|
|
687
|
+
const sliderElem = document.getElementById(`${name}-slider`);
|
|
688
|
+
const numInputElem = document.getElementById(`${name}-input`);
|
|
689
|
+
updateNumericProps(sliderElem, param);
|
|
690
|
+
updateNumericProps(numInputElem, param);
|
|
691
|
+
safeUpdate(sliderElem, 'value', value);
|
|
692
|
+
safeUpdate(numInputElem, 'value', value);
|
|
693
|
+
break;
|
|
694
|
+
case 'selection':
|
|
695
|
+
const selectElem = document.getElementById(`${name}-select`);
|
|
696
|
+
if (selectElem) {
|
|
697
|
+
// Rebuild options if they might have changed (param object might be new)
|
|
698
|
+
if (param.options && Array.isArray(param.options)) {
|
|
699
|
+
let optionsChanged = selectElem.options.length !== param.options.length;
|
|
700
|
+
if (!optionsChanged) {
|
|
701
|
+
// Quick check if values differ
|
|
702
|
+
for(let i=0; i < param.options.length; i++) {
|
|
703
|
+
if (selectElem.options[i].value !== String(param.options[i])) {
|
|
704
|
+
optionsChanged = true;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (optionsChanged) {
|
|
711
|
+
selectElem.innerHTML = ''; // Clear existing
|
|
712
|
+
param.options.forEach(option => {
|
|
713
|
+
const optionElement = document.createElement('option');
|
|
714
|
+
optionElement.value = option;
|
|
715
|
+
optionElement.textContent = formatLabel(String(option));
|
|
716
|
+
optionElement.dataset.originalType = typeof option;
|
|
717
|
+
if (typeof option === 'number') {
|
|
718
|
+
optionElement.dataset.originalValue = option;
|
|
719
|
+
}
|
|
720
|
+
selectElem.appendChild(optionElement);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// Update the selected value
|
|
725
|
+
safeUpdate(selectElem, 'value', value);
|
|
726
|
+
} else {
|
|
727
|
+
console.warn(`Select element not found for ${name}`);
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
case 'multiple-selection':
|
|
731
|
+
const multiSelectElem = document.getElementById(`${name}-select`);
|
|
732
|
+
if (multiSelectElem) {
|
|
733
|
+
// Similar logic to rebuild options if necessary
|
|
734
|
+
if (param.options && Array.isArray(param.options)) {
|
|
735
|
+
let optionsChanged = multiSelectElem.options.length !== param.options.length;
|
|
736
|
+
if (!optionsChanged) {
|
|
737
|
+
for(let i=0; i < param.options.length; i++) {
|
|
738
|
+
if (multiSelectElem.options[i].value !== String(param.options[i])) {
|
|
739
|
+
optionsChanged = true;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (optionsChanged) {
|
|
746
|
+
multiSelectElem.innerHTML = ''; // Clear existing
|
|
747
|
+
param.options.forEach(option => {
|
|
748
|
+
const optionElement = document.createElement('option');
|
|
749
|
+
optionElement.value = option;
|
|
750
|
+
optionElement.textContent = formatLabel(String(option));
|
|
751
|
+
optionElement.dataset.originalType = typeof option;
|
|
752
|
+
if (typeof option === 'number') {
|
|
753
|
+
optionElement.dataset.originalValue = option;
|
|
754
|
+
}
|
|
755
|
+
multiSelectElem.appendChild(optionElement);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// Update selected status for all options
|
|
760
|
+
const valueArray = Array.isArray(value) ? value : [value]; // Ensure value is array
|
|
761
|
+
Array.from(multiSelectElem.options).forEach(option => {
|
|
762
|
+
// Check against original value if available, otherwise string value
|
|
763
|
+
const optionValue = option.dataset.originalValue ? parseFloat(option.dataset.originalValue) : option.value;
|
|
764
|
+
const shouldBeSelected = valueArray.some(val => val == optionValue); // Use loose equality for comparison after potential type conversion
|
|
765
|
+
safeUpdate(option, 'selected', shouldBeSelected);
|
|
766
|
+
});
|
|
767
|
+
} else {
|
|
768
|
+
console.warn(`Multi-select element not found for ${name}`);
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
case 'integer-range':
|
|
772
|
+
case 'float-range':
|
|
773
|
+
const minSliderElem = document.getElementById(`${name}-min-slider`);
|
|
774
|
+
const maxSliderElem = document.getElementById(`${name}-max-slider`);
|
|
775
|
+
const minInputElem = document.getElementById(`${name}-min-input`);
|
|
776
|
+
const maxInputElem = document.getElementById(`${name}-max-input`);
|
|
777
|
+
|
|
778
|
+
updateNumericProps(minSliderElem, param);
|
|
779
|
+
updateNumericProps(maxSliderElem, param);
|
|
780
|
+
updateNumericProps(minInputElem, param);
|
|
781
|
+
updateNumericProps(maxInputElem, param);
|
|
782
|
+
|
|
783
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
784
|
+
safeUpdate(minSliderElem, 'value', value[0]);
|
|
785
|
+
safeUpdate(maxSliderElem, 'value', value[1]);
|
|
786
|
+
safeUpdate(minInputElem, 'value', value[0]);
|
|
787
|
+
safeUpdate(maxInputElem, 'value', value[1]);
|
|
788
|
+
|
|
789
|
+
// Update the gradient background
|
|
790
|
+
const sliderContainer = minSliderElem ? minSliderElem.closest('.range-slider-container') : null;
|
|
791
|
+
if (sliderContainer && minSliderElem && maxSliderElem) {
|
|
792
|
+
updateSliderGradient(minSliderElem, maxSliderElem, sliderContainer);
|
|
793
|
+
} else {
|
|
794
|
+
console.warn(`Could not find elements for updating range slider gradient: ${name}`);
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
console.warn(`Invalid value format for range control ${name}:`, value);
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
case 'button':
|
|
801
|
+
// Buttons don't have a 'value' to update in the traditional sense.
|
|
802
|
+
// We might update the label if param.label changed, but that's less common.
|
|
803
|
+
const buttonElem = document.getElementById(`${name}-button`);
|
|
804
|
+
if (param.label) {
|
|
805
|
+
safeUpdate(buttonElem, 'textContent', param.label);
|
|
806
|
+
}
|
|
807
|
+
break;
|
|
808
|
+
default:
|
|
809
|
+
// Type was already checked earlier, but good to have a default
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|