fragment-tools 0.2.3 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-tools",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "A web development environment for creative coding",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -125,7 +125,6 @@ class MIDI extends Input {
125
125
  }
126
126
 
127
127
  async request() {
128
- console.log('MIDI :: request!');
129
128
  try {
130
129
  if (!this.requesting && !this.access) {
131
130
  localStorage.setItem(LOCAL_STORAGE_KEY, true);
@@ -6,17 +6,19 @@
6
6
 
7
7
  let { mID, headless = false, ...restProps } = $props();
8
8
 
9
- let input = $state(null);
10
- let output = $state(null);
11
- let inputs = $state([]);
12
- let outputs = $state([]);
9
+ let input = $state(undefined);
10
+ let output = $state(undefined);
11
+ let inputs = $state(MIDI.inputs);
12
+ let outputs = $state(MIDI.outputs);
13
+ let inputOptions = $derived.by(() => createDeviceOptions(inputs));
14
+ let outputOptions = $derived.by(() => createDeviceOptions(outputs));
13
15
  let messages = $state([]);
14
16
 
15
17
  function createDeviceOptions(deviceMap = new Map()) {
16
18
  let options = [];
17
19
 
18
20
  if (deviceMap.size !== 0) {
19
- options.push({ value: 'none', label: 'No device selected.' });
21
+ options.push({ value: undefined, label: 'No device selected.' });
20
22
  }
21
23
 
22
24
  for (let entry of deviceMap) {
@@ -30,7 +32,7 @@
30
32
  }
31
33
 
32
34
  if (options.length === 0) {
33
- options = [{ value: 'none', label: 'No device detected.' }];
35
+ options.push({ value: undefined, label: 'No device detected.' });
34
36
  }
35
37
 
36
38
  return options;
@@ -45,15 +47,23 @@
45
47
  await MIDI.request();
46
48
 
47
49
  function refresh() {
48
- inputs = createDeviceOptions(MIDI.inputs);
49
- outputs = createDeviceOptions(MIDI.outputs);
50
+ inputs = MIDI.inputs;
51
+ outputs = MIDI.outputs;
50
52
 
51
53
  // if a single device is connected, select it by default
52
- input = inputs.length === 2 ? inputs[1].value : inputs[0].value;
53
- output = outputs.length === 2 ? outputs[1].value : outputs[0].value;
54
+ input =
55
+ inputs.size === 1
56
+ ? MIDI.inputs.values().next().value.id
57
+ : undefined;
58
+ output =
59
+ outputs.size === 1
60
+ ? MIDI.outputs.values().next().value.id
61
+ : undefined;
54
62
  }
55
63
 
56
- MIDI.addEventListener('connected', refresh);
64
+ MIDI.addEventListener('connected', () => {
65
+ refresh();
66
+ });
57
67
  MIDI.addEventListener('disconnected', () => {
58
68
  refresh();
59
69
  });
@@ -83,7 +93,7 @@
83
93
  value={input}
84
94
  onchange={(value) => (input = value)}
85
95
  params={{
86
- options: inputs,
96
+ options: inputOptions,
87
97
  }}
88
98
  />
89
99
  <Field
@@ -91,7 +101,7 @@
91
101
  value={output}
92
102
  onchange={(value) => (output = value)}
93
103
  params={{
94
- options: outputs,
104
+ options: outputOptions,
95
105
  }}
96
106
  />
97
107
  <Field key="messages" value={messages} type="list" disabled />
@@ -7,6 +7,7 @@
7
7
  import { layout } from '../state/layout.svelte';
8
8
  import FieldGroup from '../ui/FieldGroup.svelte';
9
9
  import { sketchesManager } from '../state/sketches.svelte';
10
+ import { parseFolder } from '../utils/fields.utils';
10
11
 
11
12
  let {
12
13
  id = layout.getID(),
@@ -52,16 +53,25 @@
52
53
 
53
54
  if (!sketchPropGroup || group === sketchPropGroup) {
54
55
  if (folder) {
56
+ const parsed = parseFolder(folder);
57
+ const current = parsed.find((m) => m.isCurrent);
58
+
55
59
  const fieldgroup = sketch?.propsFolders.find(
56
- (f) => f.id === folder,
60
+ (f) => f.id === current.id,
57
61
  );
58
62
 
59
63
  if (fieldgroup) {
60
- const { depth, root } = fieldgroup;
64
+ const { depth, rootId } = fieldgroup;
65
+ const root =
66
+ rootId &&
67
+ sketch?.propsFolders.find((f) => f.id === rootId);
61
68
 
62
69
  if (depth === 0 && !tree.includes(fieldgroup)) {
63
70
  tree.push(fieldgroup);
64
- } else if (root && !tree.includes(root)) {
71
+ } else if (
72
+ root &&
73
+ !tree.some((fieldgroup) => fieldgroup.id === rootId)
74
+ ) {
65
75
  tree.push(root);
66
76
  }
67
77
  } else {
@@ -164,19 +174,21 @@
164
174
  sketchProps[item.key],
165
175
  )}
166
176
  {:else if item.type === 'fieldgroup'}
167
- <FieldGroup
168
- name={item.displayName}
169
- collapsed={item.collapsed}
170
- onchange={(collapsed) => {
171
- sketch.updateFolder(item, collapsed);
172
- }}
173
- >
174
- {#if item.children.length > 0}
175
- {#each item.children as child, childIndex}
176
- {@render sketchPropItem(childIndex, child)}
177
- {/each}
178
- {/if}
179
- </FieldGroup>
177
+ {#if !item.hidden}
178
+ <FieldGroup
179
+ name={item.displayName}
180
+ collapsed={item.collapsed}
181
+ onchange={(collapsed) => {
182
+ sketch.updateFolder(item, collapsed);
183
+ }}
184
+ >
185
+ {#if item.children.length > 0}
186
+ {#each item.children as child, childIndex}
187
+ {@render sketchPropItem(childIndex, child)}
188
+ {/each}
189
+ {/if}
190
+ </FieldGroup>
191
+ {/if}
180
192
  {/if}
181
193
  {/snippet}
182
194
  {#each sketchPropsTree as sketchPropsTreeItem, index}
@@ -1,6 +1,8 @@
1
+ import { parseFolder } from '../utils/fields.utils';
1
2
  import { rendering } from './rendering.svelte';
2
3
  import {
3
4
  deepAssign,
5
+ deepClone,
4
6
  deepEqual,
5
7
  hydrate,
6
8
  isFunction,
@@ -46,7 +48,10 @@ class Sketch {
46
48
 
47
49
  reset() {
48
50
  Object.keys(this.props).forEach((key) => {
49
- this.updateProp(key, this.props[key].__initialValue);
51
+ this.updateProp(
52
+ key,
53
+ $state.snapshot(this.props[key].__initialValue),
54
+ );
50
55
  });
51
56
 
52
57
  this.propsFolders.forEach((fieldgroup) => {
@@ -89,12 +94,16 @@ class Sketch {
89
94
  ) {
90
95
  deepAssign(newProp.value, prevProp.value);
91
96
  deepAssign(instanceProp.value, prevProp.value);
92
- newProp.__currentValue = newProp.value;
97
+ newProp.__currentValue = deepClone(
98
+ $state.snapshot(newProp.value),
99
+ );
93
100
  } else if (
94
101
  newProp.__initialValue === prevProp.__initialValue
95
102
  ) {
96
103
  newProp.value = prevProp.value;
97
- newProp.__currentValue = newProp.value;
104
+ newProp.__currentValue = deepClone(
105
+ $state.snapshot(newProp.value),
106
+ );
98
107
  instanceProp.value = prevProp.value;
99
108
  }
100
109
 
@@ -164,18 +173,6 @@ class Sketch {
164
173
  propsFoldersCollection,
165
174
  propsGroupsCollection,
166
175
  ) {
167
- const duplicateInitialValue = (value) => {
168
- if (isFunction(value)) {
169
- return value;
170
- }
171
-
172
- if (isObject(value)) {
173
- return structuredClone(value);
174
- }
175
-
176
- return value;
177
- };
178
-
179
176
  let {
180
177
  value,
181
178
  params = {},
@@ -203,7 +200,7 @@ class Sketch {
203
200
  ? instanceProp.hidden
204
201
  : () => instanceProp.hidden;
205
202
 
206
- let initialValue = duplicateInitialValue(value);
203
+ let initialValue = deepClone(value);
207
204
 
208
205
  if (group && !propsGroupsCollection.includes(group)) {
209
206
  propsGroupsCollection.push(group);
@@ -216,7 +213,7 @@ class Sketch {
216
213
  let prop = {
217
214
  value,
218
215
  __initialValue: initialValue,
219
- __currentValue: value,
216
+ __currentValue: deepClone(value),
220
217
  __hidden,
221
218
  type,
222
219
  params: structuredClone(params),
@@ -237,12 +234,16 @@ class Sketch {
237
234
 
238
235
  if (prop) {
239
236
  prop.value = newValue;
240
- prop.__currentValue = newValue;
237
+ prop.__currentValue = deepClone(newValue);
241
238
  }
242
239
 
243
240
  if (instanceProp) {
244
241
  if (!deepEqual(instanceProp.value, newValue)) {
245
- instanceProp.value = newValue;
242
+ if (isObject(instanceProp.value) && isObject(newValue)) {
243
+ deepAssign(instanceProp.value, newValue);
244
+ } else {
245
+ instanceProp.value = newValue;
246
+ }
246
247
  }
247
248
 
248
249
  instanceProp.onChange?.(instanceProp, {
@@ -264,21 +265,24 @@ class Sketch {
264
265
  if (!folder) return undefined;
265
266
 
266
267
  let propFolder;
267
- let names = folder.split('.');
268
268
 
269
- if (names.length > 0) {
270
- let root;
271
- let collapsedRegex = /^(.*?)(?:\[collapsed=(true|false)\])?$/;
269
+ const parsed = parseFolder(folder);
272
270
 
273
- for (let i = 0; i < names.length; i++) {
274
- let name = names[i];
275
- let match = name.match(collapsedRegex);
276
- let displayName = match[1];
277
- let collapsed = match[2] ? match[2] === 'true' : false;
271
+ if (parsed.length > 0) {
272
+ for (let i = 0; i < parsed.length; i++) {
273
+ let match = parsed[i];
274
+ let {
275
+ depth,
276
+ id,
277
+ parentId,
278
+ rootId,
279
+ isCurrent,
280
+ name,
281
+ attributes = {},
282
+ } = match;
283
+ let { collapsed = false } = attributes;
284
+ let displayName = name;
278
285
 
279
- let depth = i;
280
- let id = [...names].slice(0, i + 1).join('.');
281
- let parentId = [...names].slice(0, i).join('.');
282
286
  let parent =
283
287
  depth > 0
284
288
  ? collection.find((f) => f.id === parentId)
@@ -296,7 +300,8 @@ class Sketch {
296
300
  children: [],
297
301
  parent,
298
302
  depth,
299
- root,
303
+ rootId,
304
+ hidden: false,
300
305
  };
301
306
 
302
307
  if (parent) {
@@ -306,11 +311,7 @@ class Sketch {
306
311
  collection.push(fieldgroup);
307
312
  }
308
313
 
309
- if (i === 0) {
310
- root = fieldgroup;
311
- }
312
-
313
- if (i === names.length - 1) {
314
+ if (isCurrent) {
314
315
  fieldgroup.children.push({
315
316
  type: 'field',
316
317
  key,
@@ -326,7 +327,7 @@ class Sketch {
326
327
 
327
328
  updateFolder(folder, collapsed) {
328
329
  this.propsFolders.forEach((f, index) => {
329
- if (f === folder) {
330
+ if (f.id === folder.id) {
330
331
  this.propsFolders[index].collapsed = collapsed;
331
332
  }
332
333
  });
@@ -366,6 +367,9 @@ class Sketch {
366
367
  !deepEqual(instanceProp.value, prop.__currentValue)
367
368
  ) {
368
369
  this.updateProp(key, instanceProp.value);
370
+ prop.__initialValue = deepClone(
371
+ $state.snapshot(prop.value),
372
+ );
369
373
  }
370
374
 
371
375
  // sync displayName
@@ -462,6 +466,29 @@ class Sketch {
462
466
  delete this.props[key];
463
467
  }
464
468
  });
469
+
470
+ const fieldgroups = [...this.propsFolders].sort(
471
+ (a, b) => b.depth - a.depth,
472
+ );
473
+
474
+ fieldgroups.forEach((fieldgroup) => {
475
+ const hasAllFieldsHidden = fieldgroup.children
476
+ .filter((child) => child.type === 'field')
477
+ .every((child) => this.props[child.key].__hidden());
478
+ const hasAllFieldgroupsHidden = fieldgroup.children
479
+ .filter((child) => child.type === 'fieldgroup')
480
+ .every((child) => child.hidden);
481
+
482
+ if (hasAllFieldsHidden && hasAllFieldgroupsHidden) {
483
+ if (!fieldgroup.hidden) {
484
+ fieldgroup.hidden = true;
485
+ }
486
+ } else {
487
+ if (fieldgroup.hidden) {
488
+ fieldgroup.hidden = false;
489
+ }
490
+ }
491
+ });
465
492
  }
466
493
 
467
494
  onBeforeCapture(fn) {
@@ -320,12 +320,27 @@ export class Render {
320
320
  this.time = 0;
321
321
  this.recording = false;
322
322
 
323
- $effect(() => {
323
+ let resizeTimeout;
324
+
325
+ $effect.pre(() => {
324
326
  const { width, height, pixelRatio } = rendering;
325
327
 
326
328
  if (this.loaded) {
327
- this.resize(width, height, pixelRatio);
328
- } else {
329
+ if (resizeTimeout) clearTimeout(resizeTimeout);
330
+
331
+ resizeTimeout = setTimeout(() => {
332
+ clearTimeout(resizeTimeout);
333
+ resizeTimeout = null;
334
+
335
+ this.resize(width, height, pixelRatio);
336
+ }, 0);
337
+ }
338
+ });
339
+
340
+ $effect(() => {
341
+ const { width, height, pixelRatio } = rendering;
342
+
343
+ if (!this.loaded) {
329
344
  this.width = width;
330
345
  this.height = height;
331
346
  this.pixelRatio = pixelRatio;
@@ -74,3 +74,16 @@ export function deepEqual(target, source) {
74
74
 
75
75
  return target === source;
76
76
  }
77
+
78
+ export function deepClone(value) {
79
+ if (isFunction(value)) {
80
+ return value;
81
+ }
82
+
83
+ if (isObject(value)) {
84
+ const clone = structuredClone(value);
85
+ return clone;
86
+ }
87
+
88
+ return value;
89
+ }
@@ -9,7 +9,7 @@
9
9
  import ButtonInput from './fields/ButtonInput.svelte';
10
10
  import ImageInput from './fields/ImageInput.svelte';
11
11
  import IntervalInput from './fields/IntervalInput.svelte';
12
- import { fieldTypes, hasChanged } from '../utils/fields.utils.js';
12
+ import { fieldTypes } from '../utils/fields.utils.js';
13
13
 
14
14
  const fields = {
15
15
  [`${fieldTypes.SELECT}`]: Select,
@@ -37,6 +37,7 @@
37
37
  import IconTriggers from '../components/IconTriggers.svelte';
38
38
  import IconLocked from '../components/IconLocked.svelte';
39
39
  import ImportInput from './fields/ImportInput.svelte';
40
+ import { deepEqual } from '../state/utils.svelte';
40
41
 
41
42
  let {
42
43
  key,
@@ -91,7 +92,7 @@
91
92
  let fieldType = $derived(inferFieldType({ type, value, params, key }));
92
93
  let fieldProps = $derived(composeFieldProps(params, disabled));
93
94
  let onTrigger = $derived(frameDebounce(onTriggers[fieldType]));
94
- let input = $derived(fields[fieldType]);
95
+ let Component = $derived(fields[fieldType]);
95
96
  let triggerable = $derived(
96
97
  params.triggerable !== false &&
97
98
  ((fieldType === fieldTypes.NUMBER &&
@@ -117,12 +118,17 @@
117
118
  context,
118
119
  };
119
120
  }
121
+
122
+ function hasChanged(current, next) {
123
+ const changed = !deepEqual(current, next);
124
+ return changed;
125
+ }
120
126
  </script>
121
127
 
122
128
  <div
123
129
  class="field"
124
130
  class:disabled
125
- class:changed={!disabled && hasChanged(initialValue, value)}
131
+ class:changed={!disabled && hasChanged(value, initialValue)}
126
132
  style="--index: {index};"
127
133
  >
128
134
  <FieldSection
@@ -153,13 +159,7 @@
153
159
  {/if}
154
160
  </div>
155
161
  {/snippet}
156
- <svelte:component
157
- this={input}
158
- {value}
159
- {...fieldProps}
160
- {onchange}
161
- onclick={onTrigger}
162
- />
162
+ <Component {value} {...fieldProps} {onchange} onclick={onTrigger} />
163
163
  {@render children?.()}
164
164
  </FieldSection>
165
165
  {#if triggerable}
@@ -1,22 +1,14 @@
1
1
  <script>
2
- import { createEventDispatcher } from 'svelte';
2
+ import Keyboard from '../../inputs/Keyboard.js';
3
3
  import { map, clamp, roundToStep } from '../../utils/math.utils.js';
4
4
 
5
- let {
6
- value,
7
- min,
8
- max,
9
- step,
10
- context = null,
11
- key = '',
12
- disabled = false,
13
- onchange,
14
- } = $props();
5
+ let { value, min, max, step, disabled = false, onchange } = $props();
15
6
 
16
7
  let node;
17
8
  let rect;
18
9
 
19
10
  let isDragging = $state(false);
11
+ let steppedValue = $derived(roundToStep(value, step));
20
12
 
21
13
  // handlers
22
14
  function handleMouseDown(event) {
@@ -48,6 +40,28 @@
48
40
  }
49
41
  }
50
42
 
43
+ function handleKeyDown(event) {
44
+ const direction = ['ArrowUp', 'ArrowRight'].includes(event.key)
45
+ ? 1
46
+ : ['ArrowDown', 'ArrowLeft'].includes(event.key)
47
+ ? -1
48
+ : 0;
49
+
50
+ const diff = Keyboard.getStepFromEvent(event) * step;
51
+
52
+ if (direction !== 0) {
53
+ const newValue = clamp(
54
+ roundToStep(value + direction * diff, step),
55
+ min,
56
+ max,
57
+ );
58
+
59
+ if (newValue !== value) {
60
+ onchange(newValue);
61
+ }
62
+ }
63
+ }
64
+
51
65
  function handleMouseUp() {
52
66
  document.body.classList.remove('fragment-dragging');
53
67
  document.removeEventListener('mousemove', handleMouseMove);
@@ -56,7 +70,9 @@
56
70
  isDragging = false;
57
71
  }
58
72
 
59
- let progress = $derived(clamp(map(value, min, max, 0, 1), 0.0001, 1));
73
+ let progress = $derived(
74
+ clamp(map(steppedValue, min, max, 0, 1), 0.0001, 1),
75
+ );
60
76
  let opacity = $derived(progress > 0 ? 1 : 0);
61
77
  </script>
62
78
 
@@ -64,10 +80,19 @@
64
80
  class="progress"
65
81
  bind:this={node}
66
82
  onmousedown={handleMouseDown}
83
+ onkeydown={handleKeyDown}
67
84
  class:disabled
68
85
  class:dragging={isDragging}
86
+ role="slider"
87
+ aria-valuemin={min}
88
+ aria-valuemax={max}
89
+ aria-valuenow={value}
90
+ tabindex="0"
69
91
  >
70
- <div class="fill" style="--progress: {progress}; --opacity: {opacity};" />
92
+ <div
93
+ class="fill"
94
+ style="--progress: {progress}; --opacity: {opacity};"
95
+ ></div>
71
96
  </div>
72
97
 
73
98
  <style>
@@ -81,13 +106,15 @@
81
106
  background: var(--color-background-input);
82
107
  cursor: ew-resize;
83
108
  container-type: size;
109
+ outline: 0;
84
110
  }
85
111
 
86
112
  :global(body:not(.fragment-dragging)) .progress:hover {
87
113
  box-shadow: inset 0 0 0 1px var(--color-active);
88
114
  }
89
115
 
90
- .progress.dragging {
116
+ .progress.dragging,
117
+ :global(body:not(.fragment-dragging)) .progress:focus-visible {
91
118
  box-shadow: 0 0 0 2px var(--color-active);
92
119
  }
93
120
 
@@ -8,27 +8,71 @@
8
8
  min = -Infinity,
9
9
  max = Infinity,
10
10
  step = 0.1,
11
+ key,
11
12
  locked = false,
12
13
  disabled = false,
13
14
  context = null,
14
- key = '',
15
15
  onchange,
16
16
  } = $props();
17
17
 
18
+ const keysChecks = ['x', 'y', 'z', 'w'];
18
19
 
19
20
  let isArray = $derived(Array.isArray(value));
20
21
  let isObject = $derived(!isArray && typeof value === 'object');
21
- let components = $derived(isObject ? Object.values(value) : [...value]);
22
- let keys = $derived(isObject ? Object.keys(value) : value.map((v, i) => i));
22
+ let keys = $derived.by(() => {
23
+ let keys = [];
24
+
25
+ if (isArray) {
26
+ return value.map((_, index) => index);
27
+ }
28
+
29
+ if (!isArray && !isObject) return [0];
30
+
31
+ for (let i = 0; i < keysChecks; i++) {
32
+ let keyCheck = keysChecks[i];
33
+
34
+ if (keyCheck in value) {
35
+ keys.push(keyCheck);
36
+ }
37
+ }
38
+
39
+ if (value.isVector2) {
40
+ return ['x', 'y'];
41
+ }
42
+
43
+ if (value.isVector3) {
44
+ return ['x', 'y', 'z'];
45
+ }
46
+
47
+ if (value.isVector4 || value.isQuaternion) {
48
+ return ['x', 'y', 'z', 'w'];
49
+ }
50
+
51
+ if (isObject) {
52
+ return Object.keys(value);
53
+ }
54
+
55
+ return keys;
56
+ });
57
+ let components = $derived.by(() => {
58
+ if (!isObject && !isArray) {
59
+ return [value];
60
+ }
61
+
62
+ return keys.map((key) => value[key]);
63
+ });
64
+ let mins = $derived(keys.map((key) => min[key]));
65
+ let maxs = $derived(keys.map((key) => max[key]));
66
+ let steps = $derived(keys.map((key) => step[key]));
23
67
 
24
68
  function dispatchChange() {
25
- let newValue = keys.reduce((all, key, index) => {
26
- all[key] = components[index];
69
+ let clone = isArray ? [] : {};
27
70
 
28
- return all;
29
- }, isArray ? [] : {});
71
+ keys.forEach((key, index) => {
72
+ clone[key] = components[index];
73
+ });
30
74
 
31
- onchange(newValue);
75
+ onchange(clone);
32
76
  }
33
77
 
34
78
  function handleComponentChange(newValue, componentIndex) {
@@ -39,11 +83,13 @@
39
83
  }
40
84
 
41
85
  components.forEach((component, index) => {
42
- components[index] = index === componentIndex
43
- ? newValue
44
- : locked
45
- ? Math.round(component * ratio * (1 / step)) / (1 / step)
46
- : component;
86
+ components[index] =
87
+ index === componentIndex
88
+ ? newValue
89
+ : locked
90
+ ? Math.round(component * ratio * (1 / step)) /
91
+ (1 / step)
92
+ : component;
47
93
  });
48
94
 
49
95
  dispatchChange();
@@ -56,17 +102,16 @@
56
102
  >
57
103
  {#each components as component, index}
58
104
  <NumberInput
59
- {min}
60
- {max}
61
- {step}
105
+ min={mins[index]}
106
+ max={maxs[index]}
107
+ step={steps[index]}
62
108
  {suffix}
63
109
  {disabled}
64
110
  {context}
65
111
  {key}
66
112
  label={keys[index]}
67
113
  value={component}
68
- onchange={(value) =>
69
- handleComponentChange(value, index)}
114
+ onchange={(value) => handleComponentChange(value, index)}
70
115
  />
71
116
  {/each}
72
117
  </FieldInputRow>
@@ -43,11 +43,40 @@ export function inferFieldType({ type, value, params, key }) {
43
43
 
44
44
  const isArray = Array.isArray(value);
45
45
  const isObject = !isArray && typeof value === 'object';
46
- const values = isObject ? Object.values(value) : value;
46
+ const getKeys = (value) => {
47
+ if (isArray) {
48
+ return value.map((_, index) => index);
49
+ }
50
+
51
+ if (!isArray && !isObject) return [0];
52
+
53
+ if (value.isVector3) {
54
+ return ['x', 'y', 'z'];
55
+ }
56
+
57
+ if (value.isVector4 || value.isQuaternion) {
58
+ return ['x', 'y', 'z', 'w'];
59
+ }
60
+
61
+ if (isObject) {
62
+ return Object.keys(value);
63
+ }
64
+ };
65
+
66
+ const getValues = (value, keys) => {
67
+ if (!isObject && !isArray) {
68
+ value = [value];
69
+ }
70
+
71
+ return keys.map((key) => value[key]);
72
+ };
73
+
74
+ const keys = getKeys(value);
75
+ const values = getValues(value, keys);
47
76
 
48
77
  if (
49
78
  isArray &&
50
- value.length === 2 &&
79
+ values.length === 2 &&
51
80
  typeof params.min === 'number' &&
52
81
  typeof params.max === 'number'
53
82
  ) {
@@ -80,53 +109,45 @@ export function inferFieldType({ type, value, params, key }) {
80
109
  console.warn(`Field: cannot find field type for ${key}`);
81
110
  }
82
111
 
83
- export function hasChanged(initialValue, currentValue) {
84
- const initialType = initialValue && typeof initialValue;
85
- const currentType = currentValue && typeof currentValue;
86
-
87
- if (initialType !== currentType) return true;
88
-
89
- if (Array.isArray(currentValue)) {
90
- if (initialValue.length !== currentValue.length) {
91
- return true;
112
+ /**
113
+ *
114
+ * @param {string} folder
115
+ */
116
+ export function parseFolder(folder) {
117
+ const regex = /(?<name>\w+)(?:\[(?<attributes>[^\]]+)\])?/g;
118
+ const matches = [...folder.matchAll(regex)];
119
+
120
+ const results = matches.map((match) => {
121
+ return {
122
+ name: match.groups.name,
123
+ attributes: match.groups.attributes
124
+ ? Object.fromEntries(
125
+ match.groups.attributes
126
+ .split(', ')
127
+ .map((attr) => attr.split('=')),
128
+ )
129
+ : {},
130
+ };
131
+ });
132
+
133
+ let names = results.map((match) => match.name);
134
+
135
+ let rootId;
136
+
137
+ results.forEach((match, index) => {
138
+ let id = [...names].slice(0, index + 1).join('.');
139
+ let parentId = [...names].slice(0, index).join('.');
140
+
141
+ if (index === 0) {
142
+ rootId = id;
92
143
  }
93
144
 
94
- for (let i = 0; i < currentValue.length; i++) {
95
- if (currentValue[i] !== initialValue[i]) {
96
- return true;
97
- }
98
- }
99
- return false;
100
- }
101
-
102
- if (initialType === 'object') {
103
- const keys1 = Object.keys(initialValue);
104
- const keys2 = Object.keys(currentValue);
105
-
106
- if (
107
- keys1.length !== keys2.length ||
108
- !keys1.every((key) => keys2.includes(key))
109
- ) {
110
- return true;
111
- }
112
-
113
- for (const key of keys1) {
114
- const value1 = initialValue[key];
115
- const value2 = currentValue[key];
116
-
117
- if (typeof value1 === 'object' && typeof value2 === 'object') {
118
- // If both values are objects, recursively compare them
119
- if (hasChanged(value1, value2)) {
120
- return true;
121
- }
122
- } else if (value1 !== value2) {
123
- // If values are not objects, directly compare them
124
- return true;
125
- }
126
-
127
- return false;
128
- }
129
- }
145
+ match.id = id;
146
+ match.parentId = parentId;
147
+ match.depth = index;
148
+ match.isCurrent = index === results.length - 1;
149
+ match.rootId = rootId;
150
+ });
130
151
 
131
- return initialValue !== currentValue;
152
+ return results;
132
153
  }
@@ -22,6 +22,12 @@ export function clamp(value, min, max) {
22
22
  return Math.max(min, Math.min(value, max));
23
23
  }
24
24
 
25
+ /**
26
+ *
27
+ * @param {number} value
28
+ * @param {number} step
29
+ * @returns {number}
30
+ */
25
31
  export function roundToStep(value, step) {
26
32
  return Math.round(value * (1 / step)) / (1 / step);
27
33
  }