@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/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
- * @ignore
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
- * @ignore
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 ['enabled', 'index', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
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;