@playcanvas/web-components 0.13.1 → 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/colors.d.cts +1 -1
- package/dist/colors.d.ts +1 -1
- package/dist/custom-elements.json +113 -0
- 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 +362 -3
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +362 -3
- 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 +362 -3
- package/dist/pwc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +11 -1
- package/dist/web-types.json +21 -2
- package/package.json +3 -3
- package/src/colors.ts +5 -0
- package/src/entity-base.ts +3 -3
- package/src/index.ts +2 -0
- package/src/loading-bar.ts +2 -3
- package/src/material.ts +29 -0
- package/src/model.ts +156 -0
- package/src/node.ts +277 -2
- package/src/parse.ts +11 -1
package/dist/pwc.mjs
CHANGED
|
@@ -182,6 +182,7 @@ const REMOVAL_DELAY_MS = 250;
|
|
|
182
182
|
* All styling is inline, so the library injects no stylesheet. The colors and height resolve CSS
|
|
183
183
|
* custom properties — `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
184
184
|
* `--pc-loading-bar-height` — so a page can theme the bar from `pc-app` or `:root`.
|
|
185
|
+
* @internal
|
|
185
186
|
*/
|
|
186
187
|
class LoadingBar {
|
|
187
188
|
_track;
|
|
@@ -280,6 +281,11 @@ class LoadingBar {
|
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
/**
|
|
285
|
+
* The CSS color keywords, lowercase name to hex value. Read by `parseColor` to accept color
|
|
286
|
+
* names as attribute values.
|
|
287
|
+
* @internal
|
|
288
|
+
*/
|
|
283
289
|
const CSS_COLORS = {
|
|
284
290
|
aliceblue: '#f0f8ff',
|
|
285
291
|
antiquewhite: '#faebd7',
|
|
@@ -455,7 +461,7 @@ const CSS_COLORS = {
|
|
|
455
461
|
* @param value - The value to split.
|
|
456
462
|
* @param count - The required number of components.
|
|
457
463
|
* @returns The parsed components, or `null`.
|
|
458
|
-
* @
|
|
464
|
+
* @internal
|
|
459
465
|
*/
|
|
460
466
|
const parseComponents = (value, count) => {
|
|
461
467
|
const components = value.trim().split(/\s+/).map(Number);
|
|
@@ -486,6 +492,7 @@ const cloneDefault = (value) => {
|
|
|
486
492
|
* @param value - The attribute value to parse (`null` when the attribute is absent).
|
|
487
493
|
* @param defaultValue - The value to use when the attribute is absent or removed.
|
|
488
494
|
* @returns The parsed boolean.
|
|
495
|
+
* @internal
|
|
489
496
|
*/
|
|
490
497
|
const parseBool = (value, defaultValue) => {
|
|
491
498
|
return value === null ? defaultValue : value !== 'false';
|
|
@@ -501,6 +508,7 @@ const parseBool = (value, defaultValue) => {
|
|
|
501
508
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
502
509
|
* @param attribute - The attribute name, used in the warning message.
|
|
503
510
|
* @returns The parsed Color object.
|
|
511
|
+
* @internal
|
|
504
512
|
*/
|
|
505
513
|
const parseColor = (value, defaultValue, attribute) => {
|
|
506
514
|
if (value === null) {
|
|
@@ -542,6 +550,7 @@ const parseColor = (value, defaultValue, attribute) => {
|
|
|
542
550
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
543
551
|
* @param attribute - The attribute name, used in the warning message.
|
|
544
552
|
* @returns The resolved enum name.
|
|
553
|
+
* @internal
|
|
545
554
|
*/
|
|
546
555
|
const parseEnum = (value, valid, defaultValue, attribute) => {
|
|
547
556
|
if (value === null) {
|
|
@@ -563,6 +572,7 @@ const parseEnum = (value, valid, defaultValue, attribute) => {
|
|
|
563
572
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
564
573
|
* @param attribute - The attribute name, used in the warning message.
|
|
565
574
|
* @returns The parsed number.
|
|
575
|
+
* @internal
|
|
566
576
|
*/
|
|
567
577
|
const parseNumber = (value, defaultValue, attribute) => {
|
|
568
578
|
if (value === null) {
|
|
@@ -585,6 +595,7 @@ const parseNumber = (value, defaultValue, attribute) => {
|
|
|
585
595
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
586
596
|
* @param attribute - The attribute name, used in the warning message.
|
|
587
597
|
* @returns The parsed Quat object.
|
|
598
|
+
* @internal
|
|
588
599
|
*/
|
|
589
600
|
const parseQuat = (value, defaultValue, attribute) => {
|
|
590
601
|
if (value === null) {
|
|
@@ -608,6 +619,7 @@ const parseQuat = (value, defaultValue, attribute) => {
|
|
|
608
619
|
* @param value - The attribute value to parse (`null` when the attribute is absent).
|
|
609
620
|
* @param defaultValue - The value to use when the attribute is absent or removed.
|
|
610
621
|
* @returns The parsed tag names.
|
|
622
|
+
* @internal
|
|
611
623
|
*/
|
|
612
624
|
const parseTags = (value, defaultValue = []) => {
|
|
613
625
|
if (value === null) {
|
|
@@ -629,6 +641,7 @@ const parseTags = (value, defaultValue = []) => {
|
|
|
629
641
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
630
642
|
* @param attribute - The attribute name, used in the warning message.
|
|
631
643
|
* @returns The parsed Vec2 object.
|
|
644
|
+
* @internal
|
|
632
645
|
*/
|
|
633
646
|
const parseVec2 = (value, defaultValue, attribute) => {
|
|
634
647
|
if (value === null) {
|
|
@@ -650,6 +663,7 @@ const parseVec2 = (value, defaultValue, attribute) => {
|
|
|
650
663
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
651
664
|
* @param attribute - The attribute name, used in the warning message.
|
|
652
665
|
* @returns The parsed Vec3 object.
|
|
666
|
+
* @internal
|
|
653
667
|
*/
|
|
654
668
|
const parseVec3 = (value, defaultValue, attribute) => {
|
|
655
669
|
if (value === null) {
|
|
@@ -671,6 +685,7 @@ const parseVec3 = (value, defaultValue, attribute) => {
|
|
|
671
685
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
672
686
|
* @param attribute - The attribute name, used in the warning message.
|
|
673
687
|
* @returns The parsed Vec4 object.
|
|
688
|
+
* @internal
|
|
674
689
|
*/
|
|
675
690
|
const parseVec4 = (value, defaultValue, attribute) => {
|
|
676
691
|
if (value === null) {
|
|
@@ -690,6 +705,7 @@ const parseVec4 = (value, defaultValue, attribute) => {
|
|
|
690
705
|
*
|
|
691
706
|
* @param ref - The reference string to resolve.
|
|
692
707
|
* @returns The resolved entity, or `null`.
|
|
708
|
+
* @internal
|
|
693
709
|
*/
|
|
694
710
|
const getEntity = (ref) => {
|
|
695
711
|
if (!ref) {
|
|
@@ -1594,7 +1610,7 @@ customElements.define('pc-app', AppElement);
|
|
|
1594
1610
|
/**
|
|
1595
1611
|
* The attribute names of the inline `onpointer*` event handlers, shared by every element that
|
|
1596
1612
|
* fronts an engine entity. Spread into `observedAttributes` by subclasses.
|
|
1597
|
-
* @
|
|
1613
|
+
* @internal
|
|
1598
1614
|
*/
|
|
1599
1615
|
const POINTER_ATTRIBUTES = [
|
|
1600
1616
|
'onpointerenter',
|
|
@@ -6055,6 +6071,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6055
6071
|
_metalnessMapRotation = 0;
|
|
6056
6072
|
_metalnessMapTiling = new Vec2(1, 1);
|
|
6057
6073
|
_metalnessMapUv = 0;
|
|
6074
|
+
_name = 'Untitled';
|
|
6058
6075
|
_normalMap = '';
|
|
6059
6076
|
_normalMapOffset = new Vec2(0, 0);
|
|
6060
6077
|
_normalMapRotation = 0;
|
|
@@ -6172,6 +6189,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6172
6189
|
material.metalnessMapRotation = this._metalnessMapRotation;
|
|
6173
6190
|
material.metalnessMapTiling = this._metalnessMapTiling;
|
|
6174
6191
|
material.metalnessMapUv = this._metalnessMapUv;
|
|
6192
|
+
material.name = this._name;
|
|
6175
6193
|
material.normalMapOffset = this._normalMapOffset;
|
|
6176
6194
|
material.normalMapRotation = this._normalMapRotation;
|
|
6177
6195
|
material.normalMapTiling = this._normalMapTiling;
|
|
@@ -7258,6 +7276,26 @@ class MaterialElement extends HTMLElement {
|
|
|
7258
7276
|
get metalnessMapUv() {
|
|
7259
7277
|
return this._metalnessMapUv;
|
|
7260
7278
|
}
|
|
7279
|
+
/**
|
|
7280
|
+
* Sets the name of the material.
|
|
7281
|
+
* @param value - The material name.
|
|
7282
|
+
*/
|
|
7283
|
+
set name(value) {
|
|
7284
|
+
this._name = value;
|
|
7285
|
+
if (this.material) {
|
|
7286
|
+
// A label rather than shader state, so no update() is scheduled
|
|
7287
|
+
this.material.name = value;
|
|
7288
|
+
}
|
|
7289
|
+
}
|
|
7290
|
+
/**
|
|
7291
|
+
* Gets the name of the material - the label shown wherever materials surface by name, such
|
|
7292
|
+
* as profilers, GPU captures and the assignments `pc-model.hierarchy()` reports. Purely a
|
|
7293
|
+
* label: element references resolve through `id`.
|
|
7294
|
+
* @returns The material name.
|
|
7295
|
+
*/
|
|
7296
|
+
get name() {
|
|
7297
|
+
return this._name;
|
|
7298
|
+
}
|
|
7261
7299
|
/**
|
|
7262
7300
|
* Sets the id of the `pc-asset` to use as the normal map.
|
|
7263
7301
|
* @param value - The asset id.
|
|
@@ -7829,6 +7867,7 @@ class MaterialElement extends HTMLElement {
|
|
|
7829
7867
|
'metalness-map-rotation',
|
|
7830
7868
|
'metalness-map-tiling',
|
|
7831
7869
|
'metalness-map-uv',
|
|
7870
|
+
'name',
|
|
7832
7871
|
'normal-map',
|
|
7833
7872
|
'normal-map-offset',
|
|
7834
7873
|
'normal-map-rotation',
|
|
@@ -8029,6 +8068,9 @@ class MaterialElement extends HTMLElement {
|
|
|
8029
8068
|
case 'metalness-map-uv':
|
|
8030
8069
|
this.metalnessMapUv = parseNumber(newValue, 0, name);
|
|
8031
8070
|
break;
|
|
8071
|
+
case 'name':
|
|
8072
|
+
this.name = newValue ?? 'Untitled';
|
|
8073
|
+
break;
|
|
8032
8074
|
case 'normal-map':
|
|
8033
8075
|
this.normalMap = newValue ?? '';
|
|
8034
8076
|
break;
|
|
@@ -10574,6 +10616,41 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
10574
10616
|
}
|
|
10575
10617
|
customElements.define('pc-gsplat', GSplatComponentElement);
|
|
10576
10618
|
|
|
10619
|
+
/**
|
|
10620
|
+
* Formats one line of the printable hierarchy: the node's name, an `[index]` marker when the
|
|
10621
|
+
* name is shared by several nodes in the model, the attached component types, and the material
|
|
10622
|
+
* names of a render component.
|
|
10623
|
+
*
|
|
10624
|
+
* @param node - The node to format.
|
|
10625
|
+
* @param counts - The number of nodes bearing each name.
|
|
10626
|
+
* @returns The formatted line.
|
|
10627
|
+
*/
|
|
10628
|
+
const formatNode = (node, counts) => {
|
|
10629
|
+
const index = (counts.get(node.name) ?? 0) > 1 ? ` [${node.index}]` : '';
|
|
10630
|
+
const components = node.components.length > 0 ? ` (${node.components.join(', ')})` : '';
|
|
10631
|
+
// Braces rather than brackets: `[N]` already means a match index on this line
|
|
10632
|
+
const materials = node.materials.length > 0 ? ` {${node.materials.map((slot) => slot.name ?? 'null').join(', ')}}` : '';
|
|
10633
|
+
return `${node.name}${index}${components}${materials}`;
|
|
10634
|
+
};
|
|
10635
|
+
/**
|
|
10636
|
+
* Formats the printable form of a hierarchy subtree.
|
|
10637
|
+
*
|
|
10638
|
+
* @param root - The subtree root.
|
|
10639
|
+
* @param counts - The number of nodes bearing each name.
|
|
10640
|
+
* @returns The tree, one line per node.
|
|
10641
|
+
*/
|
|
10642
|
+
const formatHierarchy = (root, counts) => {
|
|
10643
|
+
const lines = [formatNode(root, counts)];
|
|
10644
|
+
const walk = (node, prefix) => {
|
|
10645
|
+
node.children.forEach((child, i) => {
|
|
10646
|
+
const last = i === node.children.length - 1;
|
|
10647
|
+
lines.push(`${prefix}${last ? '└─ ' : '├─ '}${formatNode(child, counts)}`);
|
|
10648
|
+
walk(child, `${prefix}${last ? ' ' : '│ '}`);
|
|
10649
|
+
});
|
|
10650
|
+
};
|
|
10651
|
+
walk(root, '');
|
|
10652
|
+
return lines.join('\n');
|
|
10653
|
+
};
|
|
10577
10654
|
/**
|
|
10578
10655
|
* The ModelElement interface provides properties and methods for manipulating
|
|
10579
10656
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
|
|
@@ -10620,6 +10697,57 @@ class ModelElement extends AsyncElement {
|
|
|
10620
10697
|
get entity() {
|
|
10621
10698
|
return this._entity;
|
|
10622
10699
|
}
|
|
10700
|
+
/**
|
|
10701
|
+
* Returns a snapshot of the instantiated node tree, or `null` while there is none (the
|
|
10702
|
+
* container asset has not loaded, or the element has left the document). One call grounds a
|
|
10703
|
+
* session — a browser console, a test, an agent — in the vocabulary `pc-node` binding
|
|
10704
|
+
* resolves against: the instantiated names ({@link HierarchyNode.name}), paths, match
|
|
10705
|
+
* indices, attached component types and the material assignments of render components
|
|
10706
|
+
* ({@link HierarchyNode.materials}). `String(...)` of the result, or of any node in it,
|
|
10707
|
+
* is the printable form.
|
|
10708
|
+
*
|
|
10709
|
+
* The snapshot is plain data, computed afresh each call: it does not follow later changes
|
|
10710
|
+
* to the hierarchy, and mutating it changes nothing.
|
|
10711
|
+
*
|
|
10712
|
+
* @returns The root of the instantiated node tree, or `null`.
|
|
10713
|
+
*/
|
|
10714
|
+
hierarchy() {
|
|
10715
|
+
const root = this._entity;
|
|
10716
|
+
if (!root) {
|
|
10717
|
+
return null;
|
|
10718
|
+
}
|
|
10719
|
+
// Ordinals are assigned in the traversal resolution searches — pre-order depth-first
|
|
10720
|
+
// from the model root, the root itself included — so each node's index is exactly what
|
|
10721
|
+
// a pc-node's index attribute selects. Once the walk completes, the map holds the total
|
|
10722
|
+
// count per name, which is what the printable form reads to annotate only shared names.
|
|
10723
|
+
const ordinals = new Map();
|
|
10724
|
+
const describe = (entity, pathBelowRoot) => {
|
|
10725
|
+
const index = ordinals.get(entity.name) ?? 0;
|
|
10726
|
+
ordinals.set(entity.name, index + 1);
|
|
10727
|
+
const node = {
|
|
10728
|
+
name: entity.name,
|
|
10729
|
+
// The root has no path below itself; its own name stands in, as it does for
|
|
10730
|
+
// the path a pc-node bound to the root reports.
|
|
10731
|
+
path: pathBelowRoot || entity.name,
|
|
10732
|
+
index,
|
|
10733
|
+
// A plain GraphNode grafted into the hierarchy has no component storage
|
|
10734
|
+
components: Object.keys(entity.c ?? {}).sort(),
|
|
10735
|
+
materials: (entity.render?.meshInstances ?? []).map((meshInstance, slot) => ({
|
|
10736
|
+
index: slot,
|
|
10737
|
+
name: meshInstance.material?.name ?? null
|
|
10738
|
+
})),
|
|
10739
|
+
children: entity.children.map((child) => describe(child, pathBelowRoot ? `${pathBelowRoot}/${child.name}` : child.name))
|
|
10740
|
+
};
|
|
10741
|
+
// Non-enumerable, keeping the snapshot plain data under JSON.stringify, spreads and
|
|
10742
|
+
// key enumeration. Deferred to call time, by which the ordinal map holds its totals.
|
|
10743
|
+
Object.defineProperty(node, 'toString', {
|
|
10744
|
+
enumerable: false,
|
|
10745
|
+
value: () => formatHierarchy(node, ordinals)
|
|
10746
|
+
});
|
|
10747
|
+
return node;
|
|
10748
|
+
};
|
|
10749
|
+
return describe(root, '');
|
|
10750
|
+
}
|
|
10623
10751
|
connectedCallback() {
|
|
10624
10752
|
// A model outside an application is inert and never becomes ready, so awaiting it hangs.
|
|
10625
10753
|
// Warn rather than fail silently, naming the parent it requires, as every other misplaced
|
|
@@ -10778,6 +10906,74 @@ class ModelElement extends AsyncElement {
|
|
|
10778
10906
|
}
|
|
10779
10907
|
customElements.define('pc-model', ModelElement);
|
|
10780
10908
|
|
|
10909
|
+
/**
|
|
10910
|
+
* Parses one mapping into its valid rules, warning for each entry that is not one: an unknown
|
|
10911
|
+
* or missing selector prefix, an empty `name:` value, an `index:` value that is not a
|
|
10912
|
+
* non-negative integer, or a replacement id that is not a non-empty string. An invalid rule
|
|
10913
|
+
* behaves exactly as if absent from the mapping.
|
|
10914
|
+
*
|
|
10915
|
+
* @param overrides - The mapping to parse.
|
|
10916
|
+
* @param label - The element description for warnings.
|
|
10917
|
+
* @returns The valid rules.
|
|
10918
|
+
*/
|
|
10919
|
+
const parseMaterialRules = (overrides, label) => {
|
|
10920
|
+
const rules = [];
|
|
10921
|
+
for (const [selector, id] of Object.entries(overrides)) {
|
|
10922
|
+
if (typeof id !== 'string' || id === '') {
|
|
10923
|
+
console.warn(`${label} material-overrides '${selector}' needs a pc-material id - rule ignored`);
|
|
10924
|
+
}
|
|
10925
|
+
else if (selector.startsWith('name:')) {
|
|
10926
|
+
// The text after the prefix is the selector value, exactly as written - a material
|
|
10927
|
+
// name may legitimately begin or end with whitespace
|
|
10928
|
+
const name = selector.slice('name:'.length);
|
|
10929
|
+
if (name === '') {
|
|
10930
|
+
console.warn(`${label} material-overrides 'name:' selector is empty - rule ignored`);
|
|
10931
|
+
}
|
|
10932
|
+
else {
|
|
10933
|
+
rules.push({ kind: 'name', name, id });
|
|
10934
|
+
}
|
|
10935
|
+
}
|
|
10936
|
+
else if (selector.startsWith('index:')) {
|
|
10937
|
+
// Whitespace around the number is tolerated; Number('') is 0, so blank means NaN
|
|
10938
|
+
const text = selector.slice('index:'.length).trim();
|
|
10939
|
+
const index = text === '' ? NaN : Number(text);
|
|
10940
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
10941
|
+
console.warn(`${label} material-overrides '${selector}' is not a non-negative integer index - rule ignored`);
|
|
10942
|
+
}
|
|
10943
|
+
else {
|
|
10944
|
+
rules.push({ kind: 'index', index, id });
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10947
|
+
else {
|
|
10948
|
+
console.warn(`${label} material-overrides '${selector}' has no 'name:' or 'index:' prefix - rule ignored`);
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
return rules;
|
|
10952
|
+
};
|
|
10953
|
+
/**
|
|
10954
|
+
* Parses the material-overrides attribute text. Anything but a JSON object — malformed JSON, an
|
|
10955
|
+
* array, a primitive — warns and yields `null`, the absent mapping: a stale mapping must not
|
|
10956
|
+
* survive an attribute value the DOM no longer represents.
|
|
10957
|
+
*
|
|
10958
|
+
* @param text - The attribute text.
|
|
10959
|
+
* @param label - The element description for warnings.
|
|
10960
|
+
* @returns The mapping, or `null`.
|
|
10961
|
+
*/
|
|
10962
|
+
const parseMaterialOverridesAttribute = (text, label) => {
|
|
10963
|
+
let parsed;
|
|
10964
|
+
try {
|
|
10965
|
+
parsed = JSON.parse(text);
|
|
10966
|
+
}
|
|
10967
|
+
catch (error) {
|
|
10968
|
+
console.warn(`${label} material-overrides is not valid JSON - treated as absent: ${error.message}`);
|
|
10969
|
+
return null;
|
|
10970
|
+
}
|
|
10971
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
10972
|
+
console.warn(`${label} material-overrides must be a JSON object - treated as absent`);
|
|
10973
|
+
return null;
|
|
10974
|
+
}
|
|
10975
|
+
return parsed;
|
|
10976
|
+
};
|
|
10781
10977
|
/**
|
|
10782
10978
|
* Computes the Levenshtein distance between two strings, for near-miss suggestions in the
|
|
10783
10979
|
* resolution warnings.
|
|
@@ -10833,6 +11029,13 @@ const levenshtein = (a, b) => {
|
|
|
10833
11029
|
* "x y z" triple.
|
|
10834
11030
|
* @attribute {string} scale - Overrides the node's local scale, as an "x y z" triple.
|
|
10835
11031
|
* @attribute {string} tags - Overrides the node's tags, separated by spaces or commas.
|
|
11032
|
+
* @attribute {string} material-overrides - Overrides material assignments on the bound node's
|
|
11033
|
+
* render component, as a JSON object from selector to `pc-material` id — for example
|
|
11034
|
+
* `{"name:CarPaint": "candy-red", "index:7": "smoked-glass"}`. A `name:X` key selects every mesh
|
|
11035
|
+
* instance whose baseline material is named `X`; an `index:N` key selects mesh instance `N` and
|
|
11036
|
+
* wins over a name rule for the same instance. Assignments no rule matches keep their baseline
|
|
11037
|
+
* materials, and removing the attribute restores all of them. Use `pc-model.hierarchy()` to
|
|
11038
|
+
* discover the names and indices a node offers.
|
|
10836
11039
|
* @attribute {string} onpointerenter - Script to run when the pointer moves onto the node.
|
|
10837
11040
|
* @attribute {string} onpointerleave - Script to run when the pointer moves off the node.
|
|
10838
11041
|
* @attribute {string} onpointermove - Script to run when the pointer moves over the node.
|
|
@@ -10868,12 +11071,25 @@ class NodeElement extends EntityBaseElement {
|
|
|
10868
11071
|
_destroyHandle = null;
|
|
10869
11072
|
/** The authored values displaced by this element's overrides, captured per property. */
|
|
10870
11073
|
_authored = {};
|
|
11074
|
+
/**
|
|
11075
|
+
* The model-authored render component of the bound node, recorded at bind — before child
|
|
11076
|
+
* decorations build — so a render component added later by a child `pc-render` can never
|
|
11077
|
+
* become the override target. `null` when the bound node has none.
|
|
11078
|
+
*/
|
|
11079
|
+
_authoredRender = null;
|
|
11080
|
+
/**
|
|
11081
|
+
* The baseline assignments displaced by the material overrides, captured for every mesh
|
|
11082
|
+
* instance when the first non-empty mapping applies and released when the mapping goes
|
|
11083
|
+
* absent (restoring them) or the binding dissolves.
|
|
11084
|
+
*/
|
|
11085
|
+
_baseline = null;
|
|
10871
11086
|
// Override values. `null` means "no override": the authored value stays in force.
|
|
10872
11087
|
_enabled = null;
|
|
10873
11088
|
_position = null;
|
|
10874
11089
|
_rotation = null;
|
|
10875
11090
|
_scale = null;
|
|
10876
11091
|
_tags = null;
|
|
11092
|
+
_materialOverrides = null;
|
|
10877
11093
|
/**
|
|
10878
11094
|
* The binding state: `pending` until the host instantiates and `name` resolves, `bound`
|
|
10879
11095
|
* once decorated, `missing`/`ambiguous`/`duplicate` when resolution failed (each also
|
|
@@ -11007,6 +11223,7 @@ class NodeElement extends EntityBaseElement {
|
|
|
11007
11223
|
this._destroyHandle = target.once('destroy', this._onEntityDestroy, this);
|
|
11008
11224
|
this._state = 'bound';
|
|
11009
11225
|
this._path = this._pathOf(target, hostEntity);
|
|
11226
|
+
this._authoredRender = target.render ?? null;
|
|
11010
11227
|
this._applyOverrides();
|
|
11011
11228
|
this._onReady();
|
|
11012
11229
|
this._buildChildren();
|
|
@@ -11037,6 +11254,7 @@ class NodeElement extends EntityBaseElement {
|
|
|
11037
11254
|
this._entity = null;
|
|
11038
11255
|
this._path = null;
|
|
11039
11256
|
this._authored = {};
|
|
11257
|
+
this._authoredRender = null;
|
|
11040
11258
|
// Component decorations come off through the same hook the host-ready cycle uses. A
|
|
11041
11259
|
// dissolve that never rebinds fires no ready event, so the sweep is explicit - after
|
|
11042
11260
|
// `_entity` is cleared, so the hook sees a host without an entity.
|
|
@@ -11058,6 +11276,9 @@ class NodeElement extends EntityBaseElement {
|
|
|
11058
11276
|
this._entity = null;
|
|
11059
11277
|
this._path = null;
|
|
11060
11278
|
this._authored = {};
|
|
11279
|
+
this._authoredRender = null;
|
|
11280
|
+
// The mesh instances died with the entity - the capture is dropped, not restored
|
|
11281
|
+
this._baseline = null;
|
|
11061
11282
|
this._state = 'pending';
|
|
11062
11283
|
this._resetReady();
|
|
11063
11284
|
}
|
|
@@ -11099,6 +11320,9 @@ class NodeElement extends EntityBaseElement {
|
|
|
11099
11320
|
if (this._tags !== null) {
|
|
11100
11321
|
this.tags = this._tags;
|
|
11101
11322
|
}
|
|
11323
|
+
if (this._materialOverrides !== null) {
|
|
11324
|
+
this._applyMaterialOverrides();
|
|
11325
|
+
}
|
|
11102
11326
|
}
|
|
11103
11327
|
/**
|
|
11104
11328
|
* Restores every authored value this element's overrides displaced. The override values
|
|
@@ -11124,6 +11348,104 @@ class NodeElement extends EntityBaseElement {
|
|
|
11124
11348
|
entity.tags.add(authored.tags);
|
|
11125
11349
|
}
|
|
11126
11350
|
this._authored = {};
|
|
11351
|
+
this._restoreBaseline();
|
|
11352
|
+
}
|
|
11353
|
+
/**
|
|
11354
|
+
* Applies the material mapping to the authored render component: parse the mapping's valid
|
|
11355
|
+
* rules, capture the baseline on first application, then recompute every assignment from
|
|
11356
|
+
* that baseline - name rules write over it, index rules write over them, so `index:` wins -
|
|
11357
|
+
* and assign whatever changed. An absent mapping, or one with no valid rules, restores the
|
|
11358
|
+
* baseline instead. Called while bound, from `_applyOverrides` and the property setter.
|
|
11359
|
+
*/
|
|
11360
|
+
_applyMaterialOverrides() {
|
|
11361
|
+
const label = `pc-node '${this._name}'`;
|
|
11362
|
+
const rules = this._materialOverrides ? parseMaterialRules(this._materialOverrides, label) : [];
|
|
11363
|
+
if (rules.length === 0) {
|
|
11364
|
+
this._restoreBaseline();
|
|
11365
|
+
return;
|
|
11366
|
+
}
|
|
11367
|
+
if (!this._baseline) {
|
|
11368
|
+
if (!this._authoredRender) {
|
|
11369
|
+
console.warn(`${label} is bound to a node without an authored render component - material-overrides ignored`);
|
|
11370
|
+
return;
|
|
11371
|
+
}
|
|
11372
|
+
this._baseline = this._authoredRender.meshInstances.map((meshInstance) => ({
|
|
11373
|
+
meshInstance,
|
|
11374
|
+
material: meshInstance.material ?? null,
|
|
11375
|
+
name: meshInstance.material?.name ?? null
|
|
11376
|
+
}));
|
|
11377
|
+
}
|
|
11378
|
+
const baseline = this._baseline;
|
|
11379
|
+
/** Resolves a replacement id, warning when it does not resolve. */
|
|
11380
|
+
const resolveReplacement = (id) => {
|
|
11381
|
+
const material = MaterialElement.get(id);
|
|
11382
|
+
if (!material) {
|
|
11383
|
+
console.warn(`${label} material-overrides could not resolve pc-material '${id}' - rule ignored`);
|
|
11384
|
+
}
|
|
11385
|
+
return material ?? null;
|
|
11386
|
+
};
|
|
11387
|
+
// Recompute the whole list from the baseline: name rules write over it, index rules
|
|
11388
|
+
// write over them. Recomputing makes mapping edits order-independent, and a rule whose
|
|
11389
|
+
// replacement does not resolve simply leaves the layer below it in force.
|
|
11390
|
+
const resolved = baseline.map((assignment) => assignment.material);
|
|
11391
|
+
for (const rule of rules) {
|
|
11392
|
+
if (rule.kind !== 'name') {
|
|
11393
|
+
continue;
|
|
11394
|
+
}
|
|
11395
|
+
const material = resolveReplacement(rule.id);
|
|
11396
|
+
if (!material) {
|
|
11397
|
+
continue;
|
|
11398
|
+
}
|
|
11399
|
+
let matched = false;
|
|
11400
|
+
baseline.forEach((assignment, index) => {
|
|
11401
|
+
if (assignment.name === rule.name) {
|
|
11402
|
+
resolved[index] = material;
|
|
11403
|
+
matched = true;
|
|
11404
|
+
}
|
|
11405
|
+
});
|
|
11406
|
+
if (!matched) {
|
|
11407
|
+
const names = baseline.map((assignment) => `'${assignment.name}'`).join(', ');
|
|
11408
|
+
console.warn(`${label} material-overrides 'name:${rule.name}' matches no assignment - ` +
|
|
11409
|
+
`baseline names: ${names || '(none)'}`);
|
|
11410
|
+
}
|
|
11411
|
+
}
|
|
11412
|
+
for (const rule of rules) {
|
|
11413
|
+
if (rule.kind !== 'index') {
|
|
11414
|
+
continue;
|
|
11415
|
+
}
|
|
11416
|
+
if (rule.index >= baseline.length) {
|
|
11417
|
+
console.warn(`${label} material-overrides 'index:${rule.index}' is out of range - ` +
|
|
11418
|
+
`${baseline.length} assignment(s)`);
|
|
11419
|
+
continue;
|
|
11420
|
+
}
|
|
11421
|
+
const material = resolveReplacement(rule.id);
|
|
11422
|
+
if (material) {
|
|
11423
|
+
resolved[rule.index] = material;
|
|
11424
|
+
}
|
|
11425
|
+
}
|
|
11426
|
+
baseline.forEach((assignment, index) => {
|
|
11427
|
+
// The engine setter rebuilds material and shader state even for a redundant write,
|
|
11428
|
+
// so only actual changes are assigned
|
|
11429
|
+
if (assignment.meshInstance.material !== resolved[index]) {
|
|
11430
|
+
assignment.meshInstance.material = resolved[index];
|
|
11431
|
+
}
|
|
11432
|
+
});
|
|
11433
|
+
}
|
|
11434
|
+
/**
|
|
11435
|
+
* Restores every baseline assignment the material overrides displaced and releases the
|
|
11436
|
+
* capture, so the next non-empty mapping captures afresh. Safe to call without a capture.
|
|
11437
|
+
*/
|
|
11438
|
+
_restoreBaseline() {
|
|
11439
|
+
const baseline = this._baseline;
|
|
11440
|
+
if (!baseline) {
|
|
11441
|
+
return;
|
|
11442
|
+
}
|
|
11443
|
+
this._baseline = null;
|
|
11444
|
+
for (const assignment of baseline) {
|
|
11445
|
+
if (assignment.meshInstance.material !== assignment.material) {
|
|
11446
|
+
assignment.meshInstance.material = assignment.material;
|
|
11447
|
+
}
|
|
11448
|
+
}
|
|
11127
11449
|
}
|
|
11128
11450
|
/**
|
|
11129
11451
|
* Renders the path of `node` below `root`, for the `path` property and the resolution
|
|
@@ -11344,8 +11666,41 @@ class NodeElement extends EntityBaseElement {
|
|
|
11344
11666
|
get tags() {
|
|
11345
11667
|
return this._tags;
|
|
11346
11668
|
}
|
|
11669
|
+
/**
|
|
11670
|
+
* Sets the material overrides: a sparse mapping from selector to `pc-material` id, applied
|
|
11671
|
+
* to the bound node's authored render component. A `name:X` key selects every mesh instance
|
|
11672
|
+
* whose baseline material is named `X`; an `index:N` key selects mesh instance `N` and wins
|
|
11673
|
+
* over a name rule for the same instance. Assignments no rule matches keep their baseline
|
|
11674
|
+
* materials. `null` clears the mapping, restoring every baseline assignment.
|
|
11675
|
+
* @param value - The mapping, or `null`.
|
|
11676
|
+
*/
|
|
11677
|
+
set materialOverrides(value) {
|
|
11678
|
+
// Copied and frozen: later caller mutation of the passed object must not silently
|
|
11679
|
+
// disagree with the mapping the element applied
|
|
11680
|
+
this._materialOverrides = value === null ? null : Object.freeze({ ...value });
|
|
11681
|
+
if (this._state === 'bound') {
|
|
11682
|
+
this._applyMaterialOverrides();
|
|
11683
|
+
}
|
|
11684
|
+
}
|
|
11685
|
+
/**
|
|
11686
|
+
* Gets the material overrides.
|
|
11687
|
+
* @returns The mapping, or `null` while no override is set.
|
|
11688
|
+
*/
|
|
11689
|
+
get materialOverrides() {
|
|
11690
|
+
return this._materialOverrides;
|
|
11691
|
+
}
|
|
11347
11692
|
static get observedAttributes() {
|
|
11348
|
-
return [
|
|
11693
|
+
return [
|
|
11694
|
+
'enabled',
|
|
11695
|
+
'index',
|
|
11696
|
+
'material-overrides',
|
|
11697
|
+
'name',
|
|
11698
|
+
'position',
|
|
11699
|
+
'rotation',
|
|
11700
|
+
'scale',
|
|
11701
|
+
'tags',
|
|
11702
|
+
...POINTER_ATTRIBUTES
|
|
11703
|
+
];
|
|
11349
11704
|
}
|
|
11350
11705
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
11351
11706
|
switch (name) {
|
|
@@ -11370,6 +11725,10 @@ class NodeElement extends EntityBaseElement {
|
|
|
11370
11725
|
}
|
|
11371
11726
|
}
|
|
11372
11727
|
break;
|
|
11728
|
+
case 'material-overrides':
|
|
11729
|
+
this.materialOverrides =
|
|
11730
|
+
newValue === null ? null : parseMaterialOverridesAttribute(newValue, `pc-node '${this._name}'`);
|
|
11731
|
+
break;
|
|
11373
11732
|
case 'name':
|
|
11374
11733
|
this.name = newValue ?? '';
|
|
11375
11734
|
break;
|