fragment-tools 0.2.2 → 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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/client/app/components/Build.svelte +64 -0
  3. package/src/client/app/{ui → components}/Preview.svelte +1 -2
  4. package/src/client/app/inputs/MIDI.js +0 -1
  5. package/src/client/app/modules/MidiPanel.svelte +23 -13
  6. package/src/client/app/modules/Params.svelte +31 -16
  7. package/src/client/app/state/Sketch.svelte.js +77 -73
  8. package/src/client/app/state/layout.svelte.js +8 -2
  9. package/src/client/app/state/rendering.svelte.js +23 -5
  10. package/src/client/app/state/utils.svelte.js +28 -4
  11. package/src/client/app/ui/Field.svelte +13 -11
  12. package/src/client/app/ui/FieldGroup.svelte +5 -2
  13. package/src/client/app/ui/FieldSection.svelte +4 -4
  14. package/src/client/app/ui/Layout.svelte +1 -1
  15. package/src/client/app/ui/LayoutBuild.svelte +54 -0
  16. package/src/client/app/ui/LayoutColumn.svelte +2 -2
  17. package/src/client/app/ui/LayoutComponent.svelte +14 -4
  18. package/src/client/app/ui/LayoutResizer.svelte +11 -2
  19. package/src/client/app/ui/LayoutRow.svelte +2 -2
  20. package/src/client/app/ui/fields/ButtonInput.svelte +3 -3
  21. package/src/client/app/ui/fields/ColorInput.svelte +23 -13
  22. package/src/client/app/ui/fields/ImportInput.svelte +52 -0
  23. package/src/client/app/ui/fields/Input.svelte +4 -2
  24. package/src/client/app/ui/fields/IntervalInput.svelte +8 -6
  25. package/src/client/app/ui/fields/ProgressInput.svelte +47 -17
  26. package/src/client/app/ui/fields/Select.svelte +35 -41
  27. package/src/client/app/ui/fields/VectorInput.svelte +63 -18
  28. package/src/client/app/utils/fields.utils.js +70 -48
  29. package/src/client/app/utils/math.utils.js +6 -0
  30. package/src/client/public/css/global.css +14 -0
  31. package/src/client/app/ui/Build.svelte +0 -91
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-tools",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A web development environment for creative coding",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,64 @@
1
+ <script>
2
+ import { onDestroy } from 'svelte';
3
+ import { sketchesManager } from '../state/sketches.svelte';
4
+ import { rendering } from '../state/rendering.svelte';
5
+ import { layout } from '../state/layout.svelte';
6
+ import LayoutBuild from '../ui/LayoutBuild.svelte';
7
+
8
+ console.log(`Made with Fragment. https://fragment.tools`);
9
+
10
+ let sketchKey = $derived(sketchesManager.keys[0]);
11
+ let sketch = $derived(sketchesManager.sketches[sketchKey]);
12
+ let buildConfig = $derived(sketch?.buildConfig ?? {});
13
+ let persistent = $derived(buildConfig.layout?.persistent ?? false);
14
+ let styles = $derived(buildConfig?.styles ?? '');
15
+
16
+ /** @type {HTMLHeadElement} */
17
+ let head;
18
+ /** @type {HTMLStyleElement} */
19
+ let style;
20
+
21
+ $effect(() => {
22
+ if (__BUILD__) {
23
+ layout.persistent = persistent;
24
+ } else if (persistent && layout.previewing) {
25
+ console.warn(`Layout is not preserved while previewing`);
26
+ }
27
+ });
28
+
29
+ $effect(() => {
30
+ rendering.override(sketch?.buildConfig);
31
+ });
32
+
33
+ $effect(() => {
34
+ if (styles !== '') {
35
+ head = document.getElementsByTagName('head')[0];
36
+
37
+ if (style) {
38
+ head.removeChild(style);
39
+ }
40
+
41
+ style = document.createElement('style');
42
+ style.setAttribute('type', 'text/css');
43
+ style.appendChild(document.createTextNode(styles));
44
+ head.appendChild(style);
45
+ }
46
+ });
47
+
48
+ onDestroy(() => {
49
+ if (style && head) {
50
+ head.removeChild(style);
51
+ }
52
+ });
53
+ </script>
54
+
55
+ {#if sketch}
56
+ {#if buildConfig.layout?.component}
57
+ {#await buildConfig.layout.component() then layoutModule}
58
+ {@const LayoutBuildCustom = layoutModule.default}
59
+ <LayoutBuildCustom {sketchKey} {buildConfig} {sketch} />
60
+ {/await}
61
+ {:else}
62
+ <LayoutBuild {sketchKey} {sketch} {buildConfig} />
63
+ {/if}
64
+ {/if}
@@ -1,7 +1,6 @@
1
1
  <script>
2
- import { onDestroy, onMount } from 'svelte';
3
-
4
2
  import { rendering } from '../state/rendering.svelte';
3
+ import { onDestroy } from 'svelte';
5
4
  import Build from './Build.svelte';
6
5
 
7
6
  const { resizing, width, height, pixelRatio, aspectRatio, scale, preset } =
@@ -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 {
@@ -148,6 +158,9 @@
148
158
  // value(event, sketch.params);
149
159
  }}
150
160
  onchange={(v) => {
161
+ if (v?.currentTarget && v?.type === 'change') {
162
+ v = v.currentTarget.value;
163
+ }
151
164
  sketch.updateProp(key, v);
152
165
  }}
153
166
  />
@@ -161,19 +174,21 @@
161
174
  sketchProps[item.key],
162
175
  )}
163
176
  {:else if item.type === 'fieldgroup'}
164
- <FieldGroup
165
- name={item.displayName}
166
- collapsed={item.collapsed}
167
- onchange={(collapsed) => {
168
- sketch.updateFolder(item, collapsed);
169
- }}
170
- >
171
- {#if item.children.length > 0}
172
- {#each item.children as child, childIndex}
173
- {@render sketchPropItem(childIndex, child)}
174
- {/each}
175
- {/if}
176
- </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}
177
192
  {/if}
178
193
  {/snippet}
179
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,36 +173,25 @@ 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 = {},
182
179
  triggers = [],
183
180
  group,
181
+ type,
184
182
  folder,
185
183
  displayName,
186
184
  } = instanceProp;
187
185
 
188
- if (value.isColor) {
186
+ if (value?.isColor) {
189
187
  value = { r: value.r, g: value.g, b: value.b };
190
- } else if (value.isVector2) {
188
+ } else if (value?.isVector2) {
191
189
  value = { x: value.x, y: value.y };
192
- } else if (value.isVector3) {
190
+ } else if (value?.isVector3) {
193
191
  value = { x: value.x, y: value.y, z: value.z };
194
- } else if (value.isVector4) {
192
+ } else if (value?.isVector4) {
195
193
  value = { x: value.x, y: value.y, z: value.z, w: value.w };
196
- } else if (value.isQuaternion) {
194
+ } else if (value?.isQuaternion) {
197
195
  value = { x: value.x, y: value.y, z: value.z, w: value.w };
198
196
  }
199
197
 
@@ -202,7 +200,7 @@ class Sketch {
202
200
  ? instanceProp.hidden
203
201
  : () => instanceProp.hidden;
204
202
 
205
- let initialValue = duplicateInitialValue(value);
203
+ let initialValue = deepClone(value);
206
204
 
207
205
  if (group && !propsGroupsCollection.includes(group)) {
208
206
  propsGroupsCollection.push(group);
@@ -215,8 +213,9 @@ class Sketch {
215
213
  let prop = {
216
214
  value,
217
215
  __initialValue: initialValue,
218
- __currentValue: value,
216
+ __currentValue: deepClone(value),
219
217
  __hidden,
218
+ type,
220
219
  params: structuredClone(params),
221
220
  triggers,
222
221
  group,
@@ -235,14 +234,16 @@ class Sketch {
235
234
 
236
235
  if (prop) {
237
236
  prop.value = newValue;
238
- prop.__currentValue = newValue;
237
+ prop.__currentValue = deepClone(newValue);
239
238
  }
240
239
 
241
240
  if (instanceProp) {
242
- if (isObject(instanceProp.value)) {
243
- deepAssign(instanceProp.value, newValue);
244
- } else {
245
- instanceProp.value = newValue;
241
+ if (!deepEqual(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
  });
@@ -365,7 +366,10 @@ class Sketch {
365
366
  !isFunction(instanceProp.value) &&
366
367
  !deepEqual(instanceProp.value, prop.__currentValue)
367
368
  ) {
368
- this.updateProp(key, structuredClone(instanceProp.value));
369
+ this.updateProp(key, instanceProp.value);
370
+ prop.__initialValue = deepClone(
371
+ $state.snapshot(prop.value),
372
+ );
369
373
  }
370
374
 
371
375
  // sync displayName
@@ -440,34 +444,11 @@ class Sketch {
440
444
  for (const paramKey in instanceProp.params) {
441
445
  const instanceParam = instanceProp.params[paramKey];
442
446
  const param = prop.params[paramKey];
443
- let needsUpdate = false;
444
-
445
- if (isObject(instanceParam)) {
446
- Object.keys(instanceParam).forEach((key) => {
447
- if (isObject(instanceParam[key])) {
448
- Object.keys(instanceParam[key]).forEach(
449
- (k) => {
450
- if (
451
- instanceParam[key][k] !==
452
- param[key][k]
453
- ) {
454
- needsUpdate = true;
455
- }
456
- },
457
- );
458
- } else if (instanceParam[key] !== param[key]) {
459
- needsUpdate = true;
460
- }
461
- });
462
- } else if (instanceParam !== param) {
463
- needsUpdate = true;
464
- }
447
+ let needsUpdate = !deepEqual(instanceParam, param);
465
448
 
466
449
  if (needsUpdate) {
467
- if (needsUpdate) {
468
- prop.params[paramKey] =
469
- structuredClone(instanceParam);
470
- }
450
+ prop.params[paramKey] =
451
+ structuredClone(instanceParam);
471
452
  }
472
453
  }
473
454
  }
@@ -485,6 +466,29 @@ class Sketch {
485
466
  delete this.props[key];
486
467
  }
487
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
+ });
488
492
  }
489
493
 
490
494
  onBeforeCapture(fn) {
@@ -6,17 +6,22 @@ class Layout {
6
6
  components = $state([]);
7
7
  editing = $state(false);
8
8
  previewing = $state(false);
9
+ persistent = $state(true);
9
10
 
10
11
  getID() {
11
12
  return COMPONENT_ID++;
12
13
  }
13
14
 
14
15
  constructor() {
15
- this.key = 'layout';
16
+ this.key = __BUILD__ ? `layout${__START_TIME__}` : `layout`;
16
17
 
17
18
  $effect.root(() => {
18
19
  $effect(() => {
19
- if (!this.previewing && !__BUILD__) {
20
+ const isPersistent = __BUILD__
21
+ ? this.persistent
22
+ : !this.previewing;
23
+
24
+ if (isPersistent) {
20
25
  this.persist($state.snapshot(this.components));
21
26
  }
22
27
  });
@@ -156,6 +161,7 @@ class Layout {
156
161
  type: source.type,
157
162
  name: source.name,
158
163
  minimized: source.minimized,
164
+ headless: source.headless,
159
165
  params: source.params,
160
166
  children: [...source.children],
161
167
  }));
@@ -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;
@@ -361,7 +376,7 @@ export class Render {
361
376
 
362
377
  if (needsUpdate) {
363
378
  console.warn(
364
- `Canvas ${attributeName} was changed from sketch to ${dimension}px to previous ${rendering[attributeName]}`,
379
+ `Canvas ${attributeName} was changed from sketch from ${rendering[attributeName]}px to ${dimension}px.`,
365
380
  );
366
381
  rendering[attributeName] = dimension;
367
382
 
@@ -460,6 +475,8 @@ export class Render {
460
475
  };
461
476
 
462
477
  this.init = async () => {
478
+ if (this.errored) return;
479
+
463
480
  clearError(this.sketch.key);
464
481
  this.mountParams = this.renderer?.onMountPreview?.(this.params);
465
482
  if (this.mountParams && this.mountParams.canvas !== this.canvas) {
@@ -599,7 +616,8 @@ export class Render {
599
616
  sketch.beforeRecord.forEach((fn) => fn(params));
600
617
  },
601
618
  onTick: ({ time, deltaTime }) => {
602
- this.loop(time);
619
+ this.time += deltaTime;
620
+ this.loop(this.time);
603
621
  },
604
622
  onComplete: (params) => {
605
623
  sketch.afterRecord.forEach((fn) => fn(params));
@@ -50,11 +50,22 @@ export function deepAssign(target, source) {
50
50
  }
51
51
 
52
52
  export function deepEqual(target, source) {
53
- if (isObject(target) && isObject(target)) {
53
+ if (isObject(target) && isObject(source)) {
54
54
  let isEqual = true;
55
- for (const key in source) {
56
- if (isEqual) {
57
- isEqual = deepEqual(target[key], source[key]);
55
+
56
+ if (
57
+ Array.isArray(target) &&
58
+ Array.isArray(source) &&
59
+ target.length !== source.length
60
+ ) {
61
+ isEqual = false;
62
+ }
63
+
64
+ if (isEqual) {
65
+ for (const key in source) {
66
+ if (isEqual) {
67
+ isEqual = deepEqual(target[key], source[key]);
68
+ }
58
69
  }
59
70
  }
60
71
 
@@ -63,3 +74,16 @@ export function deepEqual(target, source) {
63
74
 
64
75
  return target === source;
65
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
+ }