@playcanvas/web-components 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +31 -49
  3. package/dist/asset.d.ts +152 -12
  4. package/dist/async-element.d.ts +26 -9
  5. package/dist/components/button-component.d.ts +3 -7
  6. package/dist/components/camera-component.d.ts +3 -7
  7. package/dist/components/collision-component.d.ts +19 -7
  8. package/dist/components/component.d.ts +41 -4
  9. package/dist/components/element-component.d.ts +4 -8
  10. package/dist/components/gsplat-component.d.ts +2 -7
  11. package/dist/components/layoutchild-component.d.ts +2 -7
  12. package/dist/components/layoutgroup-component.d.ts +3 -7
  13. package/dist/components/light-component.d.ts +3 -7
  14. package/dist/components/listener-component.d.ts +1 -6
  15. package/dist/components/particlesystem-component.d.ts +2 -7
  16. package/dist/components/render-component.d.ts +2 -7
  17. package/dist/components/rigidbody-component.d.ts +3 -7
  18. package/dist/components/screen-component.d.ts +3 -7
  19. package/dist/components/script-component.d.ts +8 -20
  20. package/dist/components/script.d.ts +2 -22
  21. package/dist/components/scrollbar-component.d.ts +2 -7
  22. package/dist/components/scrollview-component.d.ts +3 -7
  23. package/dist/components/sound-component.d.ts +2 -7
  24. package/dist/components/sound-slot.d.ts +8 -6
  25. package/dist/custom-elements.json +5824 -10581
  26. package/dist/entity-base.d.ts +67 -0
  27. package/dist/entity.d.ts +7 -48
  28. package/dist/index.d.ts +41 -1
  29. package/dist/material.d.ts +14 -13
  30. package/dist/model.d.ts +43 -5
  31. package/dist/module.d.ts +0 -6
  32. package/dist/node.d.ts +253 -0
  33. package/dist/parse.d.ts +2 -1
  34. package/dist/pwc.cjs +1830 -274
  35. package/dist/pwc.cjs.map +1 -1
  36. package/dist/pwc.js +1830 -274
  37. package/dist/pwc.js.map +1 -1
  38. package/dist/pwc.min.js +1 -1
  39. package/dist/pwc.min.js.map +1 -1
  40. package/dist/pwc.min.mjs +1 -1
  41. package/dist/pwc.min.mjs.map +1 -1
  42. package/dist/pwc.mjs +1830 -276
  43. package/dist/pwc.mjs.map +1 -1
  44. package/dist/scene.d.ts +4 -7
  45. package/dist/sky.d.ts +13 -5
  46. package/dist/vscode.html-custom-data.json +148 -26
  47. package/dist/web-types.json +894 -581
  48. package/package.json +9 -8
  49. package/src/app.ts +163 -88
  50. package/src/asset.ts +472 -36
  51. package/src/async-element.ts +39 -12
  52. package/src/components/button-component.ts +5 -9
  53. package/src/components/camera-component.ts +24 -10
  54. package/src/components/collision-component.ts +61 -15
  55. package/src/components/component.ts +151 -11
  56. package/src/components/element-component.ts +26 -30
  57. package/src/components/gsplat-component.ts +4 -9
  58. package/src/components/layoutchild-component.ts +4 -9
  59. package/src/components/layoutgroup-component.ts +14 -9
  60. package/src/components/light-component.ts +42 -12
  61. package/src/components/listener-component.ts +1 -7
  62. package/src/components/particlesystem-component.ts +7 -15
  63. package/src/components/render-component.ts +5 -10
  64. package/src/components/rigidbody-component.ts +23 -16
  65. package/src/components/screen-component.ts +5 -9
  66. package/src/components/script-component.ts +108 -46
  67. package/src/components/script.ts +38 -33
  68. package/src/components/scrollbar-component.ts +6 -16
  69. package/src/components/scrollview-component.ts +16 -11
  70. package/src/components/sound-component.ts +10 -15
  71. package/src/components/sound-slot.ts +30 -20
  72. package/src/entity-base.ts +136 -0
  73. package/src/entity.ts +47 -118
  74. package/src/index.ts +50 -1
  75. package/src/loading-bar.ts +8 -8
  76. package/src/material.ts +65 -39
  77. package/src/model.ts +140 -17
  78. package/src/module.ts +8 -7
  79. package/src/node.ts +715 -0
  80. package/src/parse.ts +62 -17
  81. package/src/scene.ts +12 -9
  82. package/src/sky.ts +50 -10
