@playcanvas/web-components 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +13 -11
- package/dist/asset.d.ts +144 -5
- package/dist/async-element.d.ts +6 -5
- package/dist/components/collision-component.d.ts +16 -0
- package/dist/components/component.d.ts +19 -0
- package/dist/custom-elements.json +1054 -164
- package/dist/entity-base.d.ts +67 -0
- package/dist/entity.d.ts +3 -38
- package/dist/index.d.ts +4 -1
- package/dist/material.d.ts +2 -1
- package/dist/model.d.ts +26 -4
- package/dist/node.d.ts +253 -0
- package/dist/pwc.cjs +1364 -168
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +1364 -168
- 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 +1364 -170
- package/dist/pwc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +126 -4
- package/dist/web-types.json +360 -56
- package/package.json +2 -2
- package/src/app.ts +27 -24
- package/src/asset.ts +439 -9
- package/src/async-element.ts +7 -6
- package/src/components/collision-component.ts +35 -0
- package/src/components/component.ts +93 -3
- package/src/entity-base.ts +136 -0
- package/src/entity.ts +23 -117
- package/src/index.ts +5 -0
- package/src/material.ts +2 -2
- package/src/model.ts +79 -11
- package/src/node.ts +715 -0
- package/src/sky.ts +0 -1
package/src/node.ts
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import type { Entity, EventHandle, GraphNode, Quat } from 'playcanvas';
|
|
2
|
+
import { Vec3 } from 'playcanvas';
|
|
3
|
+
|
|
4
|
+
import { ComponentElement } from './components/component';
|
|
5
|
+
import type { EntityElement } from './entity';
|
|
6
|
+
import { EntityBaseElement, POINTER_ATTRIBUTES } from './entity-base';
|
|
7
|
+
import { ModelElement } from './model';
|
|
8
|
+
import { parseBool, parseTags, parseVec3 } from './parse';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The binding states a `<pc-node>` element moves through. `pending` while the host has not yet
|
|
12
|
+
* instantiated (or no `name` is assigned), `bound` once a node has been resolved and decorated,
|
|
13
|
+
* `missing`/`ambiguous`/`duplicate` when resolution failed — each accompanied by a warning
|
|
14
|
+
* naming the cause.
|
|
15
|
+
*/
|
|
16
|
+
type NodeBindingState = 'pending' | 'bound' | 'missing' | 'ambiguous' | 'duplicate';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The authored values a bound node's overrides displaced, captured per property when the first
|
|
20
|
+
* override of that property applies and restored when the override clears.
|
|
21
|
+
*/
|
|
22
|
+
type AuthoredState = {
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
position?: Vec3;
|
|
25
|
+
rotation?: Quat;
|
|
26
|
+
scale?: Vec3;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Computes the Levenshtein distance between two strings, for near-miss suggestions in the
|
|
32
|
+
* resolution warnings.
|
|
33
|
+
*
|
|
34
|
+
* @param a - The first string.
|
|
35
|
+
* @param b - The second string.
|
|
36
|
+
* @returns The edit distance.
|
|
37
|
+
*/
|
|
38
|
+
const levenshtein = (a: string, b: string): number => {
|
|
39
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
40
|
+
for (let i = 1; i <= a.length; i++) {
|
|
41
|
+
let previous = row[0];
|
|
42
|
+
row[0] = i;
|
|
43
|
+
for (let j = 1; j <= b.length; j++) {
|
|
44
|
+
const current = row[j];
|
|
45
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
46
|
+
previous = current;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return row[b.length];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The NodeElement interface provides properties and methods for manipulating
|
|
54
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-node/ | `<pc-node>`}
|
|
55
|
+
* elements. The NodeElement interface also inherits the properties and methods of the
|
|
56
|
+
* {@link HTMLElement} interface.
|
|
57
|
+
*
|
|
58
|
+
* A `pc-node` is an override element: where `pc-entity` creates an entity, `pc-node` binds to a
|
|
59
|
+
* node a `pc-model` loaded and declares overrides against the authored asset — components to
|
|
60
|
+
* add, properties to change, content to attach. Attributes present apply as overrides; attributes
|
|
61
|
+
* absent leave authored values untouched, and removing an attribute (or assigning `null` to the
|
|
62
|
+
* matching property) restores the authored value.
|
|
63
|
+
*
|
|
64
|
+
* `name` selects among the host model's nodes (first match in depth-first order), nesting a
|
|
65
|
+
* `pc-node` inside another scopes the search to that subtree, and `index` picks among identically
|
|
66
|
+
* named matches. When `name` matches more than one node and no `index` is given, the element
|
|
67
|
+
* warns and binds nothing.
|
|
68
|
+
*
|
|
69
|
+
* The element becomes ready once bound, and never while unresolved — a missing or ambiguous
|
|
70
|
+
* name warns and records the failure in `state`, readiness stays unresolved, and descendants
|
|
71
|
+
* wait with it.
|
|
72
|
+
*
|
|
73
|
+
* The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
|
|
74
|
+
* intersects the bound node's geometry, exactly as for `<pc-entity>`.
|
|
75
|
+
*
|
|
76
|
+
* @attribute {string} name - The name of the node to bind, resolved within the nearest ancestor
|
|
77
|
+
* `pc-model` (or `pc-node`) once it has instantiated.
|
|
78
|
+
* @attribute {number} index - Which match to bind when `name` matches more than one node,
|
|
79
|
+
* 0-based in depth-first order. Optional for a unique match; required for an ambiguous one.
|
|
80
|
+
* @attribute {boolean} enabled - Overrides the node's enabled state.
|
|
81
|
+
* @attribute {string} position - Overrides the node's local position, as an "x y z" triple.
|
|
82
|
+
* @attribute {string} rotation - Overrides the node's local rotation (Euler angles), as an
|
|
83
|
+
* "x y z" triple.
|
|
84
|
+
* @attribute {string} scale - Overrides the node's local scale, as an "x y z" triple.
|
|
85
|
+
* @attribute {string} tags - Overrides the node's tags, separated by spaces or commas.
|
|
86
|
+
* @attribute {string} onpointerenter - Script to run when the pointer moves onto the node.
|
|
87
|
+
* @attribute {string} onpointerleave - Script to run when the pointer moves off the node.
|
|
88
|
+
* @attribute {string} onpointermove - Script to run when the pointer moves over the node.
|
|
89
|
+
* @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
|
|
90
|
+
* node.
|
|
91
|
+
* @attribute {string} onpointerup - Script to run when a pointer button is released over the
|
|
92
|
+
* node.
|
|
93
|
+
* @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the node.
|
|
94
|
+
* @fires {PointerEvent} pointerleave - Fired when the pointer moves off the node.
|
|
95
|
+
* @fires {PointerEvent} pointermove - Fired when the pointer moves over the node.
|
|
96
|
+
* @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the node.
|
|
97
|
+
* @fires {PointerEvent} pointerup - Fired when a pointer button is released over the node.
|
|
98
|
+
*/
|
|
99
|
+
class NodeElement extends EntityBaseElement {
|
|
100
|
+
private _name = '';
|
|
101
|
+
|
|
102
|
+
private _index: number | null = null;
|
|
103
|
+
|
|
104
|
+
private _state: NodeBindingState = 'pending';
|
|
105
|
+
|
|
106
|
+
private _path: string | null = null;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The element whose entity roots this element's search: the nearest ancestor `pc-node`, or
|
|
110
|
+
* failing that the nearest ancestor `pc-model`. Resolved on connection.
|
|
111
|
+
*/
|
|
112
|
+
private _host: ModelElement | NodeElement | null = null;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The listener following the host's binding cycles. Both host kinds announce each cycle
|
|
116
|
+
* with a `ready` event — `pc-model` on every instantiation, `pc-node` on every bind.
|
|
117
|
+
*/
|
|
118
|
+
private _hostListener: EventListener | null = null;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The subscription to the bound entity's destruction, detached on unbind so a retargeted
|
|
122
|
+
* element cannot be reset by the eventual death of a node it no longer fronts.
|
|
123
|
+
*/
|
|
124
|
+
private _destroyHandle: EventHandle | null = null;
|
|
125
|
+
|
|
126
|
+
/** The authored values displaced by this element's overrides, captured per property. */
|
|
127
|
+
private _authored: AuthoredState = {};
|
|
128
|
+
|
|
129
|
+
// Override values. `null` means "no override": the authored value stays in force.
|
|
130
|
+
|
|
131
|
+
private _enabled: boolean | null = null;
|
|
132
|
+
|
|
133
|
+
private _position: Vec3 | null = null;
|
|
134
|
+
|
|
135
|
+
private _rotation: Vec3 | null = null;
|
|
136
|
+
|
|
137
|
+
private _scale: Vec3 | null = null;
|
|
138
|
+
|
|
139
|
+
private _tags: string[] | null = null;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The binding state: `pending` until the host instantiates and `name` resolves, `bound`
|
|
143
|
+
* once decorated, `missing`/`ambiguous`/`duplicate` when resolution failed (each also
|
|
144
|
+
* warns). Useful for asserting a document's bindings programmatically.
|
|
145
|
+
* @returns The binding state.
|
|
146
|
+
*/
|
|
147
|
+
get state(): NodeBindingState {
|
|
148
|
+
return this._state;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The path of the bound node below the search root, `/`-separated, or `null` while not
|
|
153
|
+
* bound.
|
|
154
|
+
* @returns The bound node's path, or `null`.
|
|
155
|
+
*/
|
|
156
|
+
get path(): string | null {
|
|
157
|
+
return this._path;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
connectedCallback() {
|
|
161
|
+
const host = (this.parentElement?.closest('pc-model, pc-node') ?? null) as
|
|
162
|
+
| ModelElement
|
|
163
|
+
| NodeElement
|
|
164
|
+
| null;
|
|
165
|
+
if (!host) {
|
|
166
|
+
const label = this._name ? ` '${this._name}'` : '';
|
|
167
|
+
console.warn(`pc-node${label} must be a descendant of pc-model - node not bound`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this._host = host;
|
|
172
|
+
|
|
173
|
+
// Follow the host's binding cycles. `ready` bubbles, so cycles of elements nested under
|
|
174
|
+
// the host pass through it - only the host's own count.
|
|
175
|
+
this._hostListener = (event: Event) => {
|
|
176
|
+
if (event.target !== this._host) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this._rebind();
|
|
180
|
+
};
|
|
181
|
+
host.addEventListener('ready', this._hostListener);
|
|
182
|
+
|
|
183
|
+
// The host may already be instantiated (an element inserted after load binds immediately)
|
|
184
|
+
this._rebind();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
disconnectedCallback() {
|
|
188
|
+
if (this._host && this._hostListener) {
|
|
189
|
+
this._host.removeEventListener('ready', this._hostListener);
|
|
190
|
+
}
|
|
191
|
+
this._host = null;
|
|
192
|
+
this._hostListener = null;
|
|
193
|
+
|
|
194
|
+
// Removal reverts: the model owns the node, so the entity is left as authored. Children
|
|
195
|
+
// clean up through their own disconnect behavior.
|
|
196
|
+
this._unbind();
|
|
197
|
+
this._state = 'pending';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Re-resolves the binding against the host's current hierarchy: on connection, on a `name`
|
|
202
|
+
* or `index` change, and on every host cycle (a model [re]instantiating, an enclosing
|
|
203
|
+
* `pc-node` [re]binding). When re-resolution yields the entity already bound, the binding
|
|
204
|
+
* is retained untouched — a redundant edit must not flicker overrides through a revert.
|
|
205
|
+
*/
|
|
206
|
+
private _rebind() {
|
|
207
|
+
const hostEntity = this._host?.entity ?? null;
|
|
208
|
+
|
|
209
|
+
if (!hostEntity || !this._name) {
|
|
210
|
+
// Host not instantiated (or nothing to look up yet): return to pending. An assigned
|
|
211
|
+
// name arriving later, or the host's next cycle, resolves it.
|
|
212
|
+
this._unbind();
|
|
213
|
+
this._state = 'pending';
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const target = this._resolve(hostEntity);
|
|
218
|
+
|
|
219
|
+
if (target && target === this._entity) {
|
|
220
|
+
this._path = this._pathOf(target, hostEntity);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this._unbind();
|
|
225
|
+
|
|
226
|
+
if (!target) {
|
|
227
|
+
// _resolve warned and set the failure state
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this._bind(target, hostEntity);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolves `name` (and `index`) to an entity under `hostEntity`, warning and recording the
|
|
236
|
+
* failure state when it cannot.
|
|
237
|
+
*
|
|
238
|
+
* @param hostEntity - The root of the search.
|
|
239
|
+
* @returns The resolved entity, or `null`.
|
|
240
|
+
*/
|
|
241
|
+
private _resolve(hostEntity: Entity): Entity | null {
|
|
242
|
+
const matches = hostEntity.find((node: GraphNode) => node.name === this._name) as Entity[];
|
|
243
|
+
|
|
244
|
+
if (matches.length === 0) {
|
|
245
|
+
const closest = this._closestName(hostEntity);
|
|
246
|
+
const hint = closest ? ` - closest match: '${closest}'` : '';
|
|
247
|
+
console.warn(`pc-node '${this._name}' not found in ${this._describeHost()}${hint}`);
|
|
248
|
+
this._state = 'missing';
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let target: Entity;
|
|
253
|
+
if (this._index !== null) {
|
|
254
|
+
if (this._index >= matches.length) {
|
|
255
|
+
console.warn(
|
|
256
|
+
`pc-node '${this._name}' index ${this._index} is out of range - ${matches.length} match(es) in ${this._describeHost()}`
|
|
257
|
+
);
|
|
258
|
+
this._state = 'missing';
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
target = matches[this._index];
|
|
262
|
+
} else if (matches.length > 1) {
|
|
263
|
+
// Ambiguity binds nothing: a fallback guess performs side effects on the wrong
|
|
264
|
+
// scene node, and would go wrong silently when a re-export introduces a duplicate
|
|
265
|
+
// name. The candidates tell the author exactly what to write.
|
|
266
|
+
const candidates = matches.map((m, i) => `[${i}] ${this._pathOf(m, hostEntity)}`).join(', ');
|
|
267
|
+
console.warn(
|
|
268
|
+
`pc-node '${this._name}' is ambiguous in ${this._describeHost()} - specify index: ${candidates}`
|
|
269
|
+
);
|
|
270
|
+
this._state = 'ambiguous';
|
|
271
|
+
return null;
|
|
272
|
+
} else {
|
|
273
|
+
target = matches[0];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const owner = this.closestApp?.elementFromEntity(target);
|
|
277
|
+
if (owner && owner !== this) {
|
|
278
|
+
console.warn(
|
|
279
|
+
`pc-node '${this._name}' resolves to a node already bound by another element - element ignored`
|
|
280
|
+
);
|
|
281
|
+
this._state = 'duplicate';
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return target;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Binds `target`: registers it (making it a pick target), hooks its destruction, applies
|
|
290
|
+
* this element's overrides, announces readiness and builds the deferred child subtree.
|
|
291
|
+
*
|
|
292
|
+
* @param target - The entity to bind.
|
|
293
|
+
* @param hostEntity - The search root, for the path.
|
|
294
|
+
*/
|
|
295
|
+
private _bind(target: Entity, hostEntity: Entity) {
|
|
296
|
+
this._entity = target;
|
|
297
|
+
this._registerEntity(target);
|
|
298
|
+
this._destroyHandle = target.once('destroy', this._onEntityDestroy, this);
|
|
299
|
+
this._state = 'bound';
|
|
300
|
+
this._path = this._pathOf(target, hostEntity);
|
|
301
|
+
|
|
302
|
+
this._applyOverrides();
|
|
303
|
+
this._onReady();
|
|
304
|
+
this._buildChildren();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Dissolves the current binding, restoring every authored value this element's overrides
|
|
309
|
+
* displaced and removing the decorations this binding hosts: attachment entities are
|
|
310
|
+
* destroyed (re-created against the next binding) and component decorations are removed
|
|
311
|
+
* from the abandoned node. Both sweeps are scoped by `closestEntity`, so a still-bound
|
|
312
|
+
* nested `pc-node` keeps its own decorations. Safe to call in any state.
|
|
313
|
+
*/
|
|
314
|
+
private _unbind() {
|
|
315
|
+
const entity = this._entity;
|
|
316
|
+
if (!entity) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this._revertOverrides();
|
|
321
|
+
|
|
322
|
+
// Attachment points anchor to the bound node, so they cannot outlive the binding. Each
|
|
323
|
+
// destroyed entity resets its element, which the next _buildChildren re-creates.
|
|
324
|
+
this.querySelectorAll<EntityElement>('pc-entity').forEach((child) => {
|
|
325
|
+
if (child.closestEntity === this) {
|
|
326
|
+
child.entity?.destroy();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this._destroyHandle?.off();
|
|
331
|
+
this._destroyHandle = null;
|
|
332
|
+
this._unregisterEntity(entity);
|
|
333
|
+
this._entity = null;
|
|
334
|
+
this._path = null;
|
|
335
|
+
this._authored = {};
|
|
336
|
+
|
|
337
|
+
// Component decorations come off through the same hook the host-ready cycle uses. A
|
|
338
|
+
// dissolve that never rebinds fires no ready event, so the sweep is explicit - after
|
|
339
|
+
// `_entity` is cleared, so the hook sees a host without an entity.
|
|
340
|
+
this.querySelectorAll('*').forEach((child) => {
|
|
341
|
+
if (child instanceof ComponentElement && child.closestEntity === this) {
|
|
342
|
+
child._hostCycled();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
this._resetReady();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Handles the destruction of the bound entity - its model unloading, reloading, or a script
|
|
351
|
+
* destroying it. There is nothing to revert on a destroyed entity; the element returns to
|
|
352
|
+
* pending and the host's next cycle re-resolves it.
|
|
353
|
+
*/
|
|
354
|
+
private _onEntityDestroy(entity: Entity) {
|
|
355
|
+
this._destroyHandle = null;
|
|
356
|
+
this._unregisterEntity(entity);
|
|
357
|
+
this._entity = null;
|
|
358
|
+
this._path = null;
|
|
359
|
+
this._authored = {};
|
|
360
|
+
this._state = 'pending';
|
|
361
|
+
this._resetReady();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Creates and parents the entities of child `pc-entity` elements - the attachment points.
|
|
366
|
+
* Mirrors the runtime-insertion path in EntityElement.connectedCallback: children were
|
|
367
|
+
* deferred while this host was unresolved (or reset when a previous binding dissolved), and
|
|
368
|
+
* build here once it binds.
|
|
369
|
+
*/
|
|
370
|
+
private _buildChildren() {
|
|
371
|
+
const app = this.closestApp?.app;
|
|
372
|
+
if (!app) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const childEntities = this.querySelectorAll<EntityElement>('pc-entity');
|
|
376
|
+
childEntities.forEach((child) => {
|
|
377
|
+
child._createEntity(app);
|
|
378
|
+
});
|
|
379
|
+
childEntities.forEach((child) => {
|
|
380
|
+
child._buildHierarchy(app);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Applies every override that is explicitly set, capturing the authored value it displaces.
|
|
386
|
+
*/
|
|
387
|
+
private _applyOverrides() {
|
|
388
|
+
if (this._enabled !== null) {
|
|
389
|
+
this.enabled = this._enabled;
|
|
390
|
+
}
|
|
391
|
+
if (this._position !== null) {
|
|
392
|
+
this.position = this._position;
|
|
393
|
+
}
|
|
394
|
+
if (this._rotation !== null) {
|
|
395
|
+
this.rotation = this._rotation;
|
|
396
|
+
}
|
|
397
|
+
if (this._scale !== null) {
|
|
398
|
+
this.scale = this._scale;
|
|
399
|
+
}
|
|
400
|
+
if (this._tags !== null) {
|
|
401
|
+
this.tags = this._tags;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Restores every authored value this element's overrides displaced. The override values
|
|
407
|
+
* themselves are kept - they re-apply on the next binding.
|
|
408
|
+
*/
|
|
409
|
+
private _revertOverrides() {
|
|
410
|
+
const entity = this._entity!;
|
|
411
|
+
const authored = this._authored;
|
|
412
|
+
if (authored.enabled !== undefined) {
|
|
413
|
+
entity.enabled = authored.enabled;
|
|
414
|
+
}
|
|
415
|
+
if (authored.position) {
|
|
416
|
+
entity.setLocalPosition(authored.position);
|
|
417
|
+
}
|
|
418
|
+
if (authored.rotation) {
|
|
419
|
+
entity.setLocalRotation(authored.rotation);
|
|
420
|
+
}
|
|
421
|
+
if (authored.scale) {
|
|
422
|
+
entity.setLocalScale(authored.scale);
|
|
423
|
+
}
|
|
424
|
+
if (authored.tags) {
|
|
425
|
+
entity.tags.clear();
|
|
426
|
+
entity.tags.add(authored.tags);
|
|
427
|
+
}
|
|
428
|
+
this._authored = {};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Renders the path of `node` below `root`, for the `path` property and the resolution
|
|
433
|
+
* warnings.
|
|
434
|
+
*
|
|
435
|
+
* @param node - The node to describe.
|
|
436
|
+
* @param root - The search root.
|
|
437
|
+
* @returns The `/`-separated path.
|
|
438
|
+
*/
|
|
439
|
+
private _pathOf(node: GraphNode, root: GraphNode): string {
|
|
440
|
+
const parts: string[] = [];
|
|
441
|
+
for (let current: GraphNode | null = node; current && current !== root; current = current.parent) {
|
|
442
|
+
parts.unshift(current.name);
|
|
443
|
+
}
|
|
444
|
+
return parts.join('/') || node.name;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Describes the search root for warnings: the model's asset id, or the enclosing node's
|
|
449
|
+
* name.
|
|
450
|
+
* @returns The description.
|
|
451
|
+
*/
|
|
452
|
+
private _describeHost(): string {
|
|
453
|
+
if (this._host instanceof ModelElement) {
|
|
454
|
+
return `model '${this._host.asset}'`;
|
|
455
|
+
}
|
|
456
|
+
return `pc-node '${this._host?.name ?? ''}' subtree`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Finds the node name nearest to the missing `name`, for the miss warning. The names are
|
|
461
|
+
* already in hand from resolution, so the suggestion is nearly free.
|
|
462
|
+
*
|
|
463
|
+
* @param hostEntity - The root of the search.
|
|
464
|
+
* @returns The closest name within an edit distance of 2, or `null`.
|
|
465
|
+
*/
|
|
466
|
+
private _closestName(hostEntity: Entity): string | null {
|
|
467
|
+
let best: string | null = null;
|
|
468
|
+
let bestDistance = 3;
|
|
469
|
+
hostEntity.find((node: GraphNode) => {
|
|
470
|
+
const distance = levenshtein(this._name, node.name);
|
|
471
|
+
if (distance < bestDistance) {
|
|
472
|
+
bestDistance = distance;
|
|
473
|
+
best = node.name;
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
});
|
|
477
|
+
return best;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Sets the name of the node to bind. A change retargets: the current binding's overrides
|
|
482
|
+
* revert and the new name resolves afresh. `name` on a `pc-node` is never a rename of the
|
|
483
|
+
* authored node - it is only ever a reference.
|
|
484
|
+
* @param value - The node name.
|
|
485
|
+
*/
|
|
486
|
+
set name(value: string) {
|
|
487
|
+
this._name = value;
|
|
488
|
+
if (this.isConnected && this._host) {
|
|
489
|
+
this._rebind();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Gets the name of the node to bind.
|
|
495
|
+
* @returns The node name.
|
|
496
|
+
*/
|
|
497
|
+
get name(): string {
|
|
498
|
+
return this._name;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Sets which match to bind when `name` matches more than one node, 0-based in depth-first
|
|
503
|
+
* order. A change retargets, like `name`. `null` means unset - required when the name is
|
|
504
|
+
* ambiguous, optional otherwise.
|
|
505
|
+
* @param value - The match index, or `null`.
|
|
506
|
+
*/
|
|
507
|
+
set index(value: number | null) {
|
|
508
|
+
this._index = value;
|
|
509
|
+
if (this.isConnected && this._host) {
|
|
510
|
+
this._rebind();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Gets which match to bind.
|
|
516
|
+
* @returns The match index, or `null` when unset.
|
|
517
|
+
*/
|
|
518
|
+
get index(): number | null {
|
|
519
|
+
return this._index;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Sets the enabled override. `null` clears it, restoring the authored state.
|
|
524
|
+
* @param value - The enabled state, or `null`.
|
|
525
|
+
*/
|
|
526
|
+
set enabled(value: boolean | null) {
|
|
527
|
+
this._enabled = value;
|
|
528
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
529
|
+
if (!entity) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (value !== null) {
|
|
533
|
+
this._authored.enabled ??= entity.enabled;
|
|
534
|
+
entity.enabled = value;
|
|
535
|
+
} else if (this._authored.enabled !== undefined) {
|
|
536
|
+
entity.enabled = this._authored.enabled;
|
|
537
|
+
delete this._authored.enabled;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Gets the enabled override.
|
|
543
|
+
* @returns The enabled state, or `null` while no override is set.
|
|
544
|
+
*/
|
|
545
|
+
get enabled(): boolean | null {
|
|
546
|
+
return this._enabled;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Sets the local position override. `null` clears it, restoring the authored position.
|
|
551
|
+
* @param value - The position, or `null`.
|
|
552
|
+
*/
|
|
553
|
+
set position(value: Vec3 | null) {
|
|
554
|
+
this._position = value;
|
|
555
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
556
|
+
if (!entity) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (value !== null) {
|
|
560
|
+
this._authored.position ??= entity.getLocalPosition().clone();
|
|
561
|
+
entity.setLocalPosition(value);
|
|
562
|
+
} else if (this._authored.position) {
|
|
563
|
+
entity.setLocalPosition(this._authored.position);
|
|
564
|
+
delete this._authored.position;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Gets the local position override.
|
|
570
|
+
* @returns The position, or `null` while no override is set.
|
|
571
|
+
*/
|
|
572
|
+
get position(): Vec3 | null {
|
|
573
|
+
return this._position;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Sets the local rotation override, as Euler angles in degrees. `null` clears it, restoring
|
|
578
|
+
* the authored rotation.
|
|
579
|
+
* @param value - The rotation, or `null`.
|
|
580
|
+
*/
|
|
581
|
+
set rotation(value: Vec3 | null) {
|
|
582
|
+
this._rotation = value;
|
|
583
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
584
|
+
if (!entity) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (value !== null) {
|
|
588
|
+
// The authored rotation is cached as a quaternion: it restores exactly, where a
|
|
589
|
+
// round trip through Euler angles need not.
|
|
590
|
+
this._authored.rotation ??= entity.getLocalRotation().clone();
|
|
591
|
+
entity.setLocalEulerAngles(value);
|
|
592
|
+
} else if (this._authored.rotation) {
|
|
593
|
+
entity.setLocalRotation(this._authored.rotation);
|
|
594
|
+
delete this._authored.rotation;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Gets the local rotation override.
|
|
600
|
+
* @returns The rotation, or `null` while no override is set.
|
|
601
|
+
*/
|
|
602
|
+
get rotation(): Vec3 | null {
|
|
603
|
+
return this._rotation;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Sets the local scale override. `null` clears it, restoring the authored scale.
|
|
608
|
+
* @param value - The scale, or `null`.
|
|
609
|
+
*/
|
|
610
|
+
set scale(value: Vec3 | null) {
|
|
611
|
+
this._scale = value;
|
|
612
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
613
|
+
if (!entity) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (value !== null) {
|
|
617
|
+
this._authored.scale ??= entity.getLocalScale().clone();
|
|
618
|
+
entity.setLocalScale(value);
|
|
619
|
+
} else if (this._authored.scale) {
|
|
620
|
+
entity.setLocalScale(this._authored.scale);
|
|
621
|
+
delete this._authored.scale;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Gets the local scale override.
|
|
627
|
+
* @returns The scale, or `null` while no override is set.
|
|
628
|
+
*/
|
|
629
|
+
get scale(): Vec3 | null {
|
|
630
|
+
return this._scale;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Sets the tags override. `null` clears it, restoring the authored tags.
|
|
635
|
+
* @param value - The tags, or `null`.
|
|
636
|
+
*/
|
|
637
|
+
set tags(value: string[] | null) {
|
|
638
|
+
this._tags = value;
|
|
639
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
640
|
+
if (!entity) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (value !== null) {
|
|
644
|
+
this._authored.tags ??= entity.tags.list().slice();
|
|
645
|
+
entity.tags.clear();
|
|
646
|
+
entity.tags.add(value);
|
|
647
|
+
} else if (this._authored.tags) {
|
|
648
|
+
entity.tags.clear();
|
|
649
|
+
entity.tags.add(this._authored.tags);
|
|
650
|
+
delete this._authored.tags;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Gets the tags override.
|
|
656
|
+
* @returns The tags, or `null` while no override is set.
|
|
657
|
+
*/
|
|
658
|
+
get tags(): string[] | null {
|
|
659
|
+
return this._tags;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
static get observedAttributes() {
|
|
663
|
+
return ['enabled', 'index', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
667
|
+
switch (name) {
|
|
668
|
+
case 'enabled':
|
|
669
|
+
this.enabled = newValue === null ? null : parseBool(newValue, true);
|
|
670
|
+
break;
|
|
671
|
+
case 'index':
|
|
672
|
+
if (newValue === null) {
|
|
673
|
+
this.index = null;
|
|
674
|
+
} else {
|
|
675
|
+
// Number('') is 0, which would make index="" silently mean the first match
|
|
676
|
+
const index = newValue.trim() === '' ? NaN : Number(newValue);
|
|
677
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
678
|
+
// Invalid values are treated as absent: under ambiguity that means
|
|
679
|
+
// unbound, the fail-safe direction.
|
|
680
|
+
console.warn(`pc-node index '${newValue}' is not a non-negative integer - treated as absent`);
|
|
681
|
+
this.index = null;
|
|
682
|
+
} else {
|
|
683
|
+
this.index = index;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
break;
|
|
687
|
+
case 'name':
|
|
688
|
+
this.name = newValue ?? '';
|
|
689
|
+
break;
|
|
690
|
+
case 'position':
|
|
691
|
+
this.position = newValue === null ? null : parseVec3(newValue, Vec3.ZERO, name);
|
|
692
|
+
break;
|
|
693
|
+
case 'rotation':
|
|
694
|
+
this.rotation = newValue === null ? null : parseVec3(newValue, Vec3.ZERO, name);
|
|
695
|
+
break;
|
|
696
|
+
case 'scale':
|
|
697
|
+
this.scale = newValue === null ? null : parseVec3(newValue, Vec3.ONE, name);
|
|
698
|
+
break;
|
|
699
|
+
case 'tags':
|
|
700
|
+
this.tags = newValue === null ? null : parseTags(newValue);
|
|
701
|
+
break;
|
|
702
|
+
case 'onpointerenter':
|
|
703
|
+
case 'onpointerleave':
|
|
704
|
+
case 'onpointerdown':
|
|
705
|
+
case 'onpointerup':
|
|
706
|
+
case 'onpointermove':
|
|
707
|
+
this._updateInlineHandler(name, newValue);
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
customElements.define('pc-node', NodeElement);
|
|
714
|
+
|
|
715
|
+
export { NodeElement };
|