@playcanvas/web-components 0.8.2 → 0.10.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.
Files changed (79) hide show
  1. package/README.md +18 -0
  2. package/dist/app.d.ts +13 -4
  3. package/dist/asset.d.ts +33 -3
  4. package/dist/async-element.d.ts +54 -4
  5. package/dist/components/button-component.d.ts +11 -5
  6. package/dist/components/camera-component.d.ts +10 -5
  7. package/dist/components/collision-component.d.ts +10 -5
  8. package/dist/components/component.d.ts +8 -2
  9. package/dist/components/element-component.d.ts +18 -13
  10. package/dist/components/gsplat-component.d.ts +7 -2
  11. package/dist/components/layoutchild-component.d.ts +7 -2
  12. package/dist/components/layoutgroup-component.d.ts +22 -16
  13. package/dist/components/light-component.d.ts +13 -7
  14. package/dist/components/listener-component.d.ts +6 -1
  15. package/dist/components/particlesystem-component.d.ts +7 -2
  16. package/dist/components/render-component.d.ts +14 -5
  17. package/dist/components/rigidbody-component.d.ts +10 -5
  18. package/dist/components/screen-component.d.ts +7 -2
  19. package/dist/components/script-component.d.ts +116 -16
  20. package/dist/components/script.d.ts +79 -8
  21. package/dist/components/scrollbar-component.d.ts +11 -5
  22. package/dist/components/scrollview-component.d.ts +18 -11
  23. package/dist/components/sound-component.d.ts +7 -2
  24. package/dist/components/sound-slot.d.ts +17 -4
  25. package/dist/custom-elements.json +16231 -0
  26. package/dist/entity.d.ts +45 -3
  27. package/dist/index.d.ts +3 -2
  28. package/dist/material.d.ts +976 -4
  29. package/dist/model.d.ts +6 -1
  30. package/dist/module.d.ts +15 -0
  31. package/dist/parse.d.ts +144 -0
  32. package/dist/pwc.cjs +5735 -2923
  33. package/dist/pwc.cjs.map +1 -1
  34. package/dist/pwc.js +5735 -2923
  35. package/dist/pwc.js.map +1 -1
  36. package/dist/pwc.min.js +1 -1
  37. package/dist/pwc.min.js.map +1 -1
  38. package/dist/pwc.min.mjs +2 -0
  39. package/dist/pwc.min.mjs.map +1 -0
  40. package/dist/pwc.mjs +5736 -2925
  41. package/dist/pwc.mjs.map +1 -1
  42. package/dist/scene.d.ts +15 -6
  43. package/dist/sky.d.ts +7 -1
  44. package/dist/vscode.html-custom-data.json +1795 -0
  45. package/dist/web-types.json +3592 -0
  46. package/package.json +32 -14
  47. package/src/app.ts +42 -15
  48. package/src/asset.ts +78 -8
  49. package/src/async-element.ts +89 -5
  50. package/src/components/button-component.ts +31 -24
  51. package/src/components/camera-component.ts +30 -24
  52. package/src/components/collision-component.ts +20 -14
  53. package/src/components/component.ts +33 -14
  54. package/src/components/element-component.ts +62 -56
  55. package/src/components/gsplat-component.ts +16 -9
  56. package/src/components/layoutchild-component.ts +17 -10
  57. package/src/components/layoutgroup-component.ts +39 -32
  58. package/src/components/light-component.ts +34 -32
  59. package/src/components/listener-component.ts +8 -2
  60. package/src/components/particlesystem-component.ts +10 -4
  61. package/src/components/render-component.ts +28 -12
  62. package/src/components/rigidbody-component.ts +22 -16
  63. package/src/components/screen-component.ts +16 -10
  64. package/src/components/script-component.ts +512 -125
  65. package/src/components/script.ts +123 -16
  66. package/src/components/scrollbar-component.ts +21 -14
  67. package/src/components/scrollview-component.ts +42 -34
  68. package/src/components/sound-component.ts +17 -10
  69. package/src/components/sound-slot.ts +50 -19
  70. package/src/entity.ts +84 -76
  71. package/src/index.ts +5 -2
  72. package/src/material.ts +2432 -62
  73. package/src/model.ts +8 -2
  74. package/src/module.ts +16 -0
  75. package/src/parse.ts +298 -0
  76. package/src/scene.ts +26 -13
  77. package/src/sky.ts +36 -18
  78. package/dist/utils.d.ts +0 -56
  79. package/src/utils.ts +0 -119