package/src/asset.ts CHANGED
@@ -1,8 +1,24 @@
1
- import { Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from 'playcanvas';
1
+ import {
2
+ ADDRESS_CLAMP_TO_EDGE,
3
+ ADDRESS_MIRRORED_REPEAT,
4
+ ADDRESS_REPEAT,
5
+ Asset,
6
+ FILTER_LINEAR,
7
+ FILTER_LINEAR_MIPMAP_LINEAR,
8
+ FILTER_LINEAR_MIPMAP_NEAREST,
9
+ FILTER_NEAREST,
10
+ FILTER_NEAREST_MIPMAP_LINEAR,
11
+ FILTER_NEAREST_MIPMAP_NEAREST,
12
+ SPRITE_RENDERMODE_SIMPLE,
13
+ SPRITE_RENDERMODE_SLICED,
14
+ SPRITE_RENDERMODE_TILED
15
+ } from 'playcanvas';
16
+ import type { Texture, TextureAtlas } from 'playcanvas';
17
+
18
+ import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
2
19
 
3
20
  import { AsyncElement } from './async-element';
4
21
  import { parseBool, parseEnum, parseNumber } from './parse';
5
- import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
6
22
 
7
23
  const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
8
24
  ['simple', SPRITE_RENDERMODE_SIMPLE],
@@ -10,6 +26,66 @@ const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
10
26
  ['tiled', SPRITE_RENDERMODE_TILED]
11
27
  ]);
12
28
 
