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