@playcanvas/web-components 0.13.0 → 0.14.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.
- package/dist/app.d.cts +35 -0
- package/dist/app.d.ts +35 -0
- package/dist/asset.d.cts +7 -1
- package/dist/asset.d.ts +7 -1
- package/dist/colors.d.cts +1 -1
- package/dist/colors.d.ts +1 -1
- package/dist/custom-elements.json +114 -1
- package/dist/entity-base.d.cts +1 -7
- package/dist/entity-base.d.ts +1 -7
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/loading-bar.d.cts +1 -35
- package/dist/loading-bar.d.ts +1 -35
- package/dist/material.d.cts +13 -0
- package/dist/material.d.ts +13 -0
- package/dist/model.d.cts +71 -0
- package/dist/model.d.ts +71 -0
- package/dist/node.d.cts +55 -0
- package/dist/node.d.ts +55 -0
- package/dist/parse.d.cts +1 -130
- package/dist/parse.d.ts +1 -130
- package/dist/pwc.cjs +506 -54
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +506 -54
- package/dist/pwc.js.map +1 -1
- package/dist/pwc.min.js +1 -1
- package/dist/pwc.min.js.map +1 -1
- package/dist/pwc.min.mjs +1 -1
- package/dist/pwc.min.mjs.map +1 -1
- package/dist/pwc.mjs +506 -54
- package/dist/pwc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +12 -2
- package/dist/web-types.json +22 -3
- package/package.json +3 -3
- package/src/app.ts +92 -19
- package/src/asset.ts +34 -2
- package/src/colors.ts +5 -0
- package/src/components/button-component.ts +7 -7
- package/src/components/element-component.ts +7 -7
- package/src/components/gsplat-component.ts +3 -3
- package/src/components/particlesystem-component.ts +7 -8
- package/src/components/script-component.ts +2 -2
- package/src/components/sound-slot.ts +2 -2
- package/src/entity-base.ts +3 -3
- package/src/index.ts +2 -0
- package/src/loading-bar.ts +2 -3
- package/src/material.ts +31 -2
- package/src/model.ts +158 -5
- package/src/node.ts +277 -2
- package/src/parse.ts +11 -1
- package/src/sky.ts +2 -3
package/dist/pwc.mjs
CHANGED
|
@@ -182,6 +182,7 @@ const REMOVAL_DELAY_MS = 250;
|
|
|
182
182
|
* All styling is inline, so the library injects no stylesheet. The colors and height resolve CSS
|
|
183
183
|
* custom properties — `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
184
184
|
* `--pc-loading-bar-height` — so a page can theme the bar from `pc-app` or `:root`.
|
|
185
|
+
* @internal
|
|
185
186
|
*/
|
|
186
187
|
class LoadingBar {
|
|
187
188
|
_track;
|
|
@@ -280,6 +281,11 @@ class LoadingBar {
|
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
/**
|
|
285
|
+
* The CSS color keywords, lowercase name to hex value. Read by `parseColor` to accept color
|
|
286
|
+
* names as attribute values.
|
|
287
|
+
* @internal
|
|
288
|
+
*/
|
|
283
289
|
const CSS_COLORS = {
|
|
284
290
|
aliceblue: '#f0f8ff',
|
|
285
291
|
antiquewhite: '#faebd7',
|
|
@@ -455,7 +461,7 @@ const CSS_COLORS = {
|
|
|
455
461
|
* @param value - The value to split.
|
|
456
462
|
* @param count - The required number of components.
|
|
457
463
|
* @returns The parsed components, or `null`.
|
|
458
|
-
* @
|
|
464
|
+
* @internal
|
|
459
465
|
*/
|
|
460
466
|
const parseComponents = (value, count) => {
|
|
461
467
|
const components = value.trim().split(/\s+/).map(Number);
|
|
@@ -486,6 +492,7 @@ const cloneDefault = (value) => {
|
|
|
486
492
|
* @param value - The attribute value to parse (`null` when the attribute is absent).
|
|
487
493
|
* @param defaultValue - The value to use when the attribute is absent or removed.
|
|
488
494
|
* @returns The parsed boolean.
|
|
495
|
+
* @internal
|
|
489
496
|
*/
|
|
490
497
|
const parseBool = (value, defaultValue) => {
|
|
491
498
|
return value === null ? defaultValue : value !== 'false';
|
|
@@ -501,6 +508,7 @@ const parseBool = (value, defaultValue) => {
|
|
|
501
508
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
502
509
|
* @param attribute - The attribute name, used in the warning message.
|
|
503
510
|
* @returns The parsed Color object.
|
|
511
|
+
* @internal
|
|
504
512
|
*/
|
|
505
513
|
const parseColor = (value, defaultValue, attribute) => {
|
|
506
514
|
if (value === null) {
|
|
@@ -542,6 +550,7 @@ const parseColor = (value, defaultValue, attribute) => {
|
|
|
542
550
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
543
551
|
* @param attribute - The attribute name, used in the warning message.
|
|
544
552
|
* @returns The resolved enum name.
|
|
553
|
+
* @internal
|
|
545
554
|
*/
|
|
546
555
|
const parseEnum = (value, valid, defaultValue, attribute) => {
|
|
547
556
|
if (value === null) {
|
|
@@ -563,6 +572,7 @@ const parseEnum = (value, valid, defaultValue, attribute) => {
|
|
|
563
572
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
564
573
|
* @param attribute - The attribute name, used in the warning message.
|
|
565
574
|
* @returns The parsed number.
|
|
575
|
+
* @internal
|
|
566
576
|
*/
|
|
567
577
|
const parseNumber = (value, defaultValue, attribute) => {
|
|
568
578
|
if (value === null) {
|
|
@@ -585,6 +595,7 @@ const parseNumber = (value, defaultValue, attribute) => {
|
|
|
585
595
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
586
596
|
* @param attribute - The attribute name, used in the warning message.
|
|
587
597
|
* @returns The parsed Quat object.
|
|
598
|
+
* @internal
|
|
588
599
|
*/
|
|
589
600
|
const parseQuat = (value, defaultValue, attribute) => {
|
|
590
601
|
if (value === null) {
|
|
@@ -608,6 +619,7 @@ const parseQuat = (value, defaultValue, attribute) => {
|
|
|
608
619
|
* @param value - The attribute value to parse (`null` when the attribute is absent).
|
|
609
620
|
* @param defaultValue - The value to use when the attribute is absent or removed.
|
|
610
621
|
* @returns The parsed tag names.
|
|
622
|
+
* @internal
|
|
611
623
|
*/
|
|
612
624
|
const parseTags = (value, defaultValue = []) => {
|
|
613
625
|
if (value === null) {
|
|
@@ -629,6 +641,7 @@ const parseTags = (value, defaultValue = []) => {
|
|
|
629
641
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
630
642
|
* @param attribute - The attribute name, used in the warning message.
|
|
631
643
|
* @returns The parsed Vec2 object.
|
|
644
|
+
* @internal
|
|
632
645
|
*/
|
|
633
646
|
const parseVec2 = (value, defaultValue, attribute) => {
|
|
634
647
|
if (value === null) {
|
|
@@ -650,6 +663,7 @@ const parseVec2 = (value, defaultValue, attribute) => {
|
|
|
650
663
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
651
664
|
* @param attribute - The attribute name, used in the warning message.
|
|
652
665
|
* @returns The parsed Vec3 object.
|
|
666
|
+
* @internal
|
|
653
667
|
*/
|
|
654
668
|
const parseVec3 = (value, defaultValue, attribute) => {
|
|
655
669
|
if (value === null) {
|
|
@@ -671,6 +685,7 @@ const parseVec3 = (value, defaultValue, attribute) => {
|
|
|
671
685
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
672
686
|
* @param attribute - The attribute name, used in the warning message.
|
|
673
687
|
* @returns The parsed Vec4 object.
|
|
688
|
+
* @internal
|
|
674
689
|
*/
|
|
675
690
|
const parseVec4 = (value, defaultValue, attribute) => {
|
|
676
691
|
if (value === null) {
|
|
@@ -690,6 +705,7 @@ const parseVec4 = (value, defaultValue, attribute) => {
|
|
|
690
705
|
*
|
|
691
706
|
* @param ref - The reference string to resolve.
|
|
692
707
|
* @returns The resolved entity, or `null`.
|
|
708
|
+
* @internal
|
|
693
709
|
*/
|
|
694
710
|
const getEntity = (ref) => {
|
|
695
711
|
if (!ref) {
|
|
@@ -1248,21 +1264,63 @@ class AppElement extends AsyncElement {
|
|
|
1248
1264
|
}
|
|
1249
1265
|
return null;
|
|
1250
1266
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1267
|
+
/**
|
|
1268
|
+
* Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
|
|
1269
|
+
* the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
|
|
1270
|
+
* to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
|
|
1271
|
+
* client coordinates are passed through unmapped and `mapped` is false, so callers know the
|
|
1272
|
+
* coordinates correspond to no real geometry.
|
|
1273
|
+
*
|
|
1274
|
+
* @param event - The pointer event to convert.
|
|
1275
|
+
* @param canvas - The canvas the event was dispatched on.
|
|
1276
|
+
* @returns The buffer-space coordinates, and whether they were actually mapped.
|
|
1277
|
+
*/
|
|
1278
|
+
_getPickerCoordinates(event, canvas) {
|
|
1279
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
1280
|
+
if (canvasRect.width === 0 || canvasRect.height === 0) {
|
|
1281
|
+
return { x: event.clientX, y: event.clientY, mapped: false };
|
|
1282
|
+
}
|
|
1283
|
+
const scaleX = canvas.width / canvasRect.width;
|
|
1284
|
+
const scaleY = canvas.height / canvasRect.height;
|
|
1285
|
+
return {
|
|
1286
|
+
x: (event.clientX - canvasRect.left) * scaleX,
|
|
1287
|
+
y: (event.clientY - canvasRect.top) * scaleY,
|
|
1288
|
+
mapped: true
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Whether a camera's viewport contains the point. A camera renders into its normalized
|
|
1293
|
+
* `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
|
|
1294
|
+
* the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
|
|
1295
|
+
* input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
|
|
1296
|
+
* pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
|
|
1297
|
+
* the viewport whose first pixel it is - never to the one it just left, whose pick buffer
|
|
1298
|
+
* holds nothing there.
|
|
1299
|
+
*
|
|
1300
|
+
* @param camera - The camera to test.
|
|
1301
|
+
* @param x - The x coordinate, in buffer space.
|
|
1302
|
+
* @param y - The y coordinate, in buffer space.
|
|
1303
|
+
* @param canvas - The canvas the coordinates are relative to.
|
|
1304
|
+
* @returns Whether the camera's viewport contains the point.
|
|
1305
|
+
*/
|
|
1306
|
+
_cameraContains(camera, x, y, canvas) {
|
|
1307
|
+
const rect = camera.rect;
|
|
1308
|
+
const left = rect.x * canvas.width;
|
|
1309
|
+
const bottom = (1 - rect.y) * canvas.height;
|
|
1310
|
+
const top = bottom - rect.w * canvas.height;
|
|
1311
|
+
return x >= left && x < left + rect.z * canvas.width && y >= top && y < bottom;
|
|
1262
1312
|
}
|
|
1263
1313
|
/**
|
|
1264
1314
|
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
1265
1315
|
*
|
|
1316
|
+
* The camera is resolved the way the engine's ElementInput resolves it for UI input:
|
|
1317
|
+
* enabled cameras are tried topmost-first (they render in ascending `priority` order),
|
|
1318
|
+
* skipping cameras that render to a texture and cameras whose viewport `rect` does not
|
|
1319
|
+
* contain the pointer. A camera that picks nothing ends the search if it clears the color
|
|
1320
|
+
* buffer - its background visually owns the pixel - and otherwise cedes to the cameras
|
|
1321
|
+
* beneath it, so an overlay camera only intercepts picks where it actually drew something.
|
|
1322
|
+
* The pick buffer is prepared per camera, so each camera picks from its own layers.
|
|
1323
|
+
*
|
|
1266
1324
|
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
1267
1325
|
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
1268
1326
|
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
@@ -1272,16 +1330,40 @@ class AppElement extends AsyncElement {
|
|
|
1272
1330
|
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
1273
1331
|
*/
|
|
1274
1332
|
async _pickNode(event) {
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
this._picker.prepare(camera, this.app.scene);
|
|
1280
|
-
const selection = await this._picker.getSelectionAsync(x, y);
|
|
1281
|
-
if (selection.length === 0)
|
|
1333
|
+
const app = this.app;
|
|
1334
|
+
const picker = this._picker;
|
|
1335
|
+
const canvas = this._canvas;
|
|
1336
|
+
if (!app || !picker || !canvas)
|
|
1282
1337
|
return null;
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1338
|
+
const { x, y, mapped } = this._getPickerCoordinates(event, canvas);
|
|
1339
|
+
// Walked from the end: the array is sorted by ascending priority, so the last camera
|
|
1340
|
+
// renders last and sits on top. Read through .at() because a pick handler may remove
|
|
1341
|
+
// cameras while an earlier iteration's read back is in flight.
|
|
1342
|
+
const cameras = app.systems.camera?.cameras ?? [];
|
|
1343
|
+
for (let i = cameras.length - 1; i >= 0; i--) {
|
|
1344
|
+
const camera = cameras.at(i);
|
|
1345
|
+
// A camera rendering to a texture is not on the canvas.
|
|
1346
|
+
if (!camera || camera.renderTarget)
|
|
1347
|
+
continue;
|
|
1348
|
+
// Coordinates that could not be mapped cannot be tested for containment.
|
|
1349
|
+
if (mapped && !this._cameraContains(camera, x, y, canvas))
|
|
1350
|
+
continue;
|
|
1351
|
+
picker.prepare(camera, app.scene);
|
|
1352
|
+
const selection = await picker.getSelectionAsync(x, y);
|
|
1353
|
+
// The element may have disconnected while the read back was in flight.
|
|
1354
|
+
if (!this._picker || !this.app)
|
|
1355
|
+
return null;
|
|
1356
|
+
if (selection.length > 0) {
|
|
1357
|
+
const item = selection[0];
|
|
1358
|
+
return item instanceof MeshInstance ? item.node : item.entity;
|
|
1359
|
+
}
|
|
1360
|
+
// Nothing hit. A camera that clears the color buffer paints its background over
|
|
1361
|
+
// everything beneath it, so the miss is final; one that does not is an overlay
|
|
1362
|
+
// that the cameras beneath show through, so they get their turn.
|
|
1363
|
+
if (camera.clearColorBuffer)
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
return null;
|
|
1285
1367
|
}
|
|
1286
1368
|
async _onPointerMove(event) {
|
|
1287
1369
|
if (!this._picker || !this.app)
|
|
@@ -1528,7 +1610,7 @@ customElements.define('pc-app', AppElement);
|
|
|
1528
1610
|
/**
|
|
1529
1611
|
* The attribute names of the inline `onpointer*` event handlers, shared by every element that
|
|
1530
1612
|
* fronts an engine entity. Spread into `observedAttributes` by subclasses.
|
|
1531
|
-
* @
|
|
1613
|
+
* @internal
|
|
1532
1614
|
*/
|
|
1533
1615
|
const POINTER_ATTRIBUTES = [
|
|
1534
1616
|
'onpointerenter',
|
|
@@ -2225,6 +2307,10 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
|
|
|
2225
2307
|
* immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
|
|
2226
2308
|
* elsewhere, or with an unsupported asset type, never become ready.
|
|
2227
2309
|
*
|
|
2310
|
+
* A `lazy` asset loads on first use: the first time any element resolves it by `id` — a model,
|
|
2311
|
+
* a material map, a sky, a script `asset:` reference — or when the `lazy` attribute is removed,
|
|
2312
|
+
* whichever comes first. Until then it stays registered and unloaded.
|
|
2313
|
+
*
|
|
2228
2314
|
* For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
|
|
2229
2315
|
* `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
|
|
2230
2316
|
* created and — like `lazy` — are observed: changing one updates a texture that has already
|
|
@@ -2424,7 +2510,9 @@ class AssetElement extends AsyncElement {
|
|
|
2424
2510
|
if (type === 'sprite') {
|
|
2425
2511
|
data = data ?? {};
|
|
2426
2512
|
// Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
|
|
2427
|
-
// declared before the sprite so its asset already exists in the registry.
|
|
2513
|
+
// declared before the sprite so its asset already exists in the registry. Resolved
|
|
2514
|
+
// with get, not useAsset: creation-time wiring is not a use, and the engine's
|
|
2515
|
+
// sprite handler loads the atlas when the sprite itself loads.
|
|
2428
2516
|
const atlas = this.getAttribute('atlas') ?? data.textureAtlasAsset;
|
|
2429
2517
|
if (typeof atlas === 'string') {
|
|
2430
2518
|
const atlasAsset = AssetElement.get(atlas);
|
|
@@ -2585,13 +2673,18 @@ class AssetElement extends AsyncElement {
|
|
|
2585
2673
|
return this._flipY;
|
|
2586
2674
|
}
|
|
2587
2675
|
/**
|
|
2588
|
-
* Sets whether the asset should be loaded lazily.
|
|
2676
|
+
* Sets whether the asset should be loaded lazily. A lazy asset is registered without being
|
|
2677
|
+
* loaded; it loads on first use - the first time any element resolves it by `id` - or when
|
|
2678
|
+
* this flag is cleared on a registered asset, whichever comes first.
|
|
2589
2679
|
* @param value - The lazy loading flag.
|
|
2590
2680
|
*/
|
|
2591
2681
|
set lazy(value) {
|
|
2592
2682
|
this._lazy = value;
|
|
2593
2683
|
if (this.asset) {
|
|
2594
2684
|
this.asset.preload = !value;
|
|
2685
|
+
if (!value) {
|
|
2686
|
+
this.asset.registry?.load(this.asset);
|
|
2687
|
+
}
|
|
2595
2688
|
}
|
|
2596
2689
|
}
|
|
2597
2690
|
/**
|
|
@@ -2775,6 +2868,26 @@ class AssetElement extends AsyncElement {
|
|
|
2775
2868
|
}
|
|
2776
2869
|
}
|
|
2777
2870
|
customElements.define('pc-asset', AssetElement);
|
|
2871
|
+
/**
|
|
2872
|
+
* Resolves an asset reference for use: {@link AssetElement.get}, plus starting the load of a
|
|
2873
|
+
* registered asset that has not begun one - a `lazy` asset. Every element that consumes assets
|
|
2874
|
+
* resolves its references here, which is what makes `lazy` mean load on first use without any
|
|
2875
|
+
* consumer having to remember the load. The load is asynchronous - callers observe the asset's
|
|
2876
|
+
* `load` event for the resource.
|
|
2877
|
+
*
|
|
2878
|
+
* @param id - The `id` of the `<pc-asset>` element.
|
|
2879
|
+
* @returns The asset, or `undefined`.
|
|
2880
|
+
* @internal
|
|
2881
|
+
*/
|
|
2882
|
+
const useAsset = (id) => {
|
|
2883
|
+
const asset = AssetElement.get(id);
|
|
2884
|
+
// load() ignores an asset that is already loaded or loading, so repeated resolution
|
|
2885
|
+
// costs nothing.
|
|
2886
|
+
if (asset) {
|
|
2887
|
+
asset.registry?.load(asset);
|
|
2888
|
+
}
|
|
2889
|
+
return asset;
|
|
2890
|
+
};
|
|
2778
2891
|
|
|
2779
2892
|
/**
|
|
2780
2893
|
* Represents a component in the PlayCanvas engine.
|
|
@@ -3058,15 +3171,15 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
3058
3171
|
if (imageEntity) {
|
|
3059
3172
|
data.imageEntity = imageEntity;
|
|
3060
3173
|
}
|
|
3061
|
-
const hoverSpriteAsset =
|
|
3174
|
+
const hoverSpriteAsset = useAsset(this._hoverSpriteAsset);
|
|
3062
3175
|
if (hoverSpriteAsset) {
|
|
3063
3176
|
data.hoverSpriteAsset = hoverSpriteAsset.id;
|
|
3064
3177
|
}
|
|
3065
|
-
const pressedSpriteAsset =
|
|
3178
|
+
const pressedSpriteAsset = useAsset(this._pressedSpriteAsset);
|
|
3066
3179
|
if (pressedSpriteAsset) {
|
|
3067
3180
|
data.pressedSpriteAsset = pressedSpriteAsset.id;
|
|
3068
3181
|
}
|
|
3069
|
-
const inactiveSpriteAsset =
|
|
3182
|
+
const inactiveSpriteAsset = useAsset(this._inactiveSpriteAsset);
|
|
3070
3183
|
if (inactiveSpriteAsset) {
|
|
3071
3184
|
data.inactiveSpriteAsset = inactiveSpriteAsset.id;
|
|
3072
3185
|
}
|
|
@@ -3228,7 +3341,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
3228
3341
|
*/
|
|
3229
3342
|
set hoverSpriteAsset(value) {
|
|
3230
3343
|
this._hoverSpriteAsset = value;
|
|
3231
|
-
const asset =
|
|
3344
|
+
const asset = useAsset(value);
|
|
3232
3345
|
if (this.component && asset) {
|
|
3233
3346
|
this.component.hoverSpriteAsset = asset.id;
|
|
3234
3347
|
}
|
|
@@ -3264,7 +3377,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
3264
3377
|
*/
|
|
3265
3378
|
set pressedSpriteAsset(value) {
|
|
3266
3379
|
this._pressedSpriteAsset = value;
|
|
3267
|
-
const asset =
|
|
3380
|
+
const asset = useAsset(value);
|
|
3268
3381
|
if (this.component && asset) {
|
|
3269
3382
|
this.component.pressedSpriteAsset = asset.id;
|
|
3270
3383
|
}
|
|
@@ -3300,7 +3413,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
3300
3413
|
*/
|
|
3301
3414
|
set inactiveSpriteAsset(value) {
|
|
3302
3415
|
this._inactiveSpriteAsset = value;
|
|
3303
|
-
const asset =
|
|
3416
|
+
const asset = useAsset(value);
|
|
3304
3417
|
if (this.component && asset) {
|
|
3305
3418
|
this.component.inactiveSpriteAsset = asset.id;
|
|
3306
3419
|
}
|
|
@@ -4158,15 +4271,15 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4158
4271
|
};
|
|
4159
4272
|
// Asset references are resolved from `<pc-asset>` element ids to engine asset ids. They are
|
|
4160
4273
|
// only included when they resolve, so image/group elements (with no font) don't error.
|
|
4161
|
-
const fontAsset =
|
|
4274
|
+
const fontAsset = useAsset(this._fontAsset);
|
|
4162
4275
|
if (fontAsset) {
|
|
4163
4276
|
data.fontAsset = fontAsset.id;
|
|
4164
4277
|
}
|
|
4165
|
-
const spriteAsset =
|
|
4278
|
+
const spriteAsset = useAsset(this._spriteAsset);
|
|
4166
4279
|
if (spriteAsset) {
|
|
4167
4280
|
data.spriteAsset = spriteAsset.id;
|
|
4168
4281
|
}
|
|
4169
|
-
const textureAsset =
|
|
4282
|
+
const textureAsset = useAsset(this._textureAsset);
|
|
4170
4283
|
if (textureAsset) {
|
|
4171
4284
|
data.textureAsset = textureAsset.id;
|
|
4172
4285
|
}
|
|
@@ -4280,7 +4393,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4280
4393
|
*/
|
|
4281
4394
|
set fontAsset(value) {
|
|
4282
4395
|
this._fontAsset = value;
|
|
4283
|
-
const asset =
|
|
4396
|
+
const asset = useAsset(value);
|
|
4284
4397
|
if (this.component && asset) {
|
|
4285
4398
|
this.component.fontAsset = asset.id;
|
|
4286
4399
|
}
|
|
@@ -4435,7 +4548,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4435
4548
|
*/
|
|
4436
4549
|
set spriteAsset(value) {
|
|
4437
4550
|
this._spriteAsset = value;
|
|
4438
|
-
const asset =
|
|
4551
|
+
const asset = useAsset(value);
|
|
4439
4552
|
if (this.component && asset) {
|
|
4440
4553
|
this.component.spriteAsset = asset.id;
|
|
4441
4554
|
}
|
|
@@ -4487,7 +4600,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4487
4600
|
*/
|
|
4488
4601
|
set textureAsset(value) {
|
|
4489
4602
|
this._textureAsset = value;
|
|
4490
|
-
const asset =
|
|
4603
|
+
const asset = useAsset(value);
|
|
4491
4604
|
if (this.component && asset) {
|
|
4492
4605
|
this.component.textureAsset = asset.id;
|
|
4493
4606
|
}
|
|
@@ -5722,13 +5835,14 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
5722
5835
|
super('particlesystem');
|
|
5723
5836
|
}
|
|
5724
5837
|
getInitialComponentData() {
|
|
5725
|
-
const asset =
|
|
5726
|
-
|
|
5838
|
+
const asset = useAsset(this._asset);
|
|
5839
|
+
// A lazy config has no resource yet - _loadAsset applies it once the load completes
|
|
5840
|
+
if (!asset || !asset.resource) {
|
|
5727
5841
|
return {};
|
|
5728
5842
|
}
|
|
5729
5843
|
if (asset.resource.colorMapAsset) {
|
|
5730
5844
|
const id = asset.resource.colorMapAsset;
|
|
5731
|
-
const colorMapAsset =
|
|
5845
|
+
const colorMapAsset = useAsset(id)?.id;
|
|
5732
5846
|
if (colorMapAsset) {
|
|
5733
5847
|
asset.resource.colorMapAsset = colorMapAsset;
|
|
5734
5848
|
}
|
|
@@ -5754,9 +5868,8 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
5754
5868
|
}
|
|
5755
5869
|
}
|
|
5756
5870
|
async _loadAsset() {
|
|
5757
|
-
|
|
5758
|
-
const
|
|
5759
|
-
const asset = AssetElement.get(this._asset);
|
|
5871
|
+
await this.closestApp?.ready();
|
|
5872
|
+
const asset = useAsset(this._asset);
|
|
5760
5873
|
if (!asset) {
|
|
5761
5874
|
return;
|
|
5762
5875
|
}
|
|
@@ -5767,7 +5880,6 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
5767
5880
|
asset.once('load', () => {
|
|
5768
5881
|
this.applyConfig(asset.resource);
|
|
5769
5882
|
});
|
|
5770
|
-
app.assets.load(asset);
|
|
5771
5883
|
}
|
|
5772
5884
|
}
|
|
5773
5885
|
/**
|
|
@@ -5959,6 +6071,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5959
6071
|
_metalnessMapRotation = 0;
|
|
5960
6072
|
_metalnessMapTiling = new Vec2(1, 1);
|
|
5961
6073
|
_metalnessMapUv = 0;
|
|
6074
|
+
_name = 'Untitled';
|
|
5962
6075
|
_normalMap = '';
|
|
5963
6076
|
_normalMapOffset = new Vec2(0, 0);
|
|
5964
6077
|
_normalMapRotation = 0;
|
|
@@ -6076,6 +6189,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6076
6189
|
material.metalnessMapRotation = this._metalnessMapRotation;
|
|
6077
6190
|
material.metalnessMapTiling = this._metalnessMapTiling;
|
|
6078
6191
|
material.metalnessMapUv = this._metalnessMapUv;
|
|
6192
|
+
material.name = this._name;
|
|
6079
6193
|
material.normalMapOffset = this._normalMapOffset;
|
|
6080
6194
|
material.normalMapRotation = this._normalMapRotation;
|
|
6081
6195
|
material.normalMapTiling = this._normalMapTiling;
|
|
@@ -6183,7 +6297,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6183
6297
|
this._scheduleUpdate();
|
|
6184
6298
|
return;
|
|
6185
6299
|
}
|
|
6186
|
-
const asset =
|
|
6300
|
+
const asset = useAsset(id);
|
|
6187
6301
|
if (!asset)
|
|
6188
6302
|
return;
|
|
6189
6303
|
if (asset.loaded) {
|
|
@@ -7162,6 +7276,26 @@ class MaterialElement extends HTMLElement {
|
|
|
7162
7276
|
get metalnessMapUv() {
|
|
7163
7277
|
return this._metalnessMapUv;
|
|
7164
7278
|
}
|
|
7279
|
+
/**
|
|
7280
|
+
* Sets the name of the material.
|
|
7281
|
+
* @param value - The material name.
|
|
7282
|
+
*/
|
|
7283
|
+
set name(value) {
|
|
7284
|
+
this._name = value;
|
|
7285
|
+
if (this.material) {
|
|
7286
|
+
// A label rather than shader state, so no update() is scheduled
|
|
7287
|
+
this.material.name = value;
|
|
7288
|
+
}
|
|
7289
|
+
}
|
|
7290
|
+
/**
|
|
7291
|
+
* Gets the name of the material - the label shown wherever materials surface by name, such
|
|
7292
|
+
* as profilers, GPU captures and the assignments `pc-model.hierarchy()` reports. Purely a
|
|
7293
|
+
* label: element references resolve through `id`.
|
|
7294
|
+
* @returns The material name.
|
|
7295
|
+
*/
|
|
7296
|
+
get name() {
|
|
7297
|
+
return this._name;
|
|
7298
|
+
}
|
|
7165
7299
|
/**
|
|
7166
7300
|
* Sets the id of the `pc-asset` to use as the normal map.
|
|
7167
7301
|
* @param value - The asset id.
|
|
@@ -7733,6 +7867,7 @@ class MaterialElement extends HTMLElement {
|
|
|
7733
7867
|
'metalness-map-rotation',
|
|
7734
7868
|
'metalness-map-tiling',
|
|
7735
7869
|
'metalness-map-uv',
|
|
7870
|
+
'name',
|
|
7736
7871
|
'normal-map',
|
|
7737
7872
|
'normal-map-offset',
|
|
7738
7873
|
'normal-map-rotation',
|
|
@@ -7933,6 +8068,9 @@ class MaterialElement extends HTMLElement {
|
|
|
7933
8068
|
case 'metalness-map-uv':
|
|
7934
8069
|
this.metalnessMapUv = parseNumber(newValue, 0, name);
|
|
7935
8070
|
break;
|
|
8071
|
+
case 'name':
|
|
8072
|
+
this.name = newValue ?? 'Untitled';
|
|
8073
|
+
break;
|
|
7936
8074
|
case 'normal-map':
|
|
7937
8075
|
this.normalMap = newValue ?? '';
|
|
7938
8076
|
break;
|
|
@@ -9284,7 +9422,7 @@ const camelToKebab = (name) => {
|
|
|
9284
9422
|
* @returns The asset, or `raw`.
|
|
9285
9423
|
*/
|
|
9286
9424
|
const assetConversion = (rest, raw) => {
|
|
9287
|
-
const asset =
|
|
9425
|
+
const asset = useAsset(rest);
|
|
9288
9426
|
if (asset) {
|
|
9289
9427
|
return asset;
|
|
9290
9428
|
}
|
|
@@ -10104,7 +10242,7 @@ class SoundSlotElement extends AsyncElement {
|
|
|
10104
10242
|
set asset(value) {
|
|
10105
10243
|
this._asset = value;
|
|
10106
10244
|
if (this.soundSlot) {
|
|
10107
|
-
const id =
|
|
10245
|
+
const id = useAsset(value)?.id;
|
|
10108
10246
|
if (id) {
|
|
10109
10247
|
this.soundSlot.asset = id;
|
|
10110
10248
|
}
|
|
@@ -10311,7 +10449,7 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
10311
10449
|
}
|
|
10312
10450
|
getInitialComponentData() {
|
|
10313
10451
|
return {
|
|
10314
|
-
asset:
|
|
10452
|
+
asset: useAsset(this._asset),
|
|
10315
10453
|
castShadows: this._castShadows,
|
|
10316
10454
|
lodBaseDistance: this._lodBaseDistance,
|
|
10317
10455
|
lodMultiplier: this._lodMultiplier,
|
|
@@ -10332,7 +10470,7 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
10332
10470
|
*/
|
|
10333
10471
|
set asset(value) {
|
|
10334
10472
|
this._asset = value;
|
|
10335
|
-
const asset =
|
|
10473
|
+
const asset = useAsset(value);
|
|
10336
10474
|
if (this.component && asset) {
|
|
10337
10475
|
this.component.asset = asset;
|
|
10338
10476
|
}
|
|
@@ -10478,6 +10616,41 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
10478
10616
|
}
|
|
10479
10617
|
customElements.define('pc-gsplat', GSplatComponentElement);
|
|
10480
10618
|
|
|
10619
|
+
/**
|
|
10620
|
+
* Formats one line of the printable hierarchy: the node's name, an `[index]` marker when the
|
|
10621
|
+
* name is shared by several nodes in the model, the attached component types, and the material
|
|
10622
|
+
* names of a render component.
|
|
10623
|
+
*
|
|
10624
|
+
* @param node - The node to format.
|
|
10625
|
+
* @param counts - The number of nodes bearing each name.
|
|
10626
|
+
* @returns The formatted line.
|
|
10627
|
+
*/
|
|
10628
|
+
const formatNode = (node, counts) => {
|
|
10629
|
+
const index = (counts.get(node.name) ?? 0) > 1 ? ` [${node.index}]` : '';
|
|
10630
|
+
const components = node.components.length > 0 ? ` (${node.components.join(', ')})` : '';
|
|
10631
|
+
// Braces rather than brackets: `[N]` already means a match index on this line
|
|
10632
|
+
const materials = node.materials.length > 0 ? ` {${node.materials.map((slot) => slot.name ?? 'null').join(', ')}}` : '';
|
|
10633
|
+
return `${node.name}${index}${components}${materials}`;
|
|
10634
|
+
};
|
|
10635
|
+
/**
|
|
10636
|
+
* Formats the printable form of a hierarchy subtree.
|
|
10637
|
+
*
|
|
10638
|
+
* @param root - The subtree root.
|
|
10639
|
+
* @param counts - The number of nodes bearing each name.
|
|
10640
|
+
* @returns The tree, one line per node.
|
|
10641
|
+
*/
|
|
10642
|
+
const formatHierarchy = (root, counts) => {
|
|
10643
|
+
const lines = [formatNode(root, counts)];
|
|
10644
|
+
const walk = (node, prefix) => {
|
|
10645
|
+
node.children.forEach((child, i) => {
|
|
10646
|
+
const last = i === node.children.length - 1;
|
|
10647
|
+
lines.push(`${prefix}${last ? '└─ ' : '├─ '}${formatNode(child, counts)}`);
|
|
10648
|
+
walk(child, `${prefix}${last ? ' ' : '│ '}`);
|
|
10649
|
+
});
|
|
10650
|
+
};
|
|
10651
|
+
walk(root, '');
|
|
10652
|
+
return lines.join('\n');
|
|
10653
|
+
};
|
|
10481
10654
|
/**
|
|
10482
10655
|
* The ModelElement interface provides properties and methods for manipulating
|
|
10483
10656
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
|
|
@@ -10524,6 +10697,57 @@ class ModelElement extends AsyncElement {
|
|
|
10524
10697
|
get entity() {
|
|
10525
10698
|
return this._entity;
|
|
10526
10699
|
}
|
|
10700
|
+
/**
|
|
10701
|
+
* Returns a snapshot of the instantiated node tree, or `null` while there is none (the
|
|
10702
|
+
* container asset has not loaded, or the element has left the document). One call grounds a
|
|
10703
|
+
* session — a browser console, a test, an agent — in the vocabulary `pc-node` binding
|
|
10704
|
+
* resolves against: the instantiated names ({@link HierarchyNode.name}), paths, match
|
|
10705
|
+
* indices, attached component types and the material assignments of render components
|
|
10706
|
+
* ({@link HierarchyNode.materials}). `String(...)` of the result, or of any node in it,
|
|
10707
|
+
* is the printable form.
|
|
10708
|
+
*
|
|
10709
|
+
* The snapshot is plain data, computed afresh each call: it does not follow later changes
|
|
10710
|
+
* to the hierarchy, and mutating it changes nothing.
|
|
10711
|
+
*
|
|
10712
|
+
* @returns The root of the instantiated node tree, or `null`.
|
|
10713
|
+
*/
|
|
10714
|
+
hierarchy() {
|
|
10715
|
+
const root = this._entity;
|
|
10716
|
+
if (!root) {
|
|
10717
|
+
return null;
|
|
10718
|
+
}
|
|
10719
|
+
// Ordinals are assigned in the traversal resolution searches — pre-order depth-first
|
|
10720
|
+
// from the model root, the root itself included — so each node's index is exactly what
|
|
10721
|
+
// a pc-node's index attribute selects. Once the walk completes, the map holds the total
|
|
10722
|
+
// count per name, which is what the printable form reads to annotate only shared names.
|
|
10723
|
+
const ordinals = new Map();
|
|
10724
|
+
const describe = (entity, pathBelowRoot) => {
|
|
10725
|
+
const index = ordinals.get(entity.name) ?? 0;
|
|
10726
|
+
ordinals.set(entity.name, index + 1);
|
|
10727
|
+
const node = {
|
|
10728
|
+
name: entity.name,
|
|
10729
|
+
// The root has no path below itself; its own name stands in, as it does for
|
|
10730
|
+
// the path a pc-node bound to the root reports.
|
|
10731
|
+
path: pathBelowRoot || entity.name,
|
|
10732
|
+
index,
|
|
10733
|
+
// A plain GraphNode grafted into the hierarchy has no component storage
|
|
10734
|
+
components: Object.keys(entity.c ?? {}).sort(),
|
|
10735
|
+
materials: (entity.render?.meshInstances ?? []).map((meshInstance, slot) => ({
|
|
10736
|
+
index: slot,
|
|
10737
|
+
name: meshInstance.material?.name ?? null
|
|
10738
|
+
})),
|
|
10739
|
+
children: entity.children.map((child) => describe(child, pathBelowRoot ? `${pathBelowRoot}/${child.name}` : child.name))
|
|
10740
|
+
};
|
|
10741
|
+
// Non-enumerable, keeping the snapshot plain data under JSON.stringify, spreads and
|
|
10742
|
+
// key enumeration. Deferred to call time, by which the ordinal map holds its totals.
|
|
10743
|
+
Object.defineProperty(node, 'toString', {
|
|
10744
|
+
enumerable: false,
|
|
10745
|
+
value: () => formatHierarchy(node, ordinals)
|
|
10746
|
+
});
|
|
10747
|
+
return node;
|
|
10748
|
+
};
|
|
10749
|
+
return describe(root, '');
|
|
10750
|
+
}
|
|
10527
10751
|
connectedCallback() {
|
|
10528
10752
|
// A model outside an application is inert and never becomes ready, so awaiting it hangs.
|
|
10529
10753
|
// Warn rather than fail silently, naming the parent it requires, as every other misplaced
|
|
@@ -10610,8 +10834,7 @@ class ModelElement extends AsyncElement {
|
|
|
10610
10834
|
if (generation !== this._loadGeneration) {
|
|
10611
10835
|
return;
|
|
10612
10836
|
}
|
|
10613
|
-
const
|
|
10614
|
-
const asset = AssetElement.get(this._asset);
|
|
10837
|
+
const asset = useAsset(this._asset);
|
|
10615
10838
|
if (!asset) {
|
|
10616
10839
|
// An empty id is a legitimate transient (the asset may be assigned later); a
|
|
10617
10840
|
// non-empty one that resolves to nothing is a dead end - say so rather than staying
|
|
@@ -10647,7 +10870,6 @@ class ModelElement extends AsyncElement {
|
|
|
10647
10870
|
}));
|
|
10648
10871
|
this._onReady();
|
|
10649
10872
|
});
|
|
10650
|
-
app.assets.load(asset);
|
|
10651
10873
|
}
|
|
10652
10874
|
}
|
|
10653
10875
|
_unloadModel() {
|
|
@@ -10684,6 +10906,74 @@ class ModelElement extends AsyncElement {
|
|
|
10684
10906
|
}
|
|
10685
10907
|
customElements.define('pc-model', ModelElement);
|
|
10686
10908
|
|
|
10909
|
+
/**
|
|
10910
|
+
* Parses one mapping into its valid rules, warning for each entry that is not one: an unknown
|
|
10911
|
+
* or missing selector prefix, an empty `name:` value, an `index:` value that is not a
|
|
10912
|
+
* non-negative integer, or a replacement id that is not a non-empty string. An invalid rule
|
|
10913
|
+
* behaves exactly as if absent from the mapping.
|
|
10914
|
+
*
|
|
10915
|
+
* @param overrides - The mapping to parse.
|
|
10916
|
+
* @param label - The element description for warnings.
|
|
10917
|
+
* @returns The valid rules.
|
|
10918
|
+
*/
|
|
10919
|
+
const parseMaterialRules = (overrides, label) => {
|
|
10920
|
+
const rules = [];
|
|
10921
|
+
for (const [selector, id] of Object.entries(overrides)) {
|
|
10922
|
+
if (typeof id !== 'string' || id === '') {
|
|
10923
|
+
console.warn(`${label} material-overrides '${selector}' needs a pc-material id - rule ignored`);
|
|
10924
|
+
}
|
|
10925
|
+
else if (selector.startsWith('name:')) {
|
|
10926
|
+
// The text after the prefix is the selector value, exactly as written - a material
|
|
10927
|
+
// name may legitimately begin or end with whitespace
|
|
10928
|
+
const name = selector.slice('name:'.length);
|
|
10929
|
+
if (name === '') {
|
|
10930
|
+
console.warn(`${label} material-overrides 'name:' selector is empty - rule ignored`);
|
|
10931
|
+
}
|
|
10932
|
+
else {
|
|
10933
|
+
rules.push({ kind: 'name', name, id });
|
|
10934
|
+
}
|
|
10935
|
+
}
|
|
10936
|
+
else if (selector.startsWith('index:')) {
|
|
10937
|
+
// Whitespace around the number is tolerated; Number('') is 0, so blank means NaN
|
|
10938
|
+
const text = selector.slice('index:'.length).trim();
|
|
10939
|
+
const index = text === '' ? NaN : Number(text);
|
|
10940
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
10941
|
+
console.warn(`${label} material-overrides '${selector}' is not a non-negative integer index - rule ignored`);
|
|
10942
|
+
}
|
|
10943
|
+
else {
|
|
10944
|
+
rules.push({ kind: 'index', index, id });
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10947
|
+
else {
|
|
10948
|
+
console.warn(`${label} material-overrides '${selector}' has no 'name:' or 'index:' prefix - rule ignored`);
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
return rules;
|
|
10952
|
+
};
|
|
10953
|
+
/**
|
|
10954
|
+
* Parses the material-overrides attribute text. Anything but a JSON object — malformed JSON, an
|
|
10955
|
+
* array, a primitive — warns and yields `null`, the absent mapping: a stale mapping must not
|
|
10956
|
+
* survive an attribute value the DOM no longer represents.
|
|
10957
|
+
*
|
|
10958
|
+
* @param text - The attribute text.
|
|
10959
|
+
* @param label - The element description for warnings.
|
|
10960
|
+
* @returns The mapping, or `null`.
|
|
10961
|
+
*/
|
|
10962
|
+
const parseMaterialOverridesAttribute = (text, label) => {
|
|
10963
|
+
let parsed;
|
|
10964
|
+
try {
|
|
10965
|
+
parsed = JSON.parse(text);
|
|
10966
|
+
}
|
|
10967
|
+
catch (error) {
|
|
10968
|
+
console.warn(`${label} material-overrides is not valid JSON - treated as absent: ${error.message}`);
|
|
10969
|
+
return null;
|
|
10970
|
+
}
|
|
10971
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
10972
|
+
console.warn(`${label} material-overrides must be a JSON object - treated as absent`);
|
|
10973
|
+
return null;
|
|
10974
|
+
}
|
|
10975
|
+
return parsed;
|
|
10976
|
+
};
|
|
10687
10977
|
/**
|
|
10688
10978
|
* Computes the Levenshtein distance between two strings, for near-miss suggestions in the
|
|
10689
10979
|
* resolution warnings.
|
|
@@ -10739,6 +11029,13 @@ const levenshtein = (a, b) => {
|
|
|
10739
11029
|
* "x y z" triple.
|
|
10740
11030
|
* @attribute {string} scale - Overrides the node's local scale, as an "x y z" triple.
|
|
10741
11031
|
* @attribute {string} tags - Overrides the node's tags, separated by spaces or commas.
|
|
11032
|
+
* @attribute {string} material-overrides - Overrides material assignments on the bound node's
|
|
11033
|
+
* render component, as a JSON object from selector to `pc-material` id — for example
|
|
11034
|
+
* `{"name:CarPaint": "candy-red", "index:7": "smoked-glass"}`. A `name:X` key selects every mesh
|
|
11035
|
+
* instance whose baseline material is named `X`; an `index:N` key selects mesh instance `N` and
|
|
11036
|
+
* wins over a name rule for the same instance. Assignments no rule matches keep their baseline
|
|
11037
|
+
* materials, and removing the attribute restores all of them. Use `pc-model.hierarchy()` to
|
|
11038
|
+
* discover the names and indices a node offers.
|
|
10742
11039
|
* @attribute {string} onpointerenter - Script to run when the pointer moves onto the node.
|
|
10743
11040
|
* @attribute {string} onpointerleave - Script to run when the pointer moves off the node.
|
|
10744
11041
|
* @attribute {string} onpointermove - Script to run when the pointer moves over the node.
|
|
@@ -10774,12 +11071,25 @@ class NodeElement extends EntityBaseElement {
|
|
|
10774
11071
|
_destroyHandle = null;
|
|
10775
11072
|
/** The authored values displaced by this element's overrides, captured per property. */
|
|
10776
11073
|
_authored = {};
|
|
11074
|
+
/**
|
|
11075
|
+
* The model-authored render component of the bound node, recorded at bind — before child
|
|
11076
|
+
* decorations build — so a render component added later by a child `pc-render` can never
|
|
11077
|
+
* become the override target. `null` when the bound node has none.
|
|
11078
|
+
*/
|
|
11079
|
+
_authoredRender = null;
|
|
11080
|
+
/**
|
|
11081
|
+
* The baseline assignments displaced by the material overrides, captured for every mesh
|
|
11082
|
+
* instance when the first non-empty mapping applies and released when the mapping goes
|
|
11083
|
+
* absent (restoring them) or the binding dissolves.
|
|
11084
|
+
*/
|
|
11085
|
+
_baseline = null;
|
|
10777
11086
|
// Override values. `null` means "no override": the authored value stays in force.
|
|
10778
11087
|
_enabled = null;
|
|
10779
11088
|
_position = null;
|
|
10780
11089
|
_rotation = null;
|
|
10781
11090
|
_scale = null;
|
|
10782
11091
|
_tags = null;
|
|
11092
|
+
_materialOverrides = null;
|
|
10783
11093
|
/**
|
|
10784
11094
|
* The binding state: `pending` until the host instantiates and `name` resolves, `bound`
|
|
10785
11095
|
* once decorated, `missing`/`ambiguous`/`duplicate` when resolution failed (each also
|
|
@@ -10913,6 +11223,7 @@ class NodeElement extends EntityBaseElement {
|
|
|
10913
11223
|
this._destroyHandle = target.once('destroy', this._onEntityDestroy, this);
|
|
10914
11224
|
this._state = 'bound';
|
|
10915
11225
|
this._path = this._pathOf(target, hostEntity);
|
|
11226
|
+
this._authoredRender = target.render ?? null;
|
|
10916
11227
|
this._applyOverrides();
|
|
10917
11228
|
this._onReady();
|
|
10918
11229
|
this._buildChildren();
|
|
@@ -10943,6 +11254,7 @@ class NodeElement extends EntityBaseElement {
|
|
|
10943
11254
|
this._entity = null;
|
|
10944
11255
|
this._path = null;
|
|
10945
11256
|
this._authored = {};
|
|
11257
|
+
this._authoredRender = null;
|
|
10946
11258
|
// Component decorations come off through the same hook the host-ready cycle uses. A
|
|
10947
11259
|
// dissolve that never rebinds fires no ready event, so the sweep is explicit - after
|
|
10948
11260
|
// `_entity` is cleared, so the hook sees a host without an entity.
|
|
@@ -10964,6 +11276,9 @@ class NodeElement extends EntityBaseElement {
|
|
|
10964
11276
|
this._entity = null;
|
|
10965
11277
|
this._path = null;
|
|
10966
11278
|
this._authored = {};
|
|
11279
|
+
this._authoredRender = null;
|
|
11280
|
+
// The mesh instances died with the entity - the capture is dropped, not restored
|
|
11281
|
+
this._baseline = null;
|
|
10967
11282
|
this._state = 'pending';
|
|
10968
11283
|
this._resetReady();
|
|
10969
11284
|
}
|
|
@@ -11005,6 +11320,9 @@ class NodeElement extends EntityBaseElement {
|
|
|
11005
11320
|
if (this._tags !== null) {
|
|
11006
11321
|
this.tags = this._tags;
|
|
11007
11322
|
}
|
|
11323
|
+
if (this._materialOverrides !== null) {
|
|
11324
|
+
this._applyMaterialOverrides();
|
|
11325
|
+
}
|
|
11008
11326
|
}
|
|
11009
11327
|
/**
|
|
11010
11328
|
* Restores every authored value this element's overrides displaced. The override values
|
|
@@ -11030,6 +11348,104 @@ class NodeElement extends EntityBaseElement {
|
|
|
11030
11348
|
entity.tags.add(authored.tags);
|
|
11031
11349
|
}
|
|
11032
11350
|
this._authored = {};
|
|
11351
|
+
this._restoreBaseline();
|
|
11352
|
+
}
|
|
11353
|
+
/**
|
|
11354
|
+
* Applies the material mapping to the authored render component: parse the mapping's valid
|
|
11355
|
+
* rules, capture the baseline on first application, then recompute every assignment from
|
|
11356
|
+
* that baseline - name rules write over it, index rules write over them, so `index:` wins -
|
|
11357
|
+
* and assign whatever changed. An absent mapping, or one with no valid rules, restores the
|
|
11358
|
+
* baseline instead. Called while bound, from `_applyOverrides` and the property setter.
|
|
11359
|
+
*/
|
|
11360
|
+
_applyMaterialOverrides() {
|
|
11361
|
+
const label = `pc-node '${this._name}'`;
|
|
11362
|
+
const rules = this._materialOverrides ? parseMaterialRules(this._materialOverrides, label) : [];
|
|
11363
|
+
if (rules.length === 0) {
|
|
11364
|
+
this._restoreBaseline();
|
|
11365
|
+
return;
|
|
11366
|
+
}
|
|
11367
|
+
if (!this._baseline) {
|
|
11368
|
+
if (!this._authoredRender) {
|
|
11369
|
+
console.warn(`${label} is bound to a node without an authored render component - material-overrides ignored`);
|
|
11370
|
+
return;
|
|
11371
|
+
}
|
|
11372
|
+
this._baseline = this._authoredRender.meshInstances.map((meshInstance) => ({
|
|
11373
|
+
meshInstance,
|
|
11374
|
+
material: meshInstance.material ?? null,
|
|
11375
|
+
name: meshInstance.material?.name ?? null
|
|
11376
|
+
}));
|
|
11377
|
+
}
|
|
11378
|
+
const baseline = this._baseline;
|
|
11379
|
+
/** Resolves a replacement id, warning when it does not resolve. */
|
|
11380
|
+
const resolveReplacement = (id) => {
|
|
11381
|
+
const material = MaterialElement.get(id);
|
|
11382
|
+
if (!material) {
|
|
11383
|
+
console.warn(`${label} material-overrides could not resolve pc-material '${id}' - rule ignored`);
|
|
11384
|
+
}
|
|
11385
|
+
return material ?? null;
|
|
11386
|
+
};
|
|
11387
|
+
// Recompute the whole list from the baseline: name rules write over it, index rules
|
|
11388
|
+
// write over them. Recomputing makes mapping edits order-independent, and a rule whose
|
|
11389
|
+
// replacement does not resolve simply leaves the layer below it in force.
|
|
11390
|
+
const resolved = baseline.map((assignment) => assignment.material);
|
|
11391
|
+
for (const rule of rules) {
|
|
11392
|
+
if (rule.kind !== 'name') {
|
|
11393
|
+
continue;
|
|
11394
|
+
}
|
|
11395
|
+
const material = resolveReplacement(rule.id);
|
|
11396
|
+
if (!material) {
|
|
11397
|
+
continue;
|
|
11398
|
+
}
|
|
11399
|
+
let matched = false;
|
|
11400
|
+
baseline.forEach((assignment, index) => {
|
|
11401
|
+
if (assignment.name === rule.name) {
|
|
11402
|
+
resolved[index] = material;
|
|
11403
|
+
matched = true;
|
|
11404
|
+
}
|
|
11405
|
+
});
|
|
11406
|
+
if (!matched) {
|
|
11407
|
+
const names = baseline.map((assignment) => `'${assignment.name}'`).join(', ');
|
|
11408
|
+
console.warn(`${label} material-overrides 'name:${rule.name}' matches no assignment - ` +
|
|
11409
|
+
`baseline names: ${names || '(none)'}`);
|
|
11410
|
+
}
|
|
11411
|
+
}
|
|
11412
|
+
for (const rule of rules) {
|
|
11413
|
+
if (rule.kind !== 'index') {
|
|
11414
|
+
continue;
|
|
11415
|
+
}
|
|
11416
|
+
if (rule.index >= baseline.length) {
|
|
11417
|
+
console.warn(`${label} material-overrides 'index:${rule.index}' is out of range - ` +
|
|
11418
|
+
`${baseline.length} assignment(s)`);
|
|
11419
|
+
continue;
|
|
11420
|
+
}
|
|
11421
|
+
const material = resolveReplacement(rule.id);
|
|
11422
|
+
if (material) {
|
|
11423
|
+
resolved[rule.index] = material;
|
|
11424
|
+
}
|
|
11425
|
+
}
|
|
11426
|
+
baseline.forEach((assignment, index) => {
|
|
11427
|
+
// The engine setter rebuilds material and shader state even for a redundant write,
|
|
11428
|
+
// so only actual changes are assigned
|
|
11429
|
+
if (assignment.meshInstance.material !== resolved[index]) {
|
|
11430
|
+
assignment.meshInstance.material = resolved[index];
|
|
11431
|
+
}
|
|
11432
|
+
});
|
|
11433
|
+
}
|
|
11434
|
+
/**
|
|
11435
|
+
* Restores every baseline assignment the material overrides displaced and releases the
|
|
11436
|
+
* capture, so the next non-empty mapping captures afresh. Safe to call without a capture.
|
|
11437
|
+
*/
|
|
11438
|
+
_restoreBaseline() {
|
|
11439
|
+
const baseline = this._baseline;
|
|
11440
|
+
if (!baseline) {
|
|
11441
|
+
return;
|
|
11442
|
+
}
|
|
11443
|
+
this._baseline = null;
|
|
11444
|
+
for (const assignment of baseline) {
|
|
11445
|
+
if (assignment.meshInstance.material !== assignment.material) {
|
|
11446
|
+
assignment.meshInstance.material = assignment.material;
|
|
11447
|
+
}
|
|
11448
|
+
}
|
|
11033
11449
|
}
|
|
11034
11450
|
/**
|
|
11035
11451
|
* Renders the path of `node` below `root`, for the `path` property and the resolution
|
|
@@ -11250,8 +11666,41 @@ class NodeElement extends EntityBaseElement {
|
|
|
11250
11666
|
get tags() {
|
|
11251
11667
|
return this._tags;
|
|
11252
11668
|
}
|
|
11669
|
+
/**
|
|
11670
|
+
* Sets the material overrides: a sparse mapping from selector to `pc-material` id, applied
|
|
11671
|
+
* to the bound node's authored render component. A `name:X` key selects every mesh instance
|
|
11672
|
+
* whose baseline material is named `X`; an `index:N` key selects mesh instance `N` and wins
|
|
11673
|
+
* over a name rule for the same instance. Assignments no rule matches keep their baseline
|
|
11674
|
+
* materials. `null` clears the mapping, restoring every baseline assignment.
|
|
11675
|
+
* @param value - The mapping, or `null`.
|
|
11676
|
+
*/
|
|
11677
|
+
set materialOverrides(value) {
|
|
11678
|
+
// Copied and frozen: later caller mutation of the passed object must not silently
|
|
11679
|
+
// disagree with the mapping the element applied
|
|
11680
|
+
this._materialOverrides = value === null ? null : Object.freeze({ ...value });
|
|
11681
|
+
if (this._state === 'bound') {
|
|
11682
|
+
this._applyMaterialOverrides();
|
|
11683
|
+
}
|
|
11684
|
+
}
|
|
11685
|
+
/**
|
|
11686
|
+
* Gets the material overrides.
|
|
11687
|
+
* @returns The mapping, or `null` while no override is set.
|
|
11688
|
+
*/
|
|
11689
|
+
get materialOverrides() {
|
|
11690
|
+
return this._materialOverrides;
|
|
11691
|
+
}
|
|
11253
11692
|
static get observedAttributes() {
|
|
11254
|
-
return [
|
|
11693
|
+
return [
|
|
11694
|
+
'enabled',
|
|
11695
|
+
'index',
|
|
11696
|
+
'material-overrides',
|
|
11697
|
+
'name',
|
|
11698
|
+
'position',
|
|
11699
|
+
'rotation',
|
|
11700
|
+
'scale',
|
|
11701
|
+
'tags',
|
|
11702
|
+
...POINTER_ATTRIBUTES
|
|
11703
|
+
];
|
|
11255
11704
|
}
|
|
11256
11705
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
11257
11706
|
switch (name) {
|
|
@@ -11276,6 +11725,10 @@ class NodeElement extends EntityBaseElement {
|
|
|
11276
11725
|
}
|
|
11277
11726
|
}
|
|
11278
11727
|
break;
|
|
11728
|
+
case 'material-overrides':
|
|
11729
|
+
this.materialOverrides =
|
|
11730
|
+
newValue === null ? null : parseMaterialOverridesAttribute(newValue, `pc-node '${this._name}'`);
|
|
11731
|
+
break;
|
|
11279
11732
|
case 'name':
|
|
11280
11733
|
this.name = newValue ?? '';
|
|
11281
11734
|
break;
|
|
@@ -11612,7 +12065,7 @@ class SkyElement extends AsyncElement {
|
|
|
11612
12065
|
return;
|
|
11613
12066
|
}
|
|
11614
12067
|
this._appElement = appElement;
|
|
11615
|
-
const asset =
|
|
12068
|
+
const asset = useAsset(this._asset);
|
|
11616
12069
|
if (!asset) {
|
|
11617
12070
|
return;
|
|
11618
12071
|
}
|
|
@@ -11631,7 +12084,6 @@ class SkyElement extends AsyncElement {
|
|
|
11631
12084
|
}
|
|
11632
12085
|
this._generateSkybox(asset);
|
|
11633
12086
|
});
|
|
11634
|
-
app.assets.load(asset);
|
|
11635
12087
|
}
|
|
11636
12088
|
}
|
|
11637
12089
|
_unloadSkybox() {
|