29
+ type AddressMode = 'repeat' | 'clamp' | 'mirror';
30
+
31
+ const addressModes = new Map<AddressMode, number>([
32
+ ['repeat', ADDRESS_REPEAT],
33
+ ['clamp', ADDRESS_CLAMP_TO_EDGE],
34
+ ['mirror', ADDRESS_MIRRORED_REPEAT]
35
+ ]);
36
+
37
+ type MinFilterMode =
38
+ 'nearest' | 'linear' | 'nearest-mip-nearest' | 'linear-mip-nearest' | 'nearest-mip-linear' | 'linear-mip-linear';
39
+
40
+ const minFilterModes = new Map<MinFilterMode, number>([
41
+ ['nearest', FILTER_NEAREST],
42
+ ['linear', FILTER_LINEAR],
43
+ ['nearest-mip-nearest', FILTER_NEAREST_MIPMAP_NEAREST],
44
+ ['linear-mip-nearest', FILTER_LINEAR_MIPMAP_NEAREST],
45
+ ['nearest-mip-linear', FILTER_NEAREST_MIPMAP_LINEAR],
46
+ ['linear-mip-linear', FILTER_LINEAR_MIPMAP_LINEAR]
47
+ ]);
48
+
49
+ // Magnification has no mip variants - the engine (and the GPU) only accepts these two.
50
+ type MagFilterMode = 'nearest' | 'linear';
51
+
52
+ const magFilterModes = new Map<MagFilterMode, number>([
53
+ ['nearest', FILTER_NEAREST],
54
+ ['linear', FILTER_LINEAR]
55
+ ]);
56
+
57
+ // The engine's texture JSON spells the filter names with underscores ('linear_mip_linear'); the
58
+ // attribute values are kebab-case like every other enum attribute in this library. The address
59
+ // mode names contain no dashes, so for them the rename is the identity.
60
+ const toTextureJson = (name: string) => name.replace(/-/g, '_');
61
+
62
+ /** The Texture properties written by the texture option attributes. */
63
+ type TextureOptionProperty =
64
+ 'addressU' | 'addressV' | 'anisotropy' | 'flipY' | 'magFilter' | 'minFilter' | 'mipmaps' | 'srgb';
65
+
66
+ // Engine Texture constructor defaults, restored on a loaded texture when a texture option
67
+ // attribute is removed.
68
+ const textureOptionDefaults: Record<TextureOptionProperty, number | boolean> = {
69
+ addressU: ADDRESS_REPEAT,
70
+ addressV: ADDRESS_REPEAT,
71
+ anisotropy: 1,
72
+ flipY: false,
73
+ magFilter: FILTER_LINEAR,
74
+ minFilter: FILTER_LINEAR_MIPMAP_LINEAR,
75
+ mipmaps: true,
76
+ srgb: false
77
+ };
78
+
79
+ // Attributes that only apply to certain asset types, used to warn when one is set on an asset of
80
+ // any other type (where it would otherwise be silently ignored).
81
+ const typeScopedAttributes: [attributes: string[], types: string[]][] = [
82
+ [
83
+ ['address-u', 'address-v', 'anisotropy', 'flip-y', 'mag-filter', 'min-filter', 'mipmaps', 'srgb'],
84
+ ['texture', 'textureatlas']
85
+ ],
86
+ [['atlas', 'frame-keys', 'pixels-per-unit', 'render-mode'], ['sprite']]
87
+ ];
88
+
13
89
  const extToType = new Map([
14
90
  ['bin', 'binary'],
15
91
  ['css', 'css'],
@@ -33,12 +109,11 @@ const extToType = new Map([
33
109
  ['webp', 'texture']
34
110
  ]);
35
111
 
36
-
37
112
  // provide buffer view callback so we can handle models compressed with MeshOptimizer
38
113
  // https://github.com/zeux/meshoptimizer
39
114
  const processBufferView = (
40
115
  gltfBuffer: any,
41
- buffers: Array<any>,
116
+ buffers: any[],
42
117
  continuation: (err: string | null, result: any) => void
43
118
  ) => {
44
119
  if (gltfBuffer.extensions && gltfBuffer.extensions.EXT_meshopt_compression) {
@@ -56,14 +131,7 @@ const processBufferView = (
56
131
  const result = new Uint8Array(count * stride);
57
132
  const source = new Uint8Array(buffer.buffer, buffer.byteOffset + byteOffset, byteLength);
58
133
 
59
- MeshoptDecoder.decodeGltfBuffer(
60
- result,
61
- count,
62
- stride,
63
- source,
64
- extensionDef.mode,
65
- extensionDef.filter
66
- );
134
+ MeshoptDecoder.decodeGltfBuffer(result, count, stride, source, extensionDef.mode, extensionDef.filter);
67
135
 
68
136
  continuation(null, result);
69
137
  });
@@ -85,8 +153,16 @@ const processBufferView = (
85
153
  * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
86
154
  * elsewhere, or with an unsupported asset type, never become ready.
87
155
  *
88
- * Apart from `lazy`, these attributes are read once when the asset is created, so changing them
89
- * later has no effect.
156
+ * For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
157
+ * `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
158
+ * created and — like `lazy` — are observed: changing one updates a texture that has already
159
+ * loaded, and removing one restores the engine default. Changing `srgb` or `mipmaps` on a loaded
160
+ * texture recreates the underlying GPU resource, so prefer declaring those up front. Each option
161
+ * overrides the matching key in the `data` JSON; options left unset write nothing, leaving the
162
+ * engine's per-format defaults in force.
163
+ *
164
+ * Apart from `lazy` and the texture options, these attributes are read once when the asset is
165
+ * created, so changing them later has no effect.
90
166
  *
91
167
  * @attribute {string} id - The identifier used to reference the asset from other elements.
92
168
  * @attribute {string} src - The URL of the asset to load.
@@ -108,7 +184,23 @@ const processBufferView = (
108
184
  * not that it succeeded.
109
185
  */
110
186
  class AssetElement extends AsyncElement {
111
- private _lazy: boolean = false;
187
+ private _addressU: AddressMode | null = null;
188
+
189
+ private _addressV: AddressMode | null = null;
190
+
191
+ private _anisotropy: number | null = null;
192
+
193
+ private _flipY: boolean | null = null;
194
+
195
+ private _lazy = false;
196
+
197
+ private _magFilter: MagFilterMode | null = null;
198
+
199
+ private _minFilter: MinFilterMode | null = null;
200
+
201
+ private _mipmaps: boolean | null = null;
202
+
203
+ private _srgb: boolean | null = null;
112
204
 
113
205
  /**
114
206
  * The asset that is loaded. Available once the element is ready — await
@@ -122,7 +214,9 @@ class AssetElement extends AsyncElement {
122
214
 
123
215
  // Assets must be direct children of pc-app (matches the boot query ':scope > pc-asset')
124
216
  if (this.parentElement !== appElement) {
125
- console.warn(`pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`);
217
+ console.warn(
218
+ `pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`
219
+ );
126
220
  return;
127
221
  }
128
222
 
@@ -137,7 +231,7 @@ class AssetElement extends AsyncElement {
137
231
  const app = appElement.app;
138
232
  if (!app) return; // pc-app is re-connecting; its own boot will create this asset
139
233
 
140
- this.createAsset();
234
+ this._createAsset();
141
235
  if (this.asset) {
142
236
  app.assets.add(this.asset); // add() auto-loads when preload is true
143
237
  if (!this.lazy) {
@@ -146,14 +240,16 @@ class AssetElement extends AsyncElement {
146
240
  }
147
241
  }
148
242
 
149
- // Never ready if createAsset failed (unsupported asset type)
243
+ // Never ready if _createAsset failed (unsupported asset type)
150
244
  if (this.asset) {
151
245
  this._onReady();
152
246
  }
153
247
  }
154
248
 
155
249
  disconnectedCallback() {
156
- this.destroyAsset();
250
+ this._destroyAsset();
251
+ // Re-arm readiness so a re-inserted element announces the asset it creates then
252
+ this._resetReady();
157
253
  }
158
254
 
159
255
  private _onAssetLoad() {
@@ -161,12 +257,21 @@ class AssetElement extends AsyncElement {
161
257
  }
162
258
 
163
259
  private _onAssetError(err: string | Error) {
164
- this.dispatchEvent(new ErrorEvent('error', {
165
- message: err instanceof Error ? err.message : String(err)
166
- }));
260
+ this.dispatchEvent(
261
+ new ErrorEvent('error', {
262
+ message: err instanceof Error ? err.message : String(err)
263
+ })
264
+ );
167
265
  }
168
266
 
169
- createAsset() {
267
+ /**
268
+ * Creates the asset from the element's attributes. Called by the containing `<pc-app>`
269
+ * element during its boot sweep, and on connection for elements inserted while the
270
+ * application is already running.
271
+ *
272
+ * @internal
273
+ */
274
+ _createAsset() {
170
275
  const id = this.getAttribute('id') || '';
171
276
  const src = this.getAttribute('src') || '';
172
277
  let type = this.getAttribute('type');
@@ -182,6 +287,18 @@ class AssetElement extends AsyncElement {
182
287
  return;
183
288
  }
184
289
 
290
+ // Attributes scoped to other asset types have no effect here - say so rather than
291
+ // failing silently.
292
+ const inapplicable = typeScopedAttributes
293
+ .filter(([, types]) => !types.includes(type))
294
+ .flatMap(([attributes]) => attributes)
295
+ .filter((attribute) => this.hasAttribute(attribute));
296
+ if (inapplicable.length > 0) {
297
+ console.warn(
298
+ `pc-asset '${id || src}' has attributes that do not apply to asset type '${type}' and are ignored: ${inapplicable.join(', ')}`
299
+ );
300
+ }
301
+
185
302
  // Optional inline asset data, used by data-driven assets such as texture atlases (frame
186
303
  // definitions) and sprites (atlas reference, frame keys, etc.).
187
304
  const data = this._buildData(type);
@@ -211,9 +328,11 @@ class AssetElement extends AsyncElement {
211
328
  }
212
329
 
213
330
  /**
214
- * Builds the `data` object for the asset from an optional inline `data` attribute (JSON) and,
215
- * for sprites, from the convenience attributes (`atlas`, `frame-keys`, `pixels-per-unit`,
216
- * `render-mode`). Returns `undefined` when there is no data to apply.
331
+ * Builds the `data` object for the asset from an optional inline `data` attribute (JSON), the
332
+ * texture option attributes (for `texture` and `textureatlas` assets), and the sprite
333
+ * convenience attributes (`atlas`, `frame-keys`, `pixels-per-unit`, `render-mode`). An
334
+ * attribute overrides the matching `data` JSON key. Returns `undefined` when there is no data
335
+ * to apply.
217
336
  * @param type - The resolved asset type.
218
337
  * @returns The asset data, or `undefined`.
219
338
  */
@@ -229,6 +348,39 @@ class AssetElement extends AsyncElement {
229
348
  }
230
349
  }
231
350
 
351
+ if (type === 'texture' || type === 'textureatlas') {
352
+ data = data ?? {};
353
+
354
+ // Only options the user actually set are written: the engine reads these keys with
355
+ // hasOwnProperty semantics, and an absent key leaves its per-format default (an HDR's
356
+ // 'rgbe' type, a KTX2's transcoded format) in force.
357
+ if (this._addressU !== null) {
358
+ data.addressu = this._addressU;
359
+ }
360
+ if (this._addressV !== null) {
361
+ data.addressv = this._addressV;
362
+ }
363
+ if (this._anisotropy !== null) {
364
+ data.anisotropy = this._anisotropy;
365
+ }
366
+ if (this._flipY !== null) {
367
+ // 'flipY' is the one camelCase key in the engine's texture JSON
368
+ data.flipY = this._flipY;
369
+ }
370
+ if (this._magFilter !== null) {
371
+ data.magfilter = toTextureJson(this._magFilter);
372
+ }
373
+ if (this._minFilter !== null) {
374
+ data.minfilter = toTextureJson(this._minFilter);
375
+ }
376
+ if (this._mipmaps !== null) {
377
+ data.mipmaps = this._mipmaps;
378
+ }
379
+ if (this._srgb !== null) {
380
+ data.srgb = this._srgb;
381
+ }
382
+ }
383
+
232
384
  if (type === 'sprite') {
233
385
  data = data ?? {};
234
386
 
@@ -268,8 +420,61 @@ class AssetElement extends AsyncElement {
268
420
  return data;
269
421
  }
270
422
 
423
+ /**
424
+ * Returns the engine texture behind this asset, when there is one: the resource itself for a
425
+ * `texture` asset, the atlas's texture for a `textureatlas` asset, `null` otherwise
426
+ * (including before the asset has loaded).
427
+ * @returns The texture, or `null`.
428
+ */
429
+ private _texture(): Texture | null {
430
+ const asset = this.asset;
431
+ if (!asset?.resource) return null;
432
+ if (asset.type === 'texture') return asset.resource as Texture;
433
+ if (asset.type === 'textureatlas') return (asset.resource as TextureAtlas).texture ?? null;
434
+ return null;
435
+ }
271
436
 
272
- destroyAsset() {
437
+ /**
438
+ * Writes one texture option through to the created asset, if any. The engine-JSON key is
439
+ * written into `asset.data`, mutated in place - replacing the whole object would make the
440
+ * registry re-patch every key, and a re-patched `srgb` or `mipmaps` recreates the texture
441
+ * even when unchanged. The in-place key is what a not-yet-started load reads at texture
442
+ * construction, and what any later reload reads. When the texture already exists, the
443
+ * corresponding property is assigned directly; `null` (attribute removed) deletes the key
444
+ * and restores the engine default. Assets of any other type are left untouched.
445
+ *
446
+ * @param key - The engine texture JSON key in `asset.data`.
447
+ * @param property - The Texture property to assign.
448
+ * @param dataValue - The engine-JSON value for `asset.data`, or `null` to delete the key.
449
+ * @param textureValue - The value for the Texture property, or `null` for the engine default.
450
+ */
451
+ private _applyTextureOption(
452
+ key: string,
453
+ property: TextureOptionProperty,
454
+ dataValue: string | number | boolean | null,
455
+ textureValue: number | boolean | null
456
+ ) {
457
+ const asset = this.asset;
458
+ if (!asset || (asset.type !== 'texture' && asset.type !== 'textureatlas')) return;
459
+
460
+ const data = asset.data as Record<string, any>;
461
+ if (dataValue === null) {
462
+ delete data[key];
463
+ } else {
464
+ data[key] = dataValue;
465
+ }
466
+
467
+ const texture = this._texture();
468
+ if (texture) {
469
+ // Every option here is a number- or boolean-valued Texture property; the
470
+ // value/property pairing is fixed by the callers, which TypeScript cannot see
471
+ // through the union.
472
+ (texture as unknown as Record<TextureOptionProperty, number | boolean>)[property] =
473
+ textureValue ?? textureOptionDefaults[property];
474
+ }
475
+ }
476
+
477
+ private _destroyAsset() {
273
478
  if (this.asset) {
274
479
  // A caller that keeps the Asset alive must not dispatch on a removed element
275
480
  this.asset.off('load', this._onAssetLoad, this);
@@ -281,6 +486,83 @@ class AssetElement extends AsyncElement {
281
486
  }
282
487
  }
283
488
 
489
+ /**
490
+ * Sets the texture's horizontal (U) address mode: how texture coordinates outside the 0 to 1
491
+ * range sample the texture. Applies to `texture` and `textureatlas` assets, both when the
492
+ * texture is created and after it has loaded.
493
+ * @param value - The address mode, or `null` to use the engine default of 'repeat'.
494
+ */
495
+ set addressU(value: AddressMode | null) {
496
+ this._addressU = value;
497
+ const constant = value === null ? null : (addressModes.get(value) ?? ADDRESS_REPEAT);
498
+ this._applyTextureOption('addressu', 'addressU', value, constant);
499
+ }
500
+
501
+ /**
502
+ * Gets the texture's horizontal (U) address mode.
503
+ * @returns The address mode, or `null` when unset.
504
+ */
505
+ get addressU(): AddressMode | null {
506
+ return this._addressU;
507
+ }
508
+
509
+ /**
510
+ * Sets the texture's vertical (V) address mode: how texture coordinates outside the 0 to 1
511
+ * range sample the texture. Applies to `texture` and `textureatlas` assets, both when the
512
+ * texture is created and after it has loaded.
513
+ * @param value - The address mode, or `null` to use the engine default of 'repeat'.
514
+ */
515
+ set addressV(value: AddressMode | null) {
516
+ this._addressV = value;
517
+ const constant = value === null ? null : (addressModes.get(value) ?? ADDRESS_REPEAT);
518
+ this._applyTextureOption('addressv', 'addressV', value, constant);
519
+ }
520
+
521
+ /**
522
+ * Gets the texture's vertical (V) address mode.
523
+ * @returns The address mode, or `null` when unset.
524
+ */
525
+ get addressV(): AddressMode | null {
526
+ return this._addressV;
527
+ }
528
+
529
+ /**
530
+ * Sets the texture's maximum anisotropic filtering level, which improves quality at oblique
531
+ * viewing angles. Applies to `texture` and `textureatlas` assets, both when the texture is
532
+ * created and after it has loaded.
533
+ * @param value - The anisotropy level, or `null` to use the engine default of 1.
534
+ */
535
+ set anisotropy(value: number | null) {
536
+ this._anisotropy = value;
537
+ this._applyTextureOption('anisotropy', 'anisotropy', value, value);
538
+ }
539
+
540
+ /**
541
+ * Gets the texture's maximum anisotropic filtering level.
542
+ * @returns The anisotropy level, or `null` when unset.
543
+ */
544
+ get anisotropy(): number | null {
545
+ return this._anisotropy;
546
+ }
547
+
548
+ /**
549
+ * Sets whether the texture's image data is flipped vertically at upload. Applies to `texture`
550
+ * and `textureatlas` assets, both when the texture is created and after it has loaded.
551
+ * @param value - The flip flag, or `null` to use the engine default of `false`.
552
+ */
553
+ set flipY(value: boolean | null) {
554
+ this._flipY = value;
555
+ this._applyTextureOption('flipY', 'flipY', value, value);
556
+ }
557
+
558
+ /**
559
+ * Gets whether the texture's image data is flipped vertically at upload.
560
+ * @returns The flip flag, or `null` when unset.
561
+ */
562
+ get flipY(): boolean | null {
563
+ return this._flipY;
564
+ }
565
+
284
566
  /**
285
567
  * Sets whether the asset should be loaded lazily.
286
568
  * @param value - The lazy loading flag.
@@ -300,28 +582,182 @@ class AssetElement extends AsyncElement {
300
582
  return this._lazy;
301
583
  }
302
584
 
585
+ /**
586
+ * Sets the texture's magnification filter, used when the texture is displayed larger than its
587
+ * source size. Applies to `texture` and `textureatlas` assets, both when the texture is
588
+ * created and after it has loaded.
589
+ * @param value - The filter, or `null` to use the engine default of 'linear'.
590
+ */
591
+ set magFilter(value: MagFilterMode | null) {
592
+ this._magFilter = value;
593
+ const json = value === null ? null : toTextureJson(value);
594
+ const constant = value === null ? null : (magFilterModes.get(value) ?? FILTER_LINEAR);
595
+ this._applyTextureOption('magfilter', 'magFilter', json, constant);
596
+ }
597
+
598
+ /**
599
+ * Gets the texture's magnification filter.
600
+ * @returns The filter, or `null` when unset.
601
+ */
602
+ get magFilter(): MagFilterMode | null {
603
+ return this._magFilter;
604
+ }
605
+
606
+ /**
607
+ * Sets the texture's minification filter, used when the texture is displayed smaller than its
608
+ * source size. The mip variants blend within (and, for the second `linear`, between) mipmap
609
+ * levels. Applies to `texture` and `textureatlas` assets, both when the texture is created
610
+ * and after it has loaded.
611
+ * @param value - The filter, or `null` to use the engine default of 'linear-mip-linear'.
612
+ */
613
+ set minFilter(value: MinFilterMode | null) {
614
+ this._minFilter = value;
615
+ const json = value === null ? null : toTextureJson(value);
616
+ const constant = value === null ? null : (minFilterModes.get(value) ?? FILTER_LINEAR_MIPMAP_LINEAR);
617
+ this._applyTextureOption('minfilter', 'minFilter', json, constant);
618
+ }
619
+
620
+ /**
621
+ * Gets the texture's minification filter.
622
+ * @returns The filter, or `null` when unset.
623
+ */
624
+ get minFilter(): MinFilterMode | null {
625
+ return this._minFilter;
626
+ }
627
+
628
+ /**
629
+ * Sets whether the texture generates and uses mipmaps. Changing this on a loaded texture
630
+ * recreates the underlying GPU resource, so prefer declaring it up front. Applies to
631
+ * `texture` and `textureatlas` assets.
632
+ * @param value - The mipmaps flag, or `null` to use the engine default of `true`.
633
+ */
634
+ set mipmaps(value: boolean | null) {
635
+ this._mipmaps = value;
636
+ this._applyTextureOption('mipmaps', 'mipmaps', value, value);
637
+ }
638
+
639
+ /**
640
+ * Gets whether the texture generates and uses mipmaps.
641
+ * @returns The mipmaps flag, or `null` when unset.
642
+ */
643
+ get mipmaps(): boolean | null {
644
+ return this._mipmaps;
645
+ }
646
+
647
+ /**
648
+ * Sets whether the texture holds sRGB (gamma-encoded) color data, enabling hardware gamma
649
+ * decode. Free when set before the texture loads; changing it on a loaded texture recreates
650
+ * the underlying GPU resource. Applies to `texture` and `textureatlas` assets.
651
+ * @param value - The sRGB flag, or `null` to use the engine default of `false`.
652
+ */
653
+ set srgb(value: boolean | null) {
654
+ this._srgb = value;
655
+ this._applyTextureOption('srgb', 'srgb', value, value);
656
+ }
657
+
658
+ /**
659
+ * Gets whether the texture holds sRGB (gamma-encoded) color data.
660
+ * @returns The sRGB flag, or `null` when unset.
661
+ */
662
+ get srgb(): boolean | null {
663
+ return this._srgb;
664
+ }
665
+
666
+ /**
667
+ * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
668
+ * `undefined` if there is no such element or its asset has not been created yet.
669
+ *
670
+ * @param id - The `id` of the `<pc-asset>` element.
671
+ * @returns The asset, or `undefined`.
672
+ */
303
673
  static get(id: string) {
304
674
  const assetElement = document.querySelector<AssetElement>(`pc-asset[id="${id}"]`);
305
675
  return assetElement?.asset;
306
676
  }
307
677
 
308
678
  static get observedAttributes() {
309
- return ['lazy'];
679
+ return [
680
+ 'address-u',
681
+ 'address-v',
682
+ 'anisotropy',
683
+ 'flip-y',
684
+ 'lazy',
685
+ 'mag-filter',
686
+ 'min-filter',
687
+ 'mipmaps',
688
+ 'srgb'
689
+ ];
310
690
  }
311
691
 
312
692
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
313
- if (name === 'lazy') {
314
- this.lazy = parseBool(newValue, false);
693
+ // Each texture option keeps its parse* call as the branch's first assignment (the CEM
694
+ // manifest derives the attribute's type and default from it - a ternary would degrade
695
+ // both to plain string) and treats a removed attribute (null) as a reset to unset,
696
+ // which restores the engine default on a loaded texture.
697
+ switch (name) {
698
+ case 'address-u':
699
+ if (newValue !== null) {
700
+ this.addressU = parseEnum(newValue, addressModes, 'repeat', name);
701
+ } else {
702
+ this.addressU = null;
703
+ }
704
+ break;
705
+ case 'address-v':
706
+ if (newValue !== null) {
707
+ this.addressV = parseEnum(newValue, addressModes, 'repeat', name);
708
+ } else {
709
+ this.addressV = null;
710
+ }
711
+ break;
712
+ case 'anisotropy':
713
+ if (newValue !== null) {
714
+ this.anisotropy = parseNumber(newValue, 1, name);
715
+ } else {
716
+ this.anisotropy = null;
717
+ }
718
+ break;
719
+ case 'flip-y':
720
+ if (newValue !== null) {
721
+ this.flipY = parseBool(newValue, false);
722
+ } else {
723
+ this.flipY = null;
724
+ }
725
+ break;
726
+ case 'lazy':
727
+ this.lazy = parseBool(newValue, false);
728
+ break;
729
+ case 'mag-filter':
730
+ if (newValue !== null) {
731
+ this.magFilter = parseEnum(newValue, magFilterModes, 'linear', name);
732
+ } else {
733
+ this.magFilter = null;
734
+ }
735
+ break;
736
+ case 'min-filter':
737
+ if (newValue !== null) {
738
+ this.minFilter = parseEnum(newValue, minFilterModes, 'linear-mip-linear', name);
739
+ } else {
740
+ this.minFilter = null;
741
+ }
742
+ break;
743
+ case 'mipmaps':
744
+ if (newValue !== null) {
745
+ this.mipmaps = parseBool(newValue, true);
746
+ } else {
747
+ this.mipmaps = null;
748
+ }
749
+ break;
750
+ case 'srgb':
751
+ if (newValue !== null) {
752
+ this.srgb = parseBool(newValue, false);
753
+ } else {
754
+ this.srgb = null;
755
+ }
756
+ break;
315
757
  }
316
758
  }
317
759
  }
318
760
 
319
761
  customElements.define('pc-asset', AssetElement);
320
762
 
321
- declare global {
322
- interface HTMLElementTagNameMap {
323
- 'pc-asset': AssetElement;
324
- }
325
- }
326
-
327
763
  export { AssetElement };