@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.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) {
|
|
@@ -1598,7 +1614,7 @@
|
|
|
1598
1614
|
/**
|
|
1599
1615
|
* The attribute names of the inline `onpointer*` event handlers, shared by every element that
|
|
1600
1616
|
* fronts an engine entity. Spread into `observedAttributes` by subclasses.
|
|
1601
|
-
* @
|
|
1617
|
+
* @internal
|
|
1602
1618
|
*/
|
|
1603
1619
|
const POINTER_ATTRIBUTES = [
|
|
1604
1620
|
'onpointerenter',
|
|
@@ -6059,6 +6075,7 @@
|
|
|
6059
6075
|
_metalnessMapRotation = 0;
|
|
6060
6076
|
_metalnessMapTiling = new playcanvas.Vec2(1, 1);
|
|
6061
6077
|
_metalnessMapUv = 0;
|
|
6078
|
+
_name = 'Untitled';
|
|
6062
6079
|
_normalMap = '';
|
|
6063
6080
|
_normalMapOffset = new playcanvas.Vec2(0, 0);
|
|
6064
6081
|
_normalMapRotation = 0;
|
|
@@ -6176,6 +6193,7 @@
|
|
|
6176
6193
|
material.metalnessMapRotation = this._metalnessMapRotation;
|
|
6177
6194
|
material.metalnessMapTiling = this._metalnessMapTiling;
|
|
6178
6195
|
material.metalnessMapUv = this._metalnessMapUv;
|
|
6196
|
+
material.name = this._name;
|
|
6179
6197
|
material.normalMapOffset = this._normalMapOffset;
|
|
6180
6198
|
material.normalMapRotation = this._normalMapRotation;
|
|
6181
6199
|
material.normalMapTiling = this._normalMapTiling;
|
|
@@ -7262,6 +7280,26 @@
|
|
|
7262
7280
|
get metalnessMapUv() {
|
|
7263
7281
|
return this._metalnessMapUv;
|
|
7264
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
|
+
}
|
|
7265
7303
|
/**
|
|
7266
7304
|
* Sets the id of the `pc-asset` to use as the normal map.
|
|
7267
7305
|
* @param value - The asset id.
|
|
@@ -7833,6 +7871,7 @@
|
|
|
7833
7871
|
'metalness-map-rotation',
|
|
7834
7872
|
'metalness-map-tiling',
|
|
7835
7873
|
'metalness-map-uv',
|
|
7874
|
+
'name',
|
|
7836
7875
|
'normal-map',
|
|
7837
7876
|
'normal-map-offset',
|
|
7838
7877
|
'normal-map-rotation',
|
|
@@ -8033,6 +8072,9 @@
|
|
|
8033
8072
|
case 'metalness-map-uv':
|
|
8034
8073
|
this.metalnessMapUv = parseNumber(newValue, 0, name);
|
|
8035
8074
|
break;
|
|
8075
|
+
case 'name':
|
|
8076
|
+
this.name = newValue ?? 'Untitled';
|
|
8077
|
+
break;
|
|
8036
8078
|
case 'normal-map':
|
|
8037
8079
|
this.normalMap = newValue ?? '';
|
|
8038
8080
|
break;
|
|
@@ -10578,6 +10620,41 @@
|
|
|
10578
10620
|
}
|
|
10579
10621
|
customElements.define('pc-gsplat', GSplatComponentElement);
|
|
10580
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
|
+
};
|
|
10581
10658
|
/**
|
|
10582
10659
|
* The ModelElement interface provides properties and methods for manipulating
|
|
10583
10660
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
|
|
@@ -10624,6 +10701,57 @@
|
|
|
10624
10701
|
get entity() {
|
|
10625
10702
|
return this._entity;
|
|
10626
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
|
+
}
|
|
10627
10755
|
connectedCallback() {
|
|
10628
10756
|
// A model outside an application is inert and never becomes ready, so awaiting it hangs.
|
|
10629
10757
|
// Warn rather than fail silently, naming the parent it requires, as every other misplaced
|
|
@@ -10782,6 +10910,74 @@
|
|
|
10782
10910
|
}
|
|
10783
10911
|
customElements.define('pc-model', ModelElement);
|
|
10784
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
|
+
};
|
|
10785
10981
|
/**
|
|
10786
10982
|
* Computes the Levenshtein distance between two strings, for near-miss suggestions in the
|
|
10787
10983
|
* resolution warnings.
|
|
@@ -10837,6 +11033,13 @@
|
|
|
10837
11033
|
* "x y z" triple.
|
|
10838
11034
|
* @attribute {string} scale - Overrides the node's local scale, as an "x y z" triple.
|
|
10839
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.
|
|
10840
11043
|
* @attribute {string} onpointerenter - Script to run when the pointer moves onto the node.
|
|
10841
11044
|
* @attribute {string} onpointerleave - Script to run when the pointer moves off the node.
|
|
10842
11045
|
* @attribute {string} onpointermove - Script to run when the pointer moves over the node.
|
|
@@ -10872,12 +11075,25 @@
|
|
|
10872
11075
|
_destroyHandle = null;
|
|
10873
11076
|
/** The authored values displaced by this element's overrides, captured per property. */
|
|
10874
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;
|
|
10875
11090
|
// Override values. `null` means "no override": the authored value stays in force.
|
|
10876
11091
|
_enabled = null;
|
|
10877
11092
|
_position = null;
|
|
10878
11093
|
_rotation = null;
|
|
10879
11094
|
_scale = null;
|
|
10880
11095
|
_tags = null;
|
|
11096
|
+
_materialOverrides = null;
|
|
10881
11097
|
/**
|
|
10882
11098
|
* The binding state: `pending` until the host instantiates and `name` resolves, `bound`
|
|
10883
11099
|
* once decorated, `missing`/`ambiguous`/`duplicate` when resolution failed (each also
|
|
@@ -11011,6 +11227,7 @@
|
|
|
11011
11227
|
this._destroyHandle = target.once('destroy', this._onEntityDestroy, this);
|
|
11012
11228
|
this._state = 'bound';
|
|
11013
11229
|
this._path = this._pathOf(target, hostEntity);
|
|
11230
|
+
this._authoredRender = target.render ?? null;
|
|
11014
11231
|
this._applyOverrides();
|
|
11015
11232
|
this._onReady();
|
|
11016
11233
|
this._buildChildren();
|
|
@@ -11041,6 +11258,7 @@
|
|
|
11041
11258
|
this._entity = null;
|
|
11042
11259
|
this._path = null;
|
|
11043
11260
|
this._authored = {};
|
|
11261
|
+
this._authoredRender = null;
|
|
11044
11262
|
// Component decorations come off through the same hook the host-ready cycle uses. A
|
|
11045
11263
|
// dissolve that never rebinds fires no ready event, so the sweep is explicit - after
|
|
11046
11264
|
// `_entity` is cleared, so the hook sees a host without an entity.
|
|
@@ -11062,6 +11280,9 @@
|
|
|
11062
11280
|
this._entity = null;
|
|
11063
11281
|
this._path = null;
|
|
11064
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;
|
|
11065
11286
|
this._state = 'pending';
|
|
11066
11287
|
this._resetReady();
|
|
11067
11288
|
}
|
|
@@ -11103,6 +11324,9 @@
|
|
|
11103
11324
|
if (this._tags !== null) {
|
|
11104
11325
|
this.tags = this._tags;
|
|
11105
11326
|
}
|
|
11327
|
+
if (this._materialOverrides !== null) {
|
|
11328
|
+
this._applyMaterialOverrides();
|
|
11329
|
+
}
|
|
11106
11330
|
}
|
|
11107
11331
|
/**
|
|
11108
11332
|
* Restores every authored value this element's overrides displaced. The override values
|
|
@@ -11128,6 +11352,104 @@
|
|
|
11128
11352
|
entity.tags.add(authored.tags);
|
|
11129
11353
|
}
|
|
11130
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
|
+
}
|
|
11131
11453
|
}
|
|
11132
11454
|
/**
|
|
11133
11455
|
* Renders the path of `node` below `root`, for the `path` property and the resolution
|
|
@@ -11348,8 +11670,41 @@
|
|
|
11348
11670
|
get tags() {
|
|
11349
11671
|
return this._tags;
|
|
11350
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
|
+
}
|
|
11351
11696
|
static get observedAttributes() {
|
|
11352
|
-
return [
|
|
11697
|
+
return [
|
|
11698
|
+
'enabled',
|
|
11699
|
+
'index',
|
|
11700
|
+
'material-overrides',
|
|
11701
|
+
'name',
|
|
11702
|
+
'position',
|
|
11703
|
+
'rotation',
|
|
11704
|
+
'scale',
|
|
11705
|
+
'tags',
|
|
11706
|
+
...POINTER_ATTRIBUTES
|
|
11707
|
+
];
|
|
11353
11708
|
}
|
|
11354
11709
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
11355
11710
|
switch (name) {
|
|
@@ -11374,6 +11729,10 @@
|
|
|
11374
11729
|
}
|
|
11375
11730
|
}
|
|
11376
11731
|
break;
|
|
11732
|
+
case 'material-overrides':
|
|
11733
|
+
this.materialOverrides =
|
|
11734
|
+
newValue === null ? null : parseMaterialOverridesAttribute(newValue, `pc-node '${this._name}'`);
|
|
11735
|
+
break;
|
|
11377
11736
|
case 'name':
|
|
11378
11737
|
this.name = newValue ?? '';
|
|
11379
11738
|
break;
|