@@ -3,6 +3,7 @@ import { SoundSlot } from 'playcanvas';
3
3
  import { AssetElement } from '../asset';
4
4
  import { AsyncElement } from '../async-element';
5
5
  import { SoundComponentElement } from './sound-component';
6
+ import { parseBool, parseNumber } from '../parse';
6
7
 
7
8
  /**
8
9
  * The SoundSlotElement interface provides properties and methods for manipulating
@@ -28,13 +29,31 @@ class SoundSlotElement extends AsyncElement {
28
29
 
29
30
  private _volume: number = 1;
30
31
 
32
+ /**
33
+ * The `<pc-sounds>` this slot was added to, captured at connect time.
34
+ *
35
+ * `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
36
+ * `parentElement` is already `null`, so a lookup would both fail to find the component and
37
+ * emit a misleading "must be a direct child" warning for what is an ordinary removal.
38
+ */
39
+ private _soundElement: SoundComponentElement | null = null;
40
+
31
41
  /**
32
42
  * The sound slot.
33
43
  */
34
44
  soundSlot: SoundSlot | null = null;
35
45
 
36
46
  async connectedCallback() {
37
- await this.soundElement?.ready();
47
+ const soundElement = this.soundElement;
48
+ await soundElement?.ready();
49
+
50
+ // The element may have been removed, or its parent torn down, while we were waiting. A
51
+ // <pc-app> disconnects before its children, so by the time we resume the component can
52
+ // already be gone - see the matching guard in disconnectedCallback below.
53
+ const component = soundElement?.component;
54
+ if (!this.isConnected || !component) {
55
+ return;
56
+ }
38
57
 
39
58
  const options = {
40
59
  autoPlay: this._autoPlay,
@@ -48,7 +67,8 @@ class SoundSlotElement extends AsyncElement {
48
67
  options.duration = this._duration;
49
68
  }
50
69
 
51
- this.soundSlot = this.soundElement!.component!.addSlot(this._name, options);
70
+ this._soundElement = soundElement;
71
+ this.soundSlot = component.addSlot(this._name, options);
52
72
  this.asset = this._asset;
53
73
  if (this._autoPlay) {
54
74
  this.soundSlot!.play();
@@ -58,14 +78,19 @@ class SoundSlotElement extends AsyncElement {
58
78
  }
59
79
 
60
80
  disconnectedCallback() {
61
- this.soundElement!.component!.removeSlot(this._name);
81
+ // Uses the cached parent rather than a fresh lookup, since parentElement is already null
82
+ // by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
83
+ // being torn down — parents disconnect first and have already removed the component.
84
+ this._soundElement?.component?.removeSlot(this._name);
85
+ this._soundElement = null;
86
+ this.soundSlot = null;
62
87
  }
63
88
 
64
89
  protected get soundElement(): SoundComponentElement | null {
65
90
  const soundElement = this.parentElement as SoundComponentElement;
66
91
 
67
92
  if (!(soundElement instanceof SoundComponentElement)) {
68
- console.warn('pc-sound-slot must be a direct child of a pc-sound element');
93
+ console.warn('pc-sound must be a direct child of a pc-sounds element');
69
94
  return null;
70
95
  }
71
96
 
@@ -114,12 +139,12 @@ class SoundSlotElement extends AsyncElement {
114
139
  }
115
140
 
116
141
  /**
117
- * Sets the duration of the sound slot.
142
+ * Sets the duration of the sound slot, in seconds (or `null` to play the whole clip).
118
143
  * @param value - The duration.
119
144
  */
120
- set duration(value: number) {
145
+ set duration(value: number | null) {
121
146
  this._duration = value;
122
- if (this.soundSlot) {
147
+ if (this.soundSlot && value !== null) {
123
148
  this.soundSlot.duration = value;
124
149
  }
125
150
  }
@@ -128,8 +153,8 @@ class SoundSlotElement extends AsyncElement {
128
153
  * Gets the duration of the sound slot.
129
154
  * @returns The duration.
130
155
  */
131
- get duration() {
132
- return this._duration as number;
156
+ get duration(): number | null {
157
+ return this._duration;
133
158
  }
134
159
 
135
160
  /**
@@ -250,34 +275,34 @@ class SoundSlotElement extends AsyncElement {
250
275
  return ['asset', 'auto-play', 'duration', 'loop', 'name', 'overlap', 'pitch', 'start-time', 'volume'];
251
276
  }
252
277
 
253
- attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
278
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
254
279
  switch (name) {
255
280
  case 'asset':
256
- this.asset = newValue;
281
+ this.asset = newValue ?? '';
257
282
  break;
258
283
  case 'auto-play':
259
- this.autoPlay = this.hasAttribute('auto-play');
284
+ this.autoPlay = parseBool(newValue, false);
260
285
  break;
261
286
  case 'duration':
262
- this.duration = parseFloat(newValue);
287
+ this.duration = parseNumber(newValue, null, name);
263
288
  break;
264
289
  case 'loop':
265
- this.loop = this.hasAttribute('loop');
290
+ this.loop = parseBool(newValue, false);
266
291
  break;
267
292
  case 'name':
268
- this.name = newValue;
293
+ this.name = newValue ?? '';
269
294
  break;
270
295
  case 'overlap':
271
- this.overlap = this.hasAttribute('overlap');
296
+ this.overlap = parseBool(newValue, false);
272
297
  break;
273
298
  case 'pitch':
274
- this.pitch = parseFloat(newValue);
299
+ this.pitch = parseNumber(newValue, 1, name);
275
300
  break;
276
301
  case 'start-time':
277
- this.startTime = parseFloat(newValue);
302
+ this.startTime = parseNumber(newValue, 0, name);
278
303
  break;
279
304
  case 'volume':
280
- this.volume = parseFloat(newValue);
305
+ this.volume = parseNumber(newValue, 1, name);
281
306
  break;
282
307
  }
283
308
  }
@@ -285,4 +310,10 @@ class SoundSlotElement extends AsyncElement {
285
310
 
286
311
  customElements.define('pc-sound', SoundSlotElement);
287
312
 
313
+ declare global {
314
+ interface HTMLElementTagNameMap {
315
+ 'pc-sound': SoundSlotElement;
316
+ }
317
+ }
318
+
288
319
  export { SoundSlotElement };
package/src/entity.ts CHANGED
@@ -1,13 +1,31 @@
1
1
  import { AppBase, Entity, Vec3 } from 'playcanvas';
2
2
 
3
3
  import { AsyncElement } from './async-element';
4
- import { parseVec3 } from './utils';
4
+ import { parseBool, parseTags, parseVec3 } from './parse';
5
5
 
6
6
  /**
7
7
  * The EntityElement interface provides properties and methods for manipulating
8
8
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
9
9
  * The EntityElement interface also inherits the properties and methods of the
10
10
  * {@link HTMLElement} interface.
11
+ *
12
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
13
+ * intersects this entity's geometry. They are only generated while the entity has a listener for
14
+ * them, registered either with {@link addEventListener} or with the matching inline `onpointer*`
15
+ * attribute.
16
+ *
17
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the entity.
18
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the entity.
19
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the entity.
20
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
21
+ * entity.
22
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
23
+ * entity.
24
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
25
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
26
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
27
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
28
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
11
29
  */
12
30
  class EntityElement extends AsyncElement {
13
31
  /**
@@ -45,76 +63,48 @@ class EntityElement extends AsyncElement {
45
63
  */
46
64
  private _listeners: { [key: string]: EventListener[] } = {};
47
65
 
66
+ /**
67
+ * The event types for which an inline `onpointer*` attribute is currently present.
68
+ */
69
+ private _inlineHandlerTypes = new Set<string>();
70
+
48
71
  /**
49
72
  * Whether the hierarchy has been built for this entity.
50
73
  */
51
74
  private _built = false;
52
75
 
76
+ private _entity: Entity | null = null;
77
+
53
78
  /**
54
- * The PlayCanvas entity instance.
79
+ * The PlayCanvas entity instance. Available once the element is ready — await
80
+ * {@link whenReady} or the element's `ready()` promise before accessing it.
81
+ * @returns The entity instance.
55
82
  */
56
- entity: Entity | null = null;
83
+ get entity(): Entity {
84
+ return this._entity!;
85
+ }
57
86
 
58
87
  createEntity(app: AppBase) {
59
88
  // Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
60
89
  // `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
61
90
  // descendants' own connectedCallbacks would otherwise create them a second time.
62
- if (this.entity) {
91
+ if (this._entity) {
63
92
  return;
64
93
  }
65
94
 
66
95
  // Create a new entity
67
- this.entity = new Entity(this.getAttribute('name') || this._name, app);
68
-
69
- const enabled = this.getAttribute('enabled');
70
- if (enabled) {
71
- this.entity.enabled = enabled !== 'false';
72
- }
96
+ const entity = new Entity(this.getAttribute('name') || this._name, app);
97
+ this._entity = entity;
73
98
 
74
- const position = this.getAttribute('position');
75
- if (position) {
76
- this.entity.setLocalPosition(parseVec3(position));
77
- }
78
-
79
- const rotation = this.getAttribute('rotation');
80
- if (rotation) {
81
- this.entity.setLocalEulerAngles(parseVec3(rotation));
82
- }
83
-
84
- const scale = this.getAttribute('scale');
85
- if (scale) {
86
- this.entity.setLocalScale(parseVec3(scale));
87
- }
99
+ entity.enabled = parseBool(this.getAttribute('enabled'), true);
100
+ entity.setLocalPosition(parseVec3(this.getAttribute('position'), Vec3.ZERO, 'position'));
101
+ entity.setLocalEulerAngles(parseVec3(this.getAttribute('rotation'), Vec3.ZERO, 'rotation'));
102
+ entity.setLocalScale(parseVec3(this.getAttribute('scale'), Vec3.ONE, 'scale'));
88
103
 
89
- const tags = this.getAttribute('tags');
90
- if (tags) {
91
- this.entity.tags.add(tags.split(',').map(tag => tag.trim()));
104
+ const tags = parseTags(this.getAttribute('tags'));
105
+ if (tags.length > 0) {
106
+ entity.tags.add(tags);
92
107
  }
93
-
94
- // Handle pointer events
95
- const pointerEvents = [
96
- 'onpointerenter',
97
- 'onpointerleave',
98
- 'onpointerdown',
99
- 'onpointerup',
100
- 'onpointermove'
101
- ];
102
-
103
- pointerEvents.forEach((eventName) => {
104
- const handler = this.getAttribute(eventName);
105
- if (handler) {
106
- const eventType = eventName.substring(2); // remove 'on' prefix
107
- const eventHandler = (event: Event) => {
108
- try {
109
- /* eslint-disable-next-line no-new-func */
110
- new Function('event', handler).call(this, event);
111
- } catch (e) {
112
- console.error('Error in event handler:', e);
113
- }
114
- };
115
- this.addEventListener(eventType, eventHandler);
116
- }
117
- });
118
108
  }
119
109
 
120
110
  buildHierarchy(app: AppBase) {
@@ -159,12 +149,12 @@ class EntityElement extends AsyncElement {
159
149
  // Notify all children that their entities are about to become invalid
160
150
  const children = this.querySelectorAll('pc-entity');
161
151
  children.forEach((child) => {
162
- (child as EntityElement).entity = null;
152
+ (child as EntityElement)._entity = null;
163
153
  });
164
154
 
165
155
  // Destroy the entity
166
156
  this.entity.destroy();
167
- this.entity = null;
157
+ this._entity = null;
168
158
  this._built = false;
169
159
  }
170
160
  }
@@ -284,6 +274,31 @@ class EntityElement extends AsyncElement {
284
274
  return this._tags;
285
275
  }
286
276
 
277
+ /**
278
+ * Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
279
+ * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
280
+ * the previous handler and removing it removes the handler, exactly like `onclick` on any
281
+ * HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
282
+ * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
283
+ * kept in sync here.
284
+ *
285
+ * @param name - The attribute name (e.g. 'onpointerdown').
286
+ * @param value - The attribute value, or `null` when the attribute has been removed.
287
+ */
288
+ private _updateInlineHandler(name: string, value: string | null) {
289
+ const type = name.substring(2);
290
+ const had = this._inlineHandlerTypes.has(type);
291
+ const has = value !== null;
292
+
293
+ if (has && !had) {
294
+ this._inlineHandlerTypes.add(type);
295
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
296
+ } else if (!has && had) {
297
+ this._inlineHandlerTypes.delete(type);
298
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
299
+ }
300
+ }
301
+
287
302
  static get observedAttributes() {
288
303
  return [
289
304
  'enabled',
@@ -300,45 +315,32 @@ class EntityElement extends AsyncElement {
300
315
  ];
301
316
  }
302
317
 
303
- attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
318
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
304
319
  switch (name) {
305
320
  case 'enabled':
306
- this.enabled = newValue !== 'false';
321
+ this.enabled = parseBool(newValue, true);
307
322
  break;
308
323
  case 'name':
309
- this.name = newValue;
324
+ this.name = newValue ?? 'Untitled';
310
325
  break;
311
326
  case 'position':
312
- this.position = parseVec3(newValue);
327
+ this.position = parseVec3(newValue, Vec3.ZERO, name);
313
328
  break;
314
329
  case 'rotation':
315
- this.rotation = parseVec3(newValue);
330
+ this.rotation = parseVec3(newValue, Vec3.ZERO, name);
316
331
  break;
317
332
  case 'scale':
318
- this.scale = parseVec3(newValue);
333
+ this.scale = parseVec3(newValue, Vec3.ONE, name);
319
334
  break;
320
335
  case 'tags':
321
- this.tags = newValue.split(',').map(tag => tag.trim());
336
+ this.tags = parseTags(newValue);
322
337
  break;
323
338
  case 'onpointerenter':
324
339
  case 'onpointerleave':
325
340
  case 'onpointerdown':
326
341
  case 'onpointerup':
327
342
  case 'onpointermove':
328
- if (newValue) {
329
- const eventName = name.substring(2);
330
- // Use Function.prototype.bind to avoid new Function
331
- const handler = (event: Event) => {
332
- try {
333
- const handlerStr = this.getAttribute(eventName) || '';
334
- /* eslint-disable-next-line no-new-func */
335
- new Function('event', handlerStr).call(this, event);
336
- } catch (e) {
337
- console.error('Error in event handler:', e);
338
- }
339
- };
340
- this.addEventListener(eventName, handler);
341
- }
343
+ this._updateInlineHandler(name, newValue);
342
344
  break;
343
345
  }
344
346
  }
@@ -365,10 +367,16 @@ class EntityElement extends AsyncElement {
365
367
  }
366
368
 
367
369
  hasListeners(type: string): boolean {
368
- return Boolean(this._listeners[type]?.length);
370
+ return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
369
371
  }
370
372
  }
371
373
 
372
374
  customElements.define('pc-entity', EntityElement);
373
375
 
376
+ declare global {
377
+ interface HTMLElementTagNameMap {
378
+ 'pc-entity': EntityElement;
379
+ }
380
+ }
381
+
374
382
  export { EntityElement };
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  /* eslint-disable import/order */
11
11
 
12
12
  // Note that order matters here (e.g. pc-entity must be defined before components)
13
- import { AsyncElement } from './async-element';
13
+ import { AsyncElement, whenReady } from './async-element';
14
14
  import { ModuleElement } from './module';
15
15
  import { AppElement } from './app';
16
16
  import { EntityElement } from './entity';
@@ -69,5 +69,8 @@ export {
69
69
  MaterialElement,
70
70
  ModelElement,
71
71
  SceneElement,
72
- SkyElement
72
+ SkyElement,
73
+ whenReady
73
74
  };
75
+
76
+ export type { AsyncElementTagName } from './async-element';