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