@playcanvas/web-components 0.1.9 → 0.1.11

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/src/app.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Application, FILLMODE_FILL_WINDOW, Keyboard, Mouse, RESOLUTION_AUTO } from 'playcanvas';
1
+ import { Application, CameraComponent, FILLMODE_FILL_WINDOW, Keyboard, Mouse, Picker, RESOLUTION_AUTO } from 'playcanvas';
2
2
 
3
3
  import { AssetElement } from './asset';
4
4
  import { AsyncElement } from './async-element';
@@ -27,6 +27,24 @@ class AppElement extends AsyncElement {
27
27
 
28
28
  private _hierarchyReady = false;
29
29
 
30
+ private _picker: Picker | null = null;
31
+
32
+ private _hasPointerListeners: { [key: string]: boolean } = {
33
+ pointerenter: false,
34
+ pointerleave: false,
35
+ pointerdown: false,
36
+ pointerup: false,
37
+ pointermove: false
38
+ };
39
+
40
+ private _hoveredEntity: EntityElement | null = null;
41
+
42
+ private _pointerHandlers: { [key: string]: EventListener | null } = {
43
+ pointermove: null,
44
+ pointerdown: null,
45
+ pointerup: null
46
+ };
47
+
30
48
  /**
31
49
  * The PlayCanvas application instance.
32
50
  */
@@ -71,6 +89,8 @@ class AppElement extends AsyncElement {
71
89
  this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
72
90
  this.app.setCanvasResolution(RESOLUTION_AUTO);
73
91
 
92
+ this._pickerCreate();
93
+
74
94
  // Get all pc-asset elements that are direct children of the pc-app element
75
95
  const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
76
96
  Array.from(assetElements).forEach((assetElement) => {
@@ -113,6 +133,8 @@ class AppElement extends AsyncElement {
113
133
  }
114
134
 
115
135
  disconnectedCallback() {
136
+ this._pickerDestroy();
137
+
116
138
  // Clean up the application
117
139
  if (this.app) {
118
140
  this.app.destroy();
@@ -135,6 +157,163 @@ class AppElement extends AsyncElement {
135
157
  }
136
158
  }
137
159
 
160
+ _pickerCreate() {
161
+ const { width, height } = this.app!.graphicsDevice;
162
+ this._picker = new Picker(this.app!, width, height);
163
+
164
+ // Create bound handlers but don't attach them yet
165
+ this._pointerHandlers.pointermove = this._onPointerMove.bind(this) as EventListener;
166
+ this._pointerHandlers.pointerdown = this._onPointerDown.bind(this) as EventListener;
167
+ this._pointerHandlers.pointerup = this._onPointerUp.bind(this) as EventListener;
168
+
169
+ // Listen for pointer listeners being added/removed
170
+ ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
171
+ this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
172
+ this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
173
+ });
174
+ }
175
+
176
+ _pickerDestroy() {
177
+ if (this._canvas) {
178
+ Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
179
+ if (handler) {
180
+ this._canvas!.removeEventListener(type, handler);
181
+ }
182
+ });
183
+ }
184
+
185
+ this._picker = null;
186
+ this._pointerHandlers = {
187
+ pointermove: null,
188
+ pointerdown: null,
189
+ pointerup: null
190
+ };
191
+ }
192
+
193
+ _onPointerMove(event: PointerEvent) {
194
+ if (!this._picker || !this.app) return;
195
+
196
+ const camera = this.app!.root.findComponent('camera') as CameraComponent;
197
+ if (!camera) return;
198
+
199
+ const canvasRect = this._canvas!.getBoundingClientRect();
200
+ const x = event.clientX - canvasRect.left;
201
+ const y = event.clientY - canvasRect.top;
202
+
203
+ this._picker.prepare(camera, this.app!.scene);
204
+ const selection = this._picker.getSelection(x, y);
205
+
206
+ // Get the currently hovered entity by walking up the hierarchy
207
+ let newHoverEntity = null;
208
+ if (selection.length > 0) {
209
+ let node = selection[0].node;
210
+ while (node && !newHoverEntity) {
211
+ const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
212
+ if (entityElement) {
213
+ newHoverEntity = entityElement;
214
+ }
215
+ node = node.parent;
216
+ }
217
+ }
218
+
219
+ // Handle enter/leave events
220
+ if (this._hoveredEntity !== newHoverEntity) {
221
+ if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
222
+ this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
223
+ }
224
+ if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
225
+ newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
226
+ }
227
+ }
228
+
229
+ // Update hover state
230
+ this._hoveredEntity = newHoverEntity;
231
+
232
+ // Handle pointermove event
233
+ if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
234
+ newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
235
+ }
236
+ }
237
+
238
+ _onPointerDown(event: PointerEvent) {
239
+ if (!this._picker || !this.app) return;
240
+
241
+ const camera = this.app!.root.findComponent('camera') as CameraComponent;
242
+ if (!camera) return;
243
+
244
+ const canvasRect = this._canvas!.getBoundingClientRect();
245
+ const x = event.clientX - canvasRect.left;
246
+ const y = event.clientY - canvasRect.top;
247
+
248
+ this._picker.prepare(camera, this.app!.scene);
249
+ const selection = this._picker.getSelection(x, y);
250
+
251
+ if (selection.length > 0) {
252
+ let node = selection[0].node;
253
+ while (node) {
254
+ const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
255
+ if (entityElement && entityElement.hasListeners('pointerdown')) {
256
+ entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
257
+ break;
258
+ }
259
+ node = node.parent;
260
+ }
261
+ }
262
+ }
263
+
264
+ _onPointerUp(event: PointerEvent) {
265
+ if (!this._picker || !this.app) return;
266
+
267
+ const camera = this.app!.root.findComponent('camera') as CameraComponent;
268
+ if (!camera) return;
269
+
270
+ const canvasRect = this._canvas!.getBoundingClientRect();
271
+ const x = event.clientX - canvasRect.left;
272
+ const y = event.clientY - canvasRect.top;
273
+
274
+ this._picker.prepare(camera, this.app!.scene);
275
+ const selection = this._picker.getSelection(x, y);
276
+
277
+ if (selection.length > 0) {
278
+ const entityElement = this.querySelector(`pc-entity[name="${selection[0].node.name}"]`) as EntityElement;
279
+ if (entityElement && entityElement.hasListeners('pointerup')) {
280
+ entityElement.dispatchEvent(new PointerEvent('pointerup', event));
281
+ }
282
+ }
283
+ }
284
+
285
+ _onPointerListenerAdded(type: string) {
286
+ if (!this._hasPointerListeners[type] && this._canvas) {
287
+ this._hasPointerListeners[type] = true;
288
+
289
+ // For enter/leave events, we need the move handler
290
+ const handler = (type === 'pointerenter' || type === 'pointerleave') ?
291
+ this._pointerHandlers.pointermove :
292
+ this._pointerHandlers[type];
293
+
294
+ if (handler) {
295
+ this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
296
+ }
297
+ }
298
+ }
299
+
300
+ _onPointerListenerRemoved(type: string) {
301
+ const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
302
+ .some(entity => entity.hasListeners(type));
303
+
304
+ if (!hasListeners && this._canvas) {
305
+ this._hasPointerListeners[type] = false;
306
+
307
+ const handler = (type === 'pointerenter' || type === 'pointerleave') ?
308
+ this._pointerHandlers.pointermove :
309
+ this._pointerHandlers[type];
310
+
311
+ if (handler) {
312
+ this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
313
+ }
314
+ }
315
+ }
316
+
138
317
  /**
139
318
  * Sets the alpha flag.
140
319
  * @param value - The alpha flag.
@@ -1,8 +1,18 @@
1
- import { PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE, CameraComponent, Color, Vec4, XRTYPE_VR } from 'playcanvas';
1
+ import { CameraComponent, Color, Vec4, GAMMA_NONE, GAMMA_SRGB, PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_NEUTRAL, TONEMAP_ACES2, TONEMAP_ACES, TONEMAP_HEJL, TONEMAP_NONE, XRTYPE_VR } from 'playcanvas';
2
2
 
3
3
  import { ComponentElement } from './component';
4
4
  import { parseColor, parseVec4 } from '../utils';
5
5
 
6
+ const tonemaps = new Map([
7
+ ['none', TONEMAP_NONE],
8
+ ['linear', TONEMAP_LINEAR],
9
+ ['filmic', TONEMAP_FILMIC],
10
+ ['hejl', TONEMAP_HEJL],
11
+ ['aces', TONEMAP_ACES],
12
+ ['aces2', TONEMAP_ACES2],
13
+ ['neutral', TONEMAP_NEUTRAL]
14
+ ]);
15
+
6
16
  /**
7
17
  * The CameraComponentElement interface provides properties and methods for manipulating
8
18
  * `<pc-camera>` elements. The CameraComponentElement interface also inherits the properties and
@@ -29,6 +39,10 @@ class CameraComponentElement extends ComponentElement {
29
39
 
30
40
  private _frustumCulling = true;
31
41
 
42
+ private _gamma: 'none' | 'srgb' = 'srgb';
43
+
44
+ private _horizontalFov = false;
45
+
32
46
  private _nearClip = 0.1;
33
47
 
34
48
  private _orthographic = false;
@@ -41,6 +55,8 @@ class CameraComponentElement extends ComponentElement {
41
55
 
42
56
  private _scissorRect = new Vec4(0, 0, 1, 1);
43
57
 
58
+ private _tonemap: 'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2' | 'neutral' = 'none';
59
+
44
60
  /** @ignore */
