@playcanvas/web-components 0.13.1 → 0.15.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.
@@ -6,9 +6,9 @@ import { AsyncElement } from './async-element';
6
6
  /**
7
7
  * The attribute names of the inline `onpointer*` event handlers, shared by every element that
8
8
  * fronts an engine entity. Spread into `observedAttributes` by subclasses.
9
- * @ignore
9
+ * @internal
10
10
  */
11
- const POINTER_ATTRIBUTES = [
11
+ export const POINTER_ATTRIBUTES = [
12
12
  'onpointerenter',
13
13
  'onpointerleave',
14
14
  'onpointerdown',
@@ -133,4 +133,4 @@ class EntityBaseElement extends AsyncElement {
133
133
  }
134
134
  }
135
135
 
136
- export { EntityBaseElement, POINTER_ATTRIBUTES };
136
+ export { EntityBaseElement };
package/src/entity.ts CHANGED
@@ -4,6 +4,33 @@ import { Entity, Vec3 } from 'playcanvas';
4
4
  import { EntityBaseElement, POINTER_ATTRIBUTES } from './entity-base';
5
5
  import { parseBool, parseTags, parseVec3 } from './parse';
6
6
 
7
+ /**
8
+ * Creates and parents the entities of every descendant `<pc-entity>` of `root`, in two passes so
9
+ * that no parent's existence depends on document order. Called wherever a subtree could not build
10
+ * itself: an element inserted into an application that is already running, and a `<pc-node>` whose
11
+ * children waited for it to bind.
12
+ *
13
+ * Descendants that are not yet custom elements are skipped, because there is nothing useful to do
14
+ * for them and reaching for `_createEntity` would throw. A subtree cloned from a `<template>`
15
+ * arrives entirely unupgraded — template content lives in an inert document, where custom element
16
+ * definitions are never looked up — and appending the clone upgrades its elements in tree order,
17
+ * an element before its descendants. So a sweep from an element's own `connectedCallback` sees
18
+ * plain `HTMLElement`s below it. Each becomes an `EntityElement` moments later and its own
19
+ * `connectedCallback` creates and parents it, by which time the ancestor it parents under has its
20
+ * entity — the same guarantee tree order gives this sweep.
21
+ *
22
+ * @param root - The element whose descendant entities to build.
23
+ * @param app - The application to create the entities in.
24
+ * @internal
25
+ */
26
+ export const buildDescendantEntities = (root: Element, app: AppBase) => {
27
+ const children = Array.from(root.querySelectorAll('pc-entity')).filter(
28
+ (child): child is EntityElement => child instanceof EntityElement
29
+ );
30
+ children.forEach((child) => child._createEntity(app));
31
+ children.forEach((child) => child._buildHierarchy(app));
32
+ };
33
+
7
34
  /**
8
35
  * The EntityElement interface provides properties and methods for manipulating
9
36
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
@@ -172,13 +199,7 @@ class EntityElement extends EntityBaseElement {
172
199
  this._buildHierarchy(app);
173
200
 
174
201
  // Handle any child entities that might exist
175
- const childEntities = this.querySelectorAll<EntityElement>('pc-entity');
176
- childEntities.forEach((child) => {
177
- child._createEntity(app);
178
- });
179
- childEntities.forEach((child) => {
180
- child._buildHierarchy(app);
181
- });
202
+ buildDescendantEntities(this, app);
182
203
  }
183
204
  }
184
205
 
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ import { CameraComponentElement } from './components/camera-component';
21
21
  import { CollisionComponentElement } from './components/collision-component';
22
22
  import { ComponentElement } from './components/component';
23
23
  import { ElementComponentElement } from './components/element-component';
24
+ import { JointComponentElement } from './components/joint-component';
24
25
  import { LayoutChildComponentElement } from './components/layoutchild-component';
25
26
  import { LayoutGroupComponentElement } from './components/layoutgroup-component';
26
27
  import { LightComponentElement } from './components/light-component';
@@ -50,6 +51,7 @@ import type {
50
51
 
51
52
  declare global {
52
53
  interface HTMLElementEventMap {
54
+ break: CustomEvent;
53
55
  scriptattributeschange: ScriptAttributesChangeEvent;
54
56
  scriptenablechange: ScriptEnableChangeEvent;
55
57
  scriptnamechange: ScriptNameChangeEvent;
@@ -64,6 +66,7 @@ declare global {
64
66
  'pc-element': ElementComponentElement;
65
67
  'pc-entity': EntityElement;
66
68
  'pc-gsplat': GSplatComponentElement;
69
+ 'pc-joint': JointComponentElement;
67
70
  'pc-layoutchild': LayoutChildComponentElement;
68
71
  'pc-layoutgroup': LayoutGroupComponentElement;
69
72
  'pc-light': LightComponentElement;
@@ -98,6 +101,7 @@ export {
98
101
  CollisionComponentElement,
99
102
  ComponentElement,
100
103
  ElementComponentElement,
104
+ JointComponentElement,
101
105
  LayoutChildComponentElement,
102
106
  LayoutGroupComponentElement,
103
107
  ParticleSystemComponentElement,
@@ -123,3 +127,5 @@ export {
123
127
  };
124
128
 
125
129
  export type { AsyncElementTagName } from './async-element';
130
+ export type { HierarchyMaterial, HierarchyNode } from './model';
131
+ export type { MaterialOverrides } from './node';
@@ -8,8 +8,9 @@ const REMOVAL_DELAY_MS = 250;
8
8
  * All styling is inline, so the library injects no stylesheet. The colors and height resolve CSS
9
9
  * custom properties — `--pc-loading-bar-color`, `--pc-loading-bar-background` and
10
10
  * `--pc-loading-bar-height` — so a page can theme the bar from `pc-app` or `:root`.
11
+ * @internal
11
12
  */
12
- class LoadingBar {
13
+ export class LoadingBar {
13
14
  private _track: HTMLDivElement;
14
15
 
15
16
  private _fill: HTMLDivElement;
@@ -118,5 +119,3 @@ class LoadingBar {
118
119
  this._track.remove();
119
120
  }
120
121
  }
121
-
122
- export { LoadingBar };
package/src/material.ts CHANGED
@@ -251,6 +251,8 @@ class MaterialElement extends HTMLElement {
251
251
 
252
252
  private _metalnessMapUv = 0;
253
253
 
254
+ private _name = 'Untitled';
255
+
254
256
  private _normalMap = '';
255
257
 
256
258
  private _normalMapOffset = new Vec2(0, 0);
@@ -402,6 +404,7 @@ class MaterialElement extends HTMLElement {
402
404
  material.metalnessMapRotation = this._metalnessMapRotation;
403
405
  material.metalnessMapTiling = this._metalnessMapTiling;
404
406
  material.metalnessMapUv = this._metalnessMapUv;
407
+ material.name = this._name;
405
408
  material.normalMapOffset = this._normalMapOffset;
406
409
  material.normalMapRotation = this._normalMapRotation;
407
410
  material.normalMapTiling = this._normalMapTiling;
@@ -1615,6 +1618,28 @@ class MaterialElement extends HTMLElement {
1615
1618
  return this._metalnessMapUv;
1616
1619
  }
1617
1620
 
1621
+ /**
1622
+ * Sets the name of the material.
1623
+ * @param value - The material name.
1624
+ */
1625
+ set name(value: string) {
1626
+ this._name = value;
1627
+ if (this.material) {
1628
+ // A label rather than shader state, so no update() is scheduled
1629
+ this.material.name = value;
1630
+ }
1631
+ }
1632
+
1633
+ /**
1634
+ * Gets the name of the material - the label shown wherever materials surface by name, such
1635
+ * as profilers, GPU captures and the assignments `pc-model.hierarchy()` reports. Purely a
1636
+ * label: element references resolve through `id`.
1637
+ * @returns The material name.
1638
+ */
1639
+ get name() {
1640
+ return this._name;
1641
+ }
1642
+
1618
1643
  /**
1619
1644
  * Sets the id of the `pc-asset` to use as the normal map.
1620
1645
  * @param value - The asset id.
@@ -2243,6 +2268,7 @@ class MaterialElement extends HTMLElement {
2243
2268
  'metalness-map-rotation',
2244
2269
  'metalness-map-tiling',
2245
2270
  'metalness-map-uv',
2271
+ 'name',
2246
2272
  'normal-map',
2247
2273
  'normal-map-offset',
2248
2274
  'normal-map-rotation',
@@ -2444,6 +2470,9 @@ class MaterialElement extends HTMLElement {
2444
2470
  case 'metalness-map-uv':
2445
2471
  this.metalnessMapUv = parseNumber(newValue, 0, name);
2446
2472
  break;
2473
+ case 'name':
2474
+ this.name = newValue ?? 'Untitled';
2475
+ break;
2447
2476
  case 'normal-map':
2448
2477
  this.normalMap = newValue ?? '';
2449
2478
  break;
package/src/model.ts CHANGED
@@ -3,6 +3,101 @@ import type { ContainerResource, Entity, EventHandle } from 'playcanvas';
3
3
  import { useAsset } from './asset';
4
4
  import { AsyncElement } from './async-element';
5
5
 
6
+ /**
7
+ * One material assignment of a {@link HierarchyNode} with a render component: a mesh instance's
8
+ * position within the component and the runtime name of its current material.
9
+ */
10
+ type HierarchyMaterial = {
11
+ /** The mesh instance's position in the render component's `meshInstances` array. */
12
+ index: number;
13
+ /**
14
+ * The runtime name of the mesh instance's current material, reported as-is: the engine
15
+ * names an unnamed glTF material `Untitled`, and assigns a shared material named
16
+ * `defaultGlbMaterial` to a primitive authored without one — neither is a unique authored
17
+ * identifier. `null` when a script has cleared the assignment.
18
+ */
19
+ name: string | null;
20
+ };
21
+
22
+ /**
23
+ * One node of the tree returned by {@link ModelElement.hierarchy}. A plain-data snapshot —
24
+ * `JSON.stringify` serializes it — whose `toString()` renders the node's subtree as a printable
25
+ * tree.
26
+ */
27
+ type HierarchyNode = {
28
+ /**
29
+ * The node's name as instantiated, which is the name `pc-node` binding resolves: the engine
30
+ * parser synthesizes `node_<index>` names for unnamed nodes and renames identically named
31
+ * siblings apart (`Wheel`, `Wheel1`, ...), so it can differ from the name authored in the
32
+ * source asset.
33
+ */
34
+ name: string;
35
+ /**
36
+ * The node's `/`-separated path below the model root — the path a `pc-node` bound to this
37
+ * node reports. The root's path is its own name.
38
+ */
39
+ path: string;
40
+ /**
41
+ * The node's position among identically named nodes in the model, counted in depth-first
42
+ * order over the whole tree: the match a `pc-node`'s `index` attribute selects when `name`
43
+ * alone is ambiguous.
44
+ */
45
+ index: number;
46
+ /** The types of the components attached to the node (e.g. 'render'), sorted. */
47
+ components: string[];
48
+ /**
49
+ * The material assignments of the node's render component, one entry per mesh instance in
50
+ * component order. Empty for a node without a render component.
51
+ */
52
+ materials: HierarchyMaterial[];
53
+ /** The node's children. */
54
+ children: HierarchyNode[];
55
+ /**
56
+ * Renders the subtree rooted at this node as a printable tree, one line per node: the name,
57
+ * `[index]` after a name that several nodes in the model share, the component types in
58
+ * parentheses, and the material names of a render component in braces.
59
+ */
60
+ toString(): string;
61
+ };
62
+
63
+ /**
64
+ * Formats one line of the printable hierarchy: the node's name, an `[index]` marker when the
65
+ * name is shared by several nodes in the model, the attached component types, and the material
66
+ * names of a render component.
67
+ *
68
+ * @param node - The node to format.
69
+ * @param counts - The number of nodes bearing each name.
70
+ * @returns The formatted line.
71
+ */
72
+ const formatNode = (node: HierarchyNode, counts: ReadonlyMap<string, number>): string => {
73
+ const index = (counts.get(node.name) ?? 0) > 1 ? ` [${node.index}]` : '';
74
+ const components = node.components.length > 0 ? ` (${node.components.join(', ')})` : '';
75
+ // Braces rather than brackets: `[N]` already means a match index on this line
76
+ const materials =
77
+ node.materials.length > 0 ? ` {${node.materials.map((slot) => slot.name ?? 'null').join(', ')}}` : '';
78
+ return `${node.name}${index}${components}${materials}`;
79
+ };
80
+
81
+ /**
82
+ * Formats the printable form of a hierarchy subtree.
83
+ *
84
+ * @param root - The subtree root.
85
+ * @param counts - The number of nodes bearing each name.
86
+ * @returns The tree, one line per node.
87
+ */
88
+ const formatHierarchy = (root: HierarchyNode, counts: ReadonlyMap<string, number>): string => {
89
+ const lines = [formatNode(root, counts)];
90
+ const walk = (node: HierarchyNode, prefix: string) => {
91
+ node.children.forEach((child, i) => {
92
+ const last = i === node.children.length - 1;
93
+ lines.push(`${prefix}${last ? '└─ ' : '├─ '}${formatNode(child, counts)}`);
94
+ walk(child, `${prefix}${last ? ' ' : '│ '}`);
95
+ });
96
+ };
97
+ walk(root, '');
98
+ return lines.join('\n');
99
+ };
100
+
6
101
  /**
7
102
  * The ModelElement interface provides properties and methods for manipulating
8
103
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
@@ -55,6 +150,66 @@ class ModelElement extends AsyncElement {
55
150
  return this._entity;
56
151
  }
57
152
 
153
+ /**
154
+ * Returns a snapshot of the instantiated node tree, or `null` while there is none (the
155
+ * container asset has not loaded, or the element has left the document). One call grounds a
156
+ * session — a browser console, a test, an agent — in the vocabulary `pc-node` binding
157
+ * resolves against: the instantiated names ({@link HierarchyNode.name}), paths, match
158
+ * indices, attached component types and the material assignments of render components
159
+ * ({@link HierarchyNode.materials}). `String(...)` of the result, or of any node in it,
160
+ * is the printable form.
161
+ *
162
+ * The snapshot is plain data, computed afresh each call: it does not follow later changes
163
+ * to the hierarchy, and mutating it changes nothing.
164
+ *
165
+ * @returns The root of the instantiated node tree, or `null`.
166
+ */
167
+ hierarchy(): HierarchyNode | null {
168
+ const root = this._entity;
169
+ if (!root) {
170
+ return null;
171
+ }
172
+
173
+ // Ordinals are assigned in the traversal resolution searches — pre-order depth-first
174
+ // from the model root, the root itself included — so each node's index is exactly what
175
+ // a pc-node's index attribute selects. Once the walk completes, the map holds the total
176
+ // count per name, which is what the printable form reads to annotate only shared names.
177
+ const ordinals = new Map<string, number>();
178
+
179
+ const describe = (entity: Entity, pathBelowRoot: string): HierarchyNode => {
180
+ const index = ordinals.get(entity.name) ?? 0;
181
+ ordinals.set(entity.name, index + 1);
182
+
183
+ const node: HierarchyNode = {
184
+ name: entity.name,
185
+ // The root has no path below itself; its own name stands in, as it does for
186
+ // the path a pc-node bound to the root reports.
187
+ path: pathBelowRoot || entity.name,
188
+ index,
189
+ // A plain GraphNode grafted into the hierarchy has no component storage
190
+ components: Object.keys(entity.c ?? {}).sort(),
191
+ materials: (entity.render?.meshInstances ?? []).map((meshInstance, slot) => ({
192
+ index: slot,
193
+ name: meshInstance.material?.name ?? null
194
+ })),
195
+ children: entity.children.map((child) =>
196
+ describe(child as Entity, pathBelowRoot ? `${pathBelowRoot}/${child.name}` : child.name)
197
+ )
198
+ };
199
+
200
+ // Non-enumerable, keeping the snapshot plain data under JSON.stringify, spreads and
201
+ // key enumeration. Deferred to call time, by which the ordinal map holds its totals.
202
+ Object.defineProperty(node, 'toString', {
203
+ enumerable: false,
204
+ value: () => formatHierarchy(node, ordinals)
205
+ });
206
+
207
+ return node;
208
+ };
209
+
210
+ return describe(root, '');
211
+ }
212
+
58
213
  connectedCallback() {
59
214
  // A model outside an application is inert and never becomes ready, so awaiting it hangs.
60
215
  // Warn rather than fail silently, naming the parent it requires, as every other misplaced
@@ -235,3 +390,4 @@ class ModelElement extends AsyncElement {
235
390
  customElements.define('pc-model', ModelElement);
236
391
 
237
392
  export { ModelElement };
393
+ export type { HierarchyMaterial, HierarchyNode };