@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/async-element.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppElement } from './app';
|
|
2
|
-
import type {
|
|
2
|
+
import type { EntityBaseElement } from './entity-base';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Base class for all PlayCanvas Web Components that initialize asynchronously.
|
|
@@ -33,12 +33,13 @@ class AsyncElement extends HTMLElement {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* The nearest ancestor
|
|
37
|
-
* ancestor. The search starts at the parent, so an element
|
|
38
|
-
*
|
|
36
|
+
* The nearest ancestor element that fronts an entity — `<pc-entity>` or `<pc-node>` — or
|
|
37
|
+
* `null` if this element has no such ancestor. The search starts at the parent, so an element
|
|
38
|
+
* never resolves to itself.
|
|
39
|
+
* @returns The closest entity-fronting element, or `null`.
|
|
39
40
|
*/
|
|
40
|
-
get closestEntity():
|
|
41
|
-
return (this.parentElement?.closest('pc-entity') as
|
|
41
|
+
get closestEntity(): EntityBaseElement | null {
|
|
42
|
+
return (this.parentElement?.closest('pc-entity, pc-node') as EntityBaseElement | null) ?? null;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
@@ -11,6 +11,12 @@ import { ComponentElement } from './component';
|
|
|
11
11
|
* The CollisionComponentElement interface also inherits the properties and methods of the
|
|
12
12
|
* {@link HTMLElement} interface.
|
|
13
13
|
*
|
|
14
|
+
* For `type="mesh"`, the collision geometry defaults to the host entity's own render component
|
|
15
|
+
* (its render asset) — a collider matching the visible mesh, which is what a mesh collider on a
|
|
16
|
+
* glTF node means. The default resolves each time the component applies, so a `pc-node` that
|
|
17
|
+
* retargets or rebinds picks up the new node's geometry. An entity with no asset-backed render
|
|
18
|
+
* component warns, and the collider has no shape.
|
|
19
|
+
*
|
|
14
20
|
* @category Components
|
|
15
21
|
*/
|
|
16
22
|
class CollisionComponentElement extends ComponentElement {
|
|
@@ -48,6 +54,34 @@ class CollisionComponentElement extends ComponentElement {
|
|
|
48
54
|
};
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
protected initComponent() {
|
|
58
|
+
this._applyMeshGeometryDefault();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Defaults a mesh collider's geometry to the host entity's own render component. The
|
|
63
|
+
* engine's mesh collider only works with explicitly supplied geometry, and the element has
|
|
64
|
+
* no attribute to supply it - so the host's visible geometry, the meaning a mesh collider
|
|
65
|
+
* on a glTF node carries, fills the gap. Runs on every application (so a rebound `pc-node`
|
|
66
|
+
* recomputes it) and on a runtime switch to `type="mesh"`; an explicitly assigned
|
|
67
|
+
* `renderAsset` is never overwritten.
|
|
68
|
+
*/
|
|
69
|
+
private _applyMeshGeometryDefault() {
|
|
70
|
+
const component = this.component;
|
|
71
|
+
if (!component || this._type !== 'mesh' || component.renderAsset !== null) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const asset = component.entity.render?.asset ?? null;
|
|
76
|
+
if (asset === null) {
|
|
77
|
+
console.warn(
|
|
78
|
+
`pc-collision type="mesh" on '${component.entity.name}' found no asset-backed render component to take geometry from - collider has no shape`
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
component.renderAsset = asset;
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
/**
|
|
52
86
|
* Gets the underlying PlayCanvas collision component.
|
|
53
87
|
* @returns The collision component.
|
|
@@ -137,6 +171,7 @@ class CollisionComponentElement extends ComponentElement {
|
|
|
137
171
|
this._type = value;
|
|
138
172
|
if (this.component) {
|
|
139
173
|
this.component.type = value;
|
|
174
|
+
this._applyMeshGeometryDefault();
|
|
140
175
|
}
|
|
141
176
|
}
|
|
142
177
|
|
|
@@ -2,6 +2,7 @@ import type { Component } from 'playcanvas';
|
|
|
2
2
|
|
|
3
3
|
import type { AppElement } from '../app';
|
|
4
4
|
import { AsyncElement } from '../async-element';
|
|
5
|
+
import type { EntityBaseElement } from '../entity-base';
|
|
5
6
|
import { parseBool } from '../parse';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -18,6 +19,18 @@ class ComponentElement extends AsyncElement {
|
|
|
18
19
|
|
|
19
20
|
private _appElement: AppElement | null = null;
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The element hosting this component, held so the host's readiness cycles can be observed
|
|
24
|
+
* even after `closestEntity` would no longer resolve (during teardown).
|
|
25
|
+
*/
|
|
26
|
+
private _hostElement: EntityBaseElement | null = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The listener re-applying this component when the host's readiness cycles. Held for
|
|
30
|
+
* removal on disconnect.
|
|
31
|
+
*/
|
|
32
|
+
private _hostReadyListener: EventListener | null = null;
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Incremented on every connect and disconnect. connectedCallback captures the value on entry
|
|
23
36
|
* and abandons itself wherever it resumes from an await if the value has moved on — so a
|
|
@@ -49,6 +62,44 @@ class ComponentElement extends AsyncElement {
|
|
|
49
62
|
return {};
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Creates the component on the host's current entity, removing it first from a previous
|
|
67
|
+
* entity that is still alive (a retargeted `<pc-node>` moves its decorations with it). When
|
|
68
|
+
* the entity already has a component of this type — a glTF node arriving with its authored
|
|
69
|
+
* `render` component, say — warns and leaves `component` null. The element-level warning is
|
|
70
|
+
* load-bearing: the engine's own duplicate-addComponent warning is Debug-stripped from
|
|
71
|
+
* production builds, which would otherwise leave a silent null.
|
|
72
|
+
*/
|
|
73
|
+
private _applyComponent() {
|
|
74
|
+
const entity = this._hostElement?.entity ?? null;
|
|
75
|
+
if (this._component && this._component.entity === entity) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A retarget leaves the previous component on a still-live entity - remove it so the
|
|
80
|
+
// decoration follows the element, or vanishes with a dissolved binding. A destroyed
|
|
81
|
+
// entity took its components with it.
|
|
82
|
+
const previous = this._component;
|
|
83
|
+
if (previous?.entity && previous.entity.c[this._componentName] === previous) {
|
|
84
|
+
previous.entity.removeComponent(this._componentName);
|
|
85
|
+
}
|
|
86
|
+
this._component = null;
|
|
87
|
+
|
|
88
|
+
if (!entity) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (entity.c[this._componentName]) {
|
|
93
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
94
|
+
console.warn(
|
|
95
|
+
`${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
|
|
101
|
+
}
|
|
102
|
+
|
|
52
103
|
private async _addComponent() {
|
|
53
104
|
const generation = this._connectionGeneration;
|
|
54
105
|
|
|
@@ -71,9 +122,42 @@ class ComponentElement extends AsyncElement {
|
|
|
71
122
|
return;
|
|
72
123
|
}
|
|
73
124
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
125
|
+
this._hostElement = entityElement;
|
|
126
|
+
this._applyComponent();
|
|
127
|
+
|
|
128
|
+
// Re-apply when the host's readiness cycles without this element disconnecting: a
|
|
129
|
+
// `<pc-node>` rebinding after its model reloads or retargets, or a re-created entity.
|
|
130
|
+
// The 'ready' event bubbles, so events from descendants pass through this host - only
|
|
131
|
+
// the host's own cycles count. Readiness is cycled here too, so decorations one level
|
|
132
|
+
// down re-apply the same way.
|
|
133
|
+
this._hostReadyListener = (event: Event) => {
|
|
134
|
+
if (event.target !== this._hostElement) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (generation !== this._connectionGeneration) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this._hostCycled();
|
|
141
|
+
};
|
|
142
|
+
entityElement.addEventListener('ready', this._hostReadyListener);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Re-evaluates this component against the host's current entity: applied to a new entity,
|
|
147
|
+
* moved from a still-live old one, or removed when the host no longer fronts an entity at
|
|
148
|
+
* all. Readiness follows - it cycles with a re-application and stays unresolved while the
|
|
149
|
+
* host is unbound. Called by the host-ready listener, and directly by a `<pc-node>`
|
|
150
|
+
* dissolving its binding: the one transition that fires no ready event to ride.
|
|
151
|
+
*
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
_hostCycled() {
|
|
155
|
+
this._resetReady();
|
|
156
|
+
this._applyComponent();
|
|
157
|
+
if (this._hostElement?.entity) {
|
|
158
|
+
this.initComponent();
|
|
159
|
+
this._onReady();
|
|
160
|
+
}
|
|
77
161
|
}
|
|
78
162
|
|
|
79
163
|
/**
|
|
@@ -111,6 +195,12 @@ class ComponentElement extends AsyncElement {
|
|
|
111
195
|
// Invalidate any connectedCallback still suspended on an await
|
|
112
196
|
this._connectionGeneration++;
|
|
113
197
|
|
|
198
|
+
if (this._hostElement && this._hostReadyListener) {
|
|
199
|
+
this._hostElement.removeEventListener('ready', this._hostReadyListener);
|
|
200
|
+
}
|
|
201
|
+
this._hostElement = null;
|
|
202
|
+
this._hostReadyListener = null;
|
|
203
|
+
|
|
114
204
|
// Remove the component when the element is disconnected. Skip this when the owning
|
|
115
205
|
// application has already been destroyed — removing a <pc-app> disconnects it before
|
|
116
206
|
// its children, taking the component systems with it.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Entity } from 'playcanvas';
|
|
2
|
+
|
|
3
|
+
import type { AppElement } from './app';
|
|
4
|
+
import { AsyncElement } from './async-element';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The attribute names of the inline `onpointer*` event handlers, shared by every element that
|
|
8
|
+
* fronts an engine entity. Spread into `observedAttributes` by subclasses.
|
|
9
|
+
* @ignore
|
|
10
|
+
*/
|
|
11
|
+
const POINTER_ATTRIBUTES = [
|
|
12
|
+
'onpointerenter',
|
|
13
|
+
'onpointerleave',
|
|
14
|
+
'onpointerdown',
|
|
15
|
+
'onpointerup',
|
|
16
|
+
'onpointermove'
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The base class for elements that front an engine {@link Entity}: `<pc-entity>`, which creates
|
|
21
|
+
* one, and `<pc-node>`, which binds to one inside a model's instantiated hierarchy. It carries
|
|
22
|
+
* what both need — the `entity` contract, registration with the owning application (which joins
|
|
23
|
+
* picked scene nodes back to elements by identity, never by name), and the pointer listener
|
|
24
|
+
* bookkeeping that lets the application lazily attach its canvas handlers.
|
|
25
|
+
*/
|
|
26
|
+
class EntityBaseElement extends AsyncElement {
|
|
27
|
+
protected _entity: Entity | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The application element this entity is registered with, cached at registration time so the
|
|
31
|
+
* entity can be unregistered even once this element has left the DOM.
|
|
32
|
+
*/
|
|
33
|
+
protected _appElement: AppElement | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The pointer event listeners for the entity.
|
|
37
|
+
*/
|
|
38
|
+
private _listeners: Record<string, EventListener[]> = {};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The event types for which an inline `onpointer*` attribute is currently present.
|
|
42
|
+
*/
|
|
43
|
+
private _inlineHandlerTypes = new Set<string>();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The PlayCanvas entity instance. `null` until the element is ready, and again once the
|
|
47
|
+
* entity is gone — await {@link whenReady} or the element's `ready()` promise before
|
|
48
|
+
* accessing it.
|
|
49
|
+
* @returns The entity instance, or `null`.
|
|
50
|
+
*/
|
|
51
|
+
get entity(): Entity | null {
|
|
52
|
+
return this._entity;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Registers `entity` as this element's backing entity with the owning application, which
|
|
57
|
+
* joins engine nodes back to elements by identity (never by name).
|
|
58
|
+
*
|
|
59
|
+
* @param entity - The entity to register.
|
|
60
|
+
*/
|
|
61
|
+
protected _registerEntity(entity: Entity) {
|
|
62
|
+
this._appElement = this.closestApp;
|
|
63
|
+
this._appElement?._registerEntityElement(entity, this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Removes the registration for `entity`.
|
|
68
|
+
*
|
|
69
|
+
* @param entity - The entity to unregister.
|
|
70
|
+
*/
|
|
71
|
+
protected _unregisterEntity(entity: Entity) {
|
|
72
|
+
this._appElement?._unregisterEntityElement(entity);
|
|
73
|
+
this._appElement = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
|
|
78
|
+
* runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
|
|
79
|
+
* the previous handler and removing it removes the handler, exactly like `onclick` on any
|
|
80
|
+
* HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
|
|
81
|
+
* bookkeeping that lets the application lazily attach its canvas pointer handlers must be
|
|
82
|
+
* kept in sync here.
|
|
83
|
+
*
|
|
84
|
+
* @param name - The attribute name (e.g. 'onpointerdown').
|
|
85
|
+
* @param value - The attribute value, or `null` when the attribute has been removed.
|
|
86
|
+
*/
|
|
87
|
+
protected _updateInlineHandler(name: string, value: string | null) {
|
|
88
|
+
const type = name.substring(2);
|
|
89
|
+
const had = this._inlineHandlerTypes.has(type);
|
|
90
|
+
const has = value !== null;
|
|
91
|
+
|
|
92
|
+
if (has && !had) {
|
|
93
|
+
this._inlineHandlerTypes.add(type);
|
|
94
|
+
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
95
|
+
} else if (!has && had) {
|
|
96
|
+
this._inlineHandlerTypes.delete(type);
|
|
97
|
+
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) {
|
|
102
|
+
if (!this._listeners[type]) {
|
|
103
|
+
this._listeners[type] = [];
|
|
104
|
+
}
|
|
105
|
+
this._listeners[type].push(listener);
|
|
106
|
+
super.addEventListener(type, listener, options);
|
|
107
|
+
if (type.startsWith('pointer')) {
|
|
108
|
+
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
removeEventListener(type: string, listener: EventListener, options?: boolean | EventListenerOptions) {
|
|
113
|
+
if (this._listeners[type]) {
|
|
114
|
+
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
115
|
+
}
|
|
116
|
+
super.removeEventListener(type, listener, options);
|
|
117
|
+
if (type.startsWith('pointer')) {
|
|
118
|
+
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether the element has a listener for an event type, registered either with
|
|
124
|
+
* {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
|
|
125
|
+
* containing `<pc-app>` element to gate pointer event synthesis.
|
|
126
|
+
*
|
|
127
|
+
* @param type - The event type.
|
|
128
|
+
* @returns Whether a listener is registered.
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
_hasListeners(type: string): boolean {
|
|
132
|
+
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { EntityBaseElement, POINTER_ATTRIBUTES };
|
package/src/entity.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { AppBase } from 'playcanvas';
|
|
2
2
|
import { Entity, Vec3 } from 'playcanvas';
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import { AsyncElement } from './async-element';
|
|
4
|
+
import { EntityBaseElement, POINTER_ATTRIBUTES } from './entity-base';
|
|
6
5
|
import { parseBool, parseTags, parseVec3 } from './parse';
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -29,7 +28,7 @@ import { parseBool, parseTags, parseVec3 } from './parse';
|
|
|
29
28
|
* @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
|
|
30
29
|
* @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
|
|
31
30
|
*/
|
|
32
|
-
class EntityElement extends
|
|
31
|
+
class EntityElement extends EntityBaseElement {
|
|
33
32
|
/**
|
|
34
33
|
* Whether the entity is enabled.
|
|
35
34
|
*/
|
|
@@ -60,39 +59,11 @@ class EntityElement extends AsyncElement {
|
|
|
60
59
|
*/
|
|
61
60
|
private _tags: string[] = [];
|
|
62
61
|
|
|
63
|
-
/**
|
|
64
|
-
* The pointer event listeners for the entity.
|
|
65
|
-
*/
|
|
66
|
-
private _listeners: Record<string, EventListener[]> = {};
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The event types for which an inline `onpointer*` attribute is currently present.
|
|
70
|
-
*/
|
|
71
|
-
private _inlineHandlerTypes = new Set<string>();
|
|
72
|
-
|
|
73
62
|
/**
|
|
74
63
|
* Whether the hierarchy has been built for this entity.
|
|
75
64
|
*/
|
|
76
65
|
private _built = false;
|
|
77
66
|
|
|
78
|
-
private _entity: Entity | null = null;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* The application element this entity is registered with, cached at creation time so the
|
|
82
|
-
* entity can be unregistered even once this element has left the DOM.
|
|
83
|
-
*/
|
|
84
|
-
private _appElement: AppElement | null = null;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* The PlayCanvas entity instance. `null` until the element is ready, and again once it has
|
|
88
|
-
* been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
89
|
-
* promise before accessing it.
|
|
90
|
-
* @returns The entity instance, or `null`.
|
|
91
|
-
*/
|
|
92
|
-
get entity(): Entity | null {
|
|
93
|
-
return this._entity;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
67
|
/**
|
|
97
68
|
* Creates the backing entity. Called by the containing `<pc-app>` element during its boot
|
|
98
69
|
* sweep, and on connection for elements inserted while the application is already running.
|
|
@@ -124,13 +95,11 @@ class EntityElement extends AsyncElement {
|
|
|
124
95
|
entity.tags.add(this._tags);
|
|
125
96
|
}
|
|
126
97
|
|
|
127
|
-
// Register with the owning application
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
this._appElement = this.closestApp;
|
|
133
|
-
this._appElement?._registerEntityElement(entity, this);
|
|
98
|
+
// Register with the owning application and hook the entity's destruction. The engine
|
|
99
|
+
// fires 'destroy' for every entity in a destroyed subtree, so the element learns of its
|
|
100
|
+
// entity's death no matter who causes it: this element, an ancestor, the whole
|
|
101
|
+
// application, or a user script calling entity.destroy().
|
|
102
|
+
this._registerEntity(entity);
|
|
134
103
|
entity.once('destroy', this._onEntityDestroy, this);
|
|
135
104
|
}
|
|
136
105
|
|
|
@@ -144,27 +113,35 @@ class EntityElement extends AsyncElement {
|
|
|
144
113
|
* @param entity - The entity that was destroyed.
|
|
145
114
|
*/
|
|
146
115
|
private _onEntityDestroy(entity: Entity) {
|
|
147
|
-
this.
|
|
148
|
-
this._appElement = null;
|
|
116
|
+
this._unregisterEntity(entity);
|
|
149
117
|
this._entity = null;
|
|
150
118
|
this._built = false;
|
|
151
119
|
this._resetReady();
|
|
152
120
|
}
|
|
153
121
|
|
|
154
122
|
/**
|
|
155
|
-
* Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>`
|
|
156
|
-
* there is one, and under the application root otherwise. Called by the
|
|
157
|
-
* element once a sweep has created every entity, so a parent's
|
|
158
|
-
* document order.
|
|
123
|
+
* Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` or
|
|
124
|
+
* `<pc-node>` when there is one, and under the application root otherwise. Called by the
|
|
125
|
+
* containing `<pc-app>` element once a sweep has created every entity, so a parent's
|
|
126
|
+
* existence never depends on document order.
|
|
159
127
|
*
|
|
160
128
|
* @param app - The application whose root adopts parentless entities.
|
|
161
129
|
* @internal
|
|
162
130
|
*/
|
|
163
131
|
_buildHierarchy(app: AppBase) {
|
|
164
132
|
if (!this.entity || this._built) return;
|
|
165
|
-
this._built = true;
|
|
166
133
|
|
|
167
134
|
const closestEntity = this.closestEntity;
|
|
135
|
+
|
|
136
|
+
// A host element without an entity is an unresolved `<pc-node>`: building now would
|
|
137
|
+
// mis-anchor this entity to the application root while the host is still resolving.
|
|
138
|
+
// Stay unbuilt - the host drives this subtree itself once it binds.
|
|
139
|
+
if (closestEntity && !closestEntity.entity) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this._built = true;
|
|
144
|
+
|
|
168
145
|
if (closestEntity?.entity) {
|
|
169
146
|
closestEntity.entity.addChild(this.entity);
|
|
170
147
|
} else {
|
|
@@ -328,45 +305,8 @@ class EntityElement extends AsyncElement {
|
|
|
328
305
|
return this._tags;
|
|
329
306
|
}
|
|
330
307
|
|
|
331
|
-
/**
|
|
332
|
-
* Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
|
|
333
|
-
* runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
|
|
334
|
-
* the previous handler and removing it removes the handler, exactly like `onclick` on any
|
|
335
|
-
* HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
|
|
336
|
-
* bookkeeping that lets the application lazily attach its canvas pointer handlers must be
|
|
337
|
-
* kept in sync here.
|
|
338
|
-
*
|
|
339
|
-
* @param name - The attribute name (e.g. 'onpointerdown').
|
|
340
|
-
* @param value - The attribute value, or `null` when the attribute has been removed.
|
|
341
|
-
*/
|
|
342
|
-
private _updateInlineHandler(name: string, value: string | null) {
|
|
343
|
-
const type = name.substring(2);
|
|
344
|
-
const had = this._inlineHandlerTypes.has(type);
|
|
345
|
-
const has = value !== null;
|
|
346
|
-
|
|
347
|
-
if (has && !had) {
|
|
348
|
-
this._inlineHandlerTypes.add(type);
|
|
349
|
-
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
350
|
-
} else if (!has && had) {
|
|
351
|
-
this._inlineHandlerTypes.delete(type);
|
|
352
|
-
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
308
|
static get observedAttributes() {
|
|
357
|
-
return [
|
|
358
|
-
'enabled',
|
|
359
|
-
'name',
|
|
360
|
-
'position',
|
|
361
|
-
'rotation',
|
|
362
|
-
'scale',
|
|
363
|
-
'tags',
|
|
364
|
-
'onpointerenter',
|
|
365
|
-
'onpointerleave',
|
|
366
|
-
'onpointerdown',
|
|
367
|
-
'onpointerup',
|
|
368
|
-
'onpointermove'
|
|
369
|
-
];
|
|
309
|
+
return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
|
|
370
310
|
}
|
|
371
311
|
|
|
372
312
|
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
@@ -398,40 +338,6 @@ class EntityElement extends AsyncElement {
|
|
|
398
338
|
break;
|
|
399
339
|
}
|
|
400
340
|
}
|
|
401
|
-
|
|
402
|
-
addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) {
|
|
403
|
-
if (!this._listeners[type]) {
|
|
404
|
-
this._listeners[type] = [];
|
|
405
|
-
}
|
|
406
|
-
this._listeners[type].push(listener);
|
|
407
|
-
super.addEventListener(type, listener, options);
|
|
408
|
-
if (type.startsWith('pointer')) {
|
|
409
|
-
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
removeEventListener(type: string, listener: EventListener, options?: boolean | EventListenerOptions) {
|
|
414
|
-
if (this._listeners[type]) {
|
|
415
|
-
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
416
|
-
}
|
|
417
|
-
super.removeEventListener(type, listener, options);
|
|
418
|
-
if (type.startsWith('pointer')) {
|
|
419
|
-
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Whether the element has a listener for an event type, registered either with
|
|
425
|
-
* {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
|
|
426
|
-
* containing `<pc-app>` element to gate pointer event synthesis.
|
|
427
|
-
*
|
|
428
|
-
* @param type - The event type.
|
|
429
|
-
* @returns Whether a listener is registered.
|
|
430
|
-
* @internal
|
|
431
|
-
*/
|
|
432
|
-
_hasListeners(type: string): boolean {
|
|
433
|
-
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
434
|
-
}
|
|
435
341
|
}
|
|
436
342
|
|
|
437
343
|
customElements.define('pc-entity', EntityElement);
|
package/src/index.ts
CHANGED
|
@@ -35,8 +35,10 @@ import { ScriptElement } from './components/script';
|
|
|
35
35
|
import { SoundComponentElement } from './components/sound-component';
|
|
36
36
|
import { SoundSlotElement } from './components/sound-slot';
|
|
37
37
|
import { GSplatComponentElement } from './components/gsplat-component';
|
|
38
|
+
import { EntityBaseElement } from './entity-base';
|
|
38
39
|
import { MaterialElement } from './material';
|
|
39
40
|
import { ModelElement } from './model';
|
|
41
|
+
import { NodeElement } from './node';
|
|
40
42
|
import { SceneElement } from './scene';
|
|
41
43
|
import { SkyElement } from './sky';
|
|
42
44
|
|
|
@@ -69,6 +71,7 @@ declare global {
|
|
|
69
71
|
'pc-material': MaterialElement;
|
|
70
72
|
'pc-model': ModelElement;
|
|
71
73
|
'pc-module': ModuleElement;
|
|
74
|
+
'pc-node': NodeElement;
|
|
72
75
|
'pc-particles': ParticleSystemComponentElement;
|
|
73
76
|
'pc-render': RenderComponentElement;
|
|
74
77
|
'pc-rigidbody': RigidBodyComponentElement;
|
|
@@ -110,8 +113,10 @@ export {
|
|
|
110
113
|
SoundComponentElement,
|
|
111
114
|
SoundSlotElement,
|
|
112
115
|
GSplatComponentElement,
|
|
116
|
+
EntityBaseElement,
|
|
113
117
|
MaterialElement,
|
|
114
118
|
ModelElement,
|
|
119
|
+
NodeElement,
|
|
115
120
|
SceneElement,
|
|
116
121
|
SkyElement,
|
|
117
122
|
whenReady
|
package/src/material.ts
CHANGED
|
@@ -542,12 +542,12 @@ class MaterialElement extends HTMLElement {
|
|
|
542
542
|
|
|
543
543
|
/**
|
|
544
544
|
* @param slot - The material property to write.
|
|
545
|
-
* @param texture - The loaded texture
|
|
545
|
+
* @param texture - The loaded texture, applied with its sampler state untouched - anisotropy
|
|
546
|
+
* and friends belong to the `pc-asset`'s texture options.
|
|
546
547
|
*/
|
|
547
548
|
private _applyMap(slot: TextureSlot, texture: Texture) {
|
|
548
549
|
if (!this.material) return;
|
|
549
550
|
this.material[slot] = texture;
|
|
550
|
-
texture.anisotropy = 4;
|
|
551
551
|
this._scheduleUpdate();
|
|
552
552
|
}
|
|
553
553
|
|