45
61
  constructor() {
46
62
  super('camera');
@@ -57,12 +73,15 @@ class CameraComponentElement extends ComponentElement {
57
73
  flipFaces: this._flipFaces,
58
74
  fov: this._fov,
59
75
  frustumCulling: this._frustumCulling,
76
+ gammaCorrection: this._gamma === 'srgb' ? GAMMA_SRGB : GAMMA_NONE,
77
+ horizontalFov: this._horizontalFov,
60
78
  nearClip: this._nearClip,
61
79
  orthographic: this._orthographic,
62
80
  orthoHeight: this._orthoHeight,
63
81
  priority: this._priority,
64
82
  rect: this._rect,
65
- scissorRect: this._scissorRect
83
+ scissorRect: this._scissorRect,
84
+ toneMapping: tonemaps.get(this._tonemap)
66
85
  };
67
86
  }
68
87
 
@@ -266,6 +285,45 @@ class CameraComponentElement extends ComponentElement {
266
285
  return this._frustumCulling;
267
286
  }
268
287
 
288
+ /**
289
+ * Sets the gamma correction of the camera.
290
+ * @param value - The gamma correction.
291
+ */
292
+ set gamma(value: 'none' | 'srgb') {
293
+ this._gamma = value;
294
+ if (this.component) {
295
+ this.component.gammaCorrection = value === 'srgb' ? GAMMA_SRGB : GAMMA_NONE;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Gets the gamma correction of the camera.
301
+ * @returns The gamma correction.
302
+ */
303
+ get gamma(): 'none' | 'srgb' {
304
+ return this._gamma;
305
+ }
306
+
307
+ /**
308
+ * Sets whether the camera's field of view (fov) is horizontal or vertical. Defaults to false
309
+ * (meaning it is vertical be default).
310
+ * @param value - Whether the camera's field of view is horizontal.
311
+ */
312
+ set horizontalFov(value: boolean) {
313
+ this._horizontalFov = value;
314
+ if (this.component) {
315
+ this.component.horizontalFov = value;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Gets whether the camera's field of view (fov) is horizontal or vertical.
321
+ * @returns Whether the camera's field of view is horizontal.
322
+ */
323
+ get horizontalFov(): boolean {
324
+ return this._horizontalFov;
325
+ }
326
+
269
327
  /**
270
328
  * Sets the near clip distance of the camera.
271
329
  * @param value - The near clip distance.
@@ -380,6 +438,25 @@ class CameraComponentElement extends ComponentElement {
380
438
  return this._scissorRect;
381
439
  }
382
440
 
441
+ /**
442
+ * Sets the tone mapping of the camera.
443
+ * @param value - The tone mapping.
444
+ */
445
+ set tonemap(value: 'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2' | 'neutral') {
446
+ this._tonemap = value;
447
+ if (this.component) {
448
+ this.component.toneMapping = tonemaps.get(value) ?? TONEMAP_NONE;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Gets the tone mapping of the camera.
454
+ * @returns The tone mapping.
455
+ */
456
+ get tonemap(): 'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2' | 'neutral' {
457
+ return this._tonemap;
458
+ }
459
+
383
460
  static get observedAttributes() {
384
461
  return [
385
462
  ...super.observedAttributes,
@@ -392,12 +469,15 @@ class CameraComponentElement extends ComponentElement {
392
469
  'flip-faces',
393
470
  'fov',
394
471
  'frustum-culling',
472
+ 'gamma',
473
+ 'horizontal-fov',
395
474
  'near-clip',
396
475
  'orthographic',
397
476
  'ortho-height',
398
477
  'priority',
399
478
  'rect',
400
- 'scissor-rect'
479
+ 'scissor-rect',
480
+ 'tonemap'
401
481
  ];
402
482
  }
403
483
 
@@ -432,6 +512,12 @@ class CameraComponentElement extends ComponentElement {
432
512
  case 'frustum-culling':
433
513
  this.frustumCulling = newValue !== 'false';
434
514
  break;
515
+ case 'gamma':
516
+ this.gamma = newValue as 'none' | 'srgb';
517
+ break;
518
+ case 'horizontal-fov':
519
+ this.horizontalFov = this.hasAttribute('horizontal-fov');
520
+ break;
435
521
  case 'near-clip':
436
522
  this.nearClip = parseFloat(newValue);
437
523
  break;
@@ -450,6 +536,9 @@ class CameraComponentElement extends ComponentElement {
450
536
  case 'scissor-rect':
451
537
  this.scissorRect = parseVec4(newValue);
452
538
  break;
539
+ case 'tonemap':
540
+ this.tonemap = newValue as 'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2' | 'neutral';
541
+ break;
453
542
  }
454
543
  }
455
544
  }
@@ -1,8 +1,20 @@
1
- import { Color, LightComponent } from 'playcanvas';
1
+ import { Color, LightComponent, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF3_32F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_PCSS_32F, SHADOW_VSM_16F, SHADOW_VSM_32F } from 'playcanvas';
2
2
 
3
3
  import { ComponentElement } from './component';
4
4
  import { parseColor } from '../utils';
5
5
 
6
+ const shadowTypes = new Map([
7
+ ['pcf1-16f', SHADOW_PCF1_16F],
8
+ ['pcf1-32f', SHADOW_PCF1_32F],
9
+ ['pcf3-16f', SHADOW_PCF3_16F],
10
+ ['pcf3-32f', SHADOW_PCF3_32F],
11
+ ['pcf5-16f', SHADOW_PCF5_16F],
12
+ ['pcf5-32f', SHADOW_PCF5_32F],
13
+ ['vsm-16f', SHADOW_VSM_16F],
14
+ ['vsm-32f', SHADOW_VSM_32F],
15
+ ['pcss-32f', SHADOW_PCSS_32F]
16
+ ]);
17
+
6
18
  /**
7
19
  * The LightComponentElement interface provides properties and methods for manipulating
8
20
  * `<pc-light>` elements. The LightComponentElement interface also inherits the properties and
@@ -29,10 +41,16 @@ class LightComponentElement extends ComponentElement {
29
41
 
30
42
  private _shadowDistance = 16;
31
43
 
44
+ private _shadowIntensity = 1;
45
+
32
46
  private _shadowResolution = 1024;
33
47
 
48
+ private _shadowType: 'pcf1-16f' | 'pcf1-32f' | 'pcf3-16f' | 'pcf3-32f' | 'pcf5-16f' | 'pcf5-32f' | 'vsm-16f' | 'vsm-32f' | 'pcss-32f' = 'pcf3-32f';
49
+
34
50
  private _type = 'directional';
35
51
 
52
+ private _vsmBias = 0.01;
53
+
36
54
  /** @ignore */
37
55
  constructor() {
38
56
  super('light');
@@ -49,8 +67,11 @@ class LightComponentElement extends ComponentElement {
49
67
  range: this._range,
50
68
  shadowBias: this._shadowBias,
51
69
  shadowDistance: this._shadowDistance,
70
+ shadowIntensity: this._shadowIntensity,
52
71
  shadowResolution: this._shadowResolution,
53
- type: this._type
72
+ shadowType: shadowTypes.get(this._shadowType),
73
+ type: this._type,
74
+ vsmBias: this._vsmBias
54
75
  };
55
76
  }
56
77
 
@@ -233,6 +254,25 @@ class LightComponentElement extends ComponentElement {
233
254
  return this._shadowDistance;
234
255
  }
235
256
 
257
+ /**
258
+ * Sets the shadow intensity of the light.
259
+ * @param value - The shadow intensity.
260
+ */
261
+ set shadowIntensity(value: number) {
262
+ this._shadowIntensity = value;
263
+ if (this.component) {
264
+ this.component.shadowIntensity = value;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Gets the shadow intensity of the light.
270
+ * @returns The shadow intensity.
271
+ */
272
+ get shadowIntensity() {
273
+ return this._shadowIntensity;
274
+ }
275
+
236
276
  /**
237
277
  * Sets the shadow resolution of the light.
238
278
  * @param value - The shadow resolution.
@@ -252,6 +292,35 @@ class LightComponentElement extends ComponentElement {
252
292
  return this._shadowResolution;
253
293
  }
254
294
 
295
+ /**
296
+ * Sets the shadow type of the light.
297
+ * @param value - The shadow type. Can be:
298
+ *
299
+ * - `pcf1-16f` - 1-tap percentage-closer filtered shadow map with 16-bit depth.
300
+ * - `pcf1-32f` - 1-tap percentage-closer filtered shadow map with 32-bit depth.
301
+ * - `pcf3-16f` - 3-tap percentage-closer filtered shadow map with 16-bit depth.
302
+ * - `pcf3-32f` - 3-tap percentage-closer filtered shadow map with 32-bit depth.
303
+ * - `pcf5-16f` - 5-tap percentage-closer filtered shadow map with 16-bit depth.
304
+ * - `pcf5-32f` - 5-tap percentage-closer filtered shadow map with 32-bit depth.
305
+ * - `vsm-16f` - Variance shadow map with 16-bit depth.
306
+ * - `vsm-32f` - Variance shadow map with 32-bit depth.
307
+ * - `pcss-32f` - Percentage-closer soft shadow with 32-bit depth.
308
+ */
309
+ set shadowType(value: 'pcf1-16f' | 'pcf1-32f' | 'pcf3-16f' | 'pcf3-32f' | 'pcf5-16f' | 'pcf5-32f' | 'vsm-16f' | 'vsm-32f' | 'pcss-32f') {
310
+ this._shadowType = value;
311
+ if (this.component) {
312
+ this.component.shadowType = shadowTypes.get(value) ?? SHADOW_PCF3_32F;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Gets the shadow type of the light.
318
+ * @returns The shadow type.
319
+ */
320
+ get shadowType() {
321
+ return this._shadowType;
322
+ }
323
+
255
324
  /**
256
325
  * Sets the type of the light.
257
326
  * @param value - The type.
@@ -276,6 +345,25 @@ class LightComponentElement extends ComponentElement {
276
345
  return this._type;
277
346
  }
278
347
 
348
+ /**
349
+ * Sets the VSM bias of the light.
350
+ * @param value - The VSM bias.
351
+ */
352
+ set vsmBias(value: number) {
353
+ this._vsmBias = value;
354
+ if (this.component) {
355
+ this.component.vsmBias = value;
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Gets the VSM bias of the light.
361
+ * @returns The VSM bias.
362
+ */
363
+ get vsmBias() {
364
+ return this._vsmBias;
365
+ }
366
+
279
367
  static get observedAttributes() {
280
368
  return [
281
369
  ...super.observedAttributes,
@@ -288,8 +376,11 @@ class LightComponentElement extends ComponentElement {
288
376
  'range',
289
377
  'shadow-bias',
290
378
  'shadow-distance',
379
+ 'shadow-intensity',
291
380
  'shadow-resolution',
292
- 'type'
381
+ 'shadow-type',
382
+ 'type',
383
+ 'vsm-bias'
293
384
  ];
294
385
  }
295
386
 
@@ -327,9 +418,18 @@ class LightComponentElement extends ComponentElement {
327
418
  case 'shadow-resolution':
328
419
  this.shadowResolution = Number(newValue);
329
420
  break;
421
+ case 'shadow-intensity':
422
+ this.shadowIntensity = Number(newValue);
423
+ break;
424
+ case 'shadow-type':
425
+ this.shadowType = newValue as 'pcf1-16f' | 'pcf1-32f' | 'pcf3-16f' | 'pcf3-32f' | 'pcf5-16f' | 'pcf5-32f' | 'vsm-16f' | 'vsm-32f' | 'pcss-32f';
426
+ break;
330
427
  case 'type':
331
428
  this.type = newValue;
332
429
  break;
430
+ case 'vsm-bias':
431
+ this.vsmBias = Number(newValue);
432
+ break;
333
433
  }
334
434
  }
335
435
  }
@@ -4,10 +4,6 @@ import { ComponentElement } from './component';
4
4
  import { ScriptElement } from './script';
5
5
  import { AssetElement } from '../asset';
6
6
 
7
- const tmpV2 = new Vec2();
8
- const tmpV3 = new Vec3();
9
- const tmpV4 = new Vec4();
10
-
11
7
  // Add these interfaces at the top of the file, after the imports
12
8
  interface ScriptAttributesChangeEvent extends CustomEvent {
13
9
  detail: { attributes: any };
@@ -76,23 +72,36 @@ class ScriptComponentElement extends ComponentElement {
76
72
  }
77
73
  }
78
74
 
79
- // Handle vectors
80
- if (Array.isArray(value)) {
81
- if (target[key] instanceof Vec2) {
82
- target[key] = tmpV2.set(value[0], value[1]);
75
+ // Handle arrays
76
+ if (value && typeof value === 'object' && Array.isArray(value)) {
77
+ // If it's an array of objects, recursively apply to each object
78
+ if (value.length > 0 && typeof value[0] === 'object') {
79
+ target[key] = value.map((item) => {
80
+ const obj = {};
81
+ for (const itemKey in item) {
82
+ applyValue(obj, itemKey, item[itemKey]);
83
+ }
84
+ return obj;
85
+ });
86
+ return;
87
+ }
88
+
89
+ // Handle vectors
90
+ if (value.length === 2 && typeof value[0] === 'number') {
91
+ target[key] = new Vec2(value[0], value[1]);
83
92
  return;
84
93
  }
85
- if (target[key] instanceof Vec3) {
86
- target[key] = tmpV3.set(value[0], value[1], value[2]);
94
+ if (value.length === 3 && typeof value[0] === 'number') {
95
+ target[key] = new Vec3(value[0], value[1], value[2]);
87
96
  return;
88
97
  }
89
- if (target[key] instanceof Vec4) {
90
- target[key] = tmpV4.set(value[0], value[1], value[2], value[3]);
98
+ if (value.length === 4 && typeof value[0] === 'number') {
99
+ target[key] = new Vec4(value[0], value[1], value[2], value[3]);
91
100
  return;
92
101
  }
93
102
  }
94
103
 
95
- // Handle nested objects
104
+ // Handle nested objects (non-array)
96
105
  if (value && typeof value === 'object' && !Array.isArray(value)) {
97
106
  if (!target[key] || typeof target[key] !== 'object') {
98
107
  target[key] = {};