@playcanvas/web-components 0.17.0 → 0.19.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 (117) hide show
  1. package/README.md +5 -3
  2. package/dist/app.d.cts +28 -3
  3. package/dist/app.d.ts +28 -3
  4. package/dist/asset.d.cts +13 -3
  5. package/dist/asset.d.ts +13 -3
  6. package/dist/components/anim-clip.d.cts +3 -1
  7. package/dist/components/anim-clip.d.ts +3 -1
  8. package/dist/components/anim-component.d.cts +5 -0
  9. package/dist/components/anim-component.d.ts +5 -0
  10. package/dist/components/audio-listener-component.d.cts +4 -0
  11. package/dist/components/audio-listener-component.d.ts +4 -0
  12. package/dist/components/button-component.d.cts +5 -0
  13. package/dist/components/button-component.d.ts +5 -0
  14. package/dist/components/camera-component.d.cts +35 -1
  15. package/dist/components/camera-component.d.ts +35 -1
  16. package/dist/components/collision-component.d.cts +4 -0
  17. package/dist/components/collision-component.d.ts +4 -0
  18. package/dist/components/element-component.d.cts +4 -0
  19. package/dist/components/element-component.d.ts +4 -0
  20. package/dist/components/gsplat-component.d.cts +4 -0
  21. package/dist/components/gsplat-component.d.ts +4 -0
  22. package/dist/components/joint-component.d.cts +12 -2
  23. package/dist/components/joint-component.d.ts +12 -2
  24. package/dist/components/layout-child-component.d.cts +4 -0
  25. package/dist/components/layout-child-component.d.ts +4 -0
  26. package/dist/components/layout-group-component.d.cts +4 -0
  27. package/dist/components/layout-group-component.d.ts +4 -0
  28. package/dist/components/light-component.d.cts +50 -1
  29. package/dist/components/light-component.d.ts +50 -1
  30. package/dist/components/particle-system-component.d.cts +4 -0
  31. package/dist/components/particle-system-component.d.ts +4 -0
  32. package/dist/components/render-component.d.cts +5 -0
  33. package/dist/components/render-component.d.ts +5 -0
  34. package/dist/components/rigid-body-component.d.cts +5 -0
  35. package/dist/components/rigid-body-component.d.ts +5 -0
  36. package/dist/components/screen-component.d.cts +4 -0
  37. package/dist/components/screen-component.d.ts +4 -0
  38. package/dist/components/script-component.d.cts +3 -0
  39. package/dist/components/script-component.d.ts +3 -0
  40. package/dist/components/script-instance.d.cts +4 -0
  41. package/dist/components/script-instance.d.ts +4 -0
  42. package/dist/components/scroll-view-component.d.cts +4 -0
  43. package/dist/components/scroll-view-component.d.ts +4 -0
  44. package/dist/components/scrollbar-component.d.cts +4 -0
  45. package/dist/components/scrollbar-component.d.ts +4 -0
  46. package/dist/components/sound-component.d.cts +4 -0
  47. package/dist/components/sound-component.d.ts +4 -0
  48. package/dist/components/sound-slot.d.cts +3 -0
  49. package/dist/components/sound-slot.d.ts +3 -0
  50. package/dist/custom-elements.json +338 -7
  51. package/dist/entity-base.d.cts +5 -4
  52. package/dist/entity-base.d.ts +5 -4
  53. package/dist/entity-owner.d.cts +2 -2
  54. package/dist/entity-owner.d.ts +2 -2
  55. package/dist/entity.d.cts +13 -2
  56. package/dist/entity.d.ts +13 -2
  57. package/dist/index.d.cts +4 -1
  58. package/dist/index.d.ts +4 -1
  59. package/dist/material.d.cts +18 -7
  60. package/dist/material.d.ts +18 -7
  61. package/dist/model.d.cts +11 -0
  62. package/dist/model.d.ts +11 -0
  63. package/dist/node.d.cts +12 -1
  64. package/dist/node.d.ts +12 -1
  65. package/dist/pwc.cjs +601 -206
  66. package/dist/pwc.cjs.map +1 -1
  67. package/dist/pwc.js +601 -206
  68. package/dist/pwc.js.map +1 -1
  69. package/dist/pwc.min.js +1 -1
  70. package/dist/pwc.min.js.map +1 -1
  71. package/dist/pwc.min.mjs +1 -1
  72. package/dist/pwc.min.mjs.map +1 -1
  73. package/dist/pwc.mjs +602 -207
  74. package/dist/pwc.mjs.map +1 -1
  75. package/dist/scene.d.cts +19 -0
  76. package/dist/scene.d.ts +19 -0
  77. package/dist/sky.d.cts +4 -0
  78. package/dist/sky.d.ts +4 -0
  79. package/dist/vscode.html-custom-data.json +257 -62
  80. package/dist/wasm.d.cts +4 -0
  81. package/dist/wasm.d.ts +4 -0
  82. package/dist/web-types.json +169 -69
  83. package/package.json +3 -3
  84. package/src/app.ts +150 -71
  85. package/src/asset.ts +13 -4
  86. package/src/components/anim-clip.ts +3 -1
  87. package/src/components/anim-component.ts +5 -0
  88. package/src/components/audio-listener-component.ts +4 -0
  89. package/src/components/button-component.ts +5 -0
  90. package/src/components/camera-component.ts +63 -6
  91. package/src/components/collision-component.ts +4 -0
  92. package/src/components/element-component.ts +4 -0
  93. package/src/components/gsplat-component.ts +4 -0
  94. package/src/components/joint-component.ts +12 -2
  95. package/src/components/layout-child-component.ts +4 -0
  96. package/src/components/layout-group-component.ts +4 -0
  97. package/src/components/light-component.ts +108 -17
  98. package/src/components/particle-system-component.ts +4 -0
  99. package/src/components/render-component.ts +5 -0
  100. package/src/components/rigid-body-component.ts +5 -0
  101. package/src/components/screen-component.ts +4 -0
  102. package/src/components/script-component.ts +3 -0
  103. package/src/components/script-instance.ts +4 -0
  104. package/src/components/scroll-view-component.ts +4 -0
  105. package/src/components/scrollbar-component.ts +4 -0
  106. package/src/components/sound-component.ts +4 -0
  107. package/src/components/sound-slot.ts +3 -0
  108. package/src/entity-base.ts +28 -16
  109. package/src/entity-owner.ts +2 -2
  110. package/src/entity.ts +16 -4
  111. package/src/index.ts +12 -1
  112. package/src/material.ts +19 -8
  113. package/src/model.ts +14 -2
  114. package/src/node.ts +15 -3
  115. package/src/scene.ts +35 -1
  116. package/src/sky.ts +4 -0
  117. package/src/wasm.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "author": "PlayCanvas <support@playcanvas.com>",
5
5
  "homepage": "https://playcanvas.com",
6
6
  "description": "Web Components for the PlayCanvas Engine",
@@ -99,12 +99,12 @@
99
99
  "eslint": "9.39.5",
100
100
  "globals": "17.11.0",
101
101
  "jsdom": "30.0.1",
102
- "mediabunny": "1.55.2",
102
+ "mediabunny": "1.55.3",
103
103
  "opentype.js": "2.0.0",
104
104
  "playcanvas": "2.21.4",
105
105
  "prettier": "3.9.6",
106
106
  "publint": "0.3.24",
107
- "rollup": "4.62.5",
107
+ "rollup": "4.63.0",
108
108
  "serve": "14.2.6",
109
109
  "tslib": "2.8.1",
110
110
  "typedoc": "0.28.20",
package/src/app.ts CHANGED
@@ -66,6 +66,7 @@ import {
66
66
 
67
67
  import type { AssetElement } from './asset';
68
68
  import { AsyncElement } from './async-element';
69
+ import { SYNTHESIZED_EVENTS } from './entity-base';
69
70
  import type { EntityBaseElement } from './entity-base';
70
71
  import type { EntityOwnerElement } from './entity-owner';
71
72
  import { LoadingBar } from './loading-bar';
@@ -73,9 +74,6 @@ import type { MaterialElement } from './material';
73
74
  import { parseBool, parseEnum, parseNumber } from './parse';
74
75
  import type { WasmElement } from './wasm';
75
76
 
76
- /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
77
- const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
78
-
79
77
  /**
80
78
  * The event types whose listeners make an element a hover target. Hover resolution walks past
81
79
  * elements listening for none of them, so a silent element never swallows an ancestor's
@@ -83,6 +81,48 @@ const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointeren
83
81
  */
84
82
  const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'] as const;
85
83
 
84
+ /**
85
+ * The canvas listeners each synthesized event type is driven by. Enter and leave are derived
86
+ * from move picks. A click is concluded from the down/up pair, with pointercancel discarding a
87
+ * press the browser takes back (for example a touch that becomes a scroll).
88
+ */
89
+ const canvasEventsFor: Record<(typeof SYNTHESIZED_EVENTS)[number], readonly string[]> = {
90
+ pointermove: ['pointermove'],
91
+ pointerenter: ['pointermove'],
92
+ pointerleave: ['pointermove'],
93
+ pointerdown: ['pointerdown'],
94
+ pointerup: ['pointerup'],
95
+ click: ['pointerdown', 'pointerup', 'pointercancel']
96
+ };
97
+
98
+ /**
99
+ * How long after a click a further click on the same target still raises the click count that
100
+ * `detail` carries, approximating the platform's double-click time.
101
+ */
102
+ const CLICK_CHAIN_MS = 500;
103
+
104
+ /**
105
+ * Finds the nearest common inclusive ancestor of two picked nodes - the node a click belongs to
106
+ * when the press and the release picked different geometry, exactly as the DOM assigns a click
107
+ * whose down and up have different targets.
108
+ *
109
+ * @param a - The node the press picked, or `null`.
110
+ * @param b - The node the release picked, or `null`.
111
+ * @returns The nearest common inclusive ancestor, or `null` when there is none.
112
+ */
113
+ const commonAncestor = (a: GraphNode | null, b: GraphNode | null): GraphNode | null => {
114
+ const ancestors = new Set<GraphNode>();
115
+ for (let node = a; node !== null; node = node.parent) {
116
+ ancestors.add(node);
117
+ }
118
+ for (let node = b; node !== null; node = node.parent) {
119
+ if (ancestors.has(node)) {
120
+ return node;
121
+ }
122
+ }
123
+ return null;
124
+ };
125
+
86
126
  /**
87
127
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
88
128
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -112,6 +152,10 @@ const ensureBaseStyles = () => {
112
152
  * tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
113
153
  * its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
114
154
  *
155
+ * @elementSummary The `<pc-app>` element creates a PlayCanvas application and the canvas it renders
156
+ * into, and is the root of every scene. It holds the `<pc-asset>`, `<pc-material>`, `<pc-wasm>` and
157
+ * `<pc-scene>` elements, and the page's CSS sizes it, as it would a `<video>`.
158
+ *
115
159
  * @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
116
160
  * `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
117
161
  * Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
@@ -178,14 +222,6 @@ class AppElement extends AsyncElement {
178
222
 
179
223
  private _picker: Picker | null = null;
180
224
 
181
- private _hasPointerListeners: Record<string, boolean> = {
182
- pointerenter: false,
183
- pointerleave: false,
184
- pointerdown: false,
185
- pointerup: false,
186
- pointermove: false
187
- };
188
-
189
225
  private _hoveredEntity: EntityBaseElement | null = null;
190
226
 
191
227
  // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
@@ -194,9 +230,28 @@ class AppElement extends AsyncElement {
194
230
  private _pointerHandlers: Record<string, EventListener | null> = {
195
231
  pointermove: null,
196
232
  pointerdown: null,
197
- pointerup: null
233
+ pointerup: null,
234
+ pointercancel: null
198
235
  };
199
236
 
237
+ /**
238
+ * The pick of each pointer's primary-button press, keyed by pointerId and kept while a click
239
+ * may still conclude it. The promise is stored rather than its result, so a release can
240
+ * await a press pick that has not resolved yet. Entries are removed by the matching
241
+ * pointerup or pointercancel, and only ever stored while some element listens for click -
242
+ * which is also what keeps those two canvas listeners attached.
243
+ */
244
+ private _downPicks = new Map<number, Promise<GraphNode | null>>();
245
+
246
+ /** Whether any element in the tree listens for click. Maintained by _syncCanvasListeners. */
247
+ private _clickListened = false;
248
+
249
+ /**
250
+ * The previous click's target, time and count, for chaining successive clicks into the
251
+ * click count that `detail` carries. `null` until a click has fired.
252
+ */
253
+ private _lastClick: { element: EntityBaseElement; time: number; count: number } | null = null;
254
+
200
255
  private _app: AppBase | null = null;
201
256
 
202
257
  private _loadProgress = 0;
@@ -237,12 +292,12 @@ class AppElement extends AsyncElement {
237
292
  constructor() {
238
293
  super();
239
294
 
240
- // Track pointer listeners being added to and removed from descendant entities.
241
- // Registered once here rather than on every boot - the handlers no-op while there is no
242
- // canvas, and a re-booted element must not stack a second set.
243
- pointerEventTypes.forEach((type) => {
244
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
245
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
295
+ // Track listeners for the synthesized events being added to and removed from descendant
296
+ // entities. Registered once here rather than on every boot - the sync no-ops while there
297
+ // is no canvas, and a re-booted element must not stack a second set.
298
+ SYNTHESIZED_EVENTS.forEach((type) => {
299
+ this.addEventListener(`${type}:connect`, () => this._syncCanvasListeners());
300
+ this.addEventListener(`${type}:disconnect`, () => this._syncCanvasListeners());
246
301
  });
247
302
  }
248
303
 
@@ -605,18 +660,14 @@ class AppElement extends AsyncElement {
605
660
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
606
661
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
607
662
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
663
+ this._pointerHandlers.pointercancel = (event: Event) => {
664
+ this._downPicks.delete((event as PointerEvent).pointerId);
665
+ };
608
666
 
609
- // Attach canvas handlers for listeners registered before this boot (e.g. handlers
610
- // created from onpointer* attributes when their elements were first upgraded, or
667
+ // Attach canvas listeners for element listeners registered before this boot (e.g.
668
+ // handlers created from inline attributes when their elements were first upgraded, or
611
669
  // listeners carried over from before a re-boot)
612
- pointerEventTypes.forEach((type) => {
613
- const anyListeners = Array.from(
614
- this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
615
- ).some((entity) => entity._hasListeners(type));
616
- if (anyListeners) {
617
- this._onPointerListenerAdded(type);
618
- }
619
- });
670
+ this._syncCanvasListeners();
620
671
  }
621
672
 
622
673
  private _pickerDestroy() {
@@ -633,15 +684,12 @@ class AppElement extends AsyncElement {
633
684
  this._pointerHandlers = {
634
685
  pointermove: null,
635
686
  pointerdown: null,
636
- pointerup: null
637
- };
638
- this._hasPointerListeners = {
639
- pointerenter: false,
640
- pointerleave: false,
641
- pointerdown: false,
642
- pointerup: false,
643
- pointermove: false
687
+ pointerup: null,
688
+ pointercancel: null
644
689
  };
690
+ this._downPicks.clear();
691
+ this._clickListened = false;
692
+ this._lastClick = null;
645
693
  }
646
694
 
647
695
  /**
@@ -867,7 +915,17 @@ class AppElement extends AsyncElement {
867
915
  private async _onPointerDown(event: PointerEvent) {
868
916
  if (!this._picker || !this.app) return;
869
917
 
870
- const node = await this._pickNode(event);
918
+ const pick = this._pickNode(event);
919
+
920
+ // A click concludes on the matching pointerup, which needs to know what the press
921
+ // picked. Primary button only - the only button a click can conclude from - and only
922
+ // while click is listened for, since it is the click mapping that keeps the pointerup
923
+ // and pointercancel listeners attached to clean the entry up again.
924
+ if (this._clickListened && event.button === 0) {
925
+ this._downPicks.set(event.pointerId, pick);
926
+ }
927
+
928
+ const node = await pick;
871
929
  if (!this._picker) return; // the element disconnected while the pick was in flight
872
930
 
873
931
  const entityElement = this._elementWithListener(node, 'pointerdown');
@@ -879,6 +937,11 @@ class AppElement extends AsyncElement {
879
937
  private async _onPointerUp(event: PointerEvent) {
880
938
  if (!this._picker || !this.app) return;
881
939
 
940
+ // The press pick this release may conclude as a click. Claimed synchronously, so the
941
+ // entry is gone before any other event for this pointer can be handled.
942
+ const downPick = this._downPicks.get(event.pointerId);
943
+ this._downPicks.delete(event.pointerId);
944
+
882
945
  const node = await this._pickNode(event);
883
946
  if (!this._picker) return; // the element disconnected while the pick was in flight
884
947
 
@@ -886,47 +949,63 @@ class AppElement extends AsyncElement {
886
949
  if (entityElement) {
887
950
  entityElement.dispatchEvent(new PointerEvent('pointerup', event));
888
951
  }
889
- }
890
952
 
891
- private _onPointerListenerAdded(type: string) {
892
- if (!this._hasPointerListeners[type] && this._canvas) {
893
- this._hasPointerListeners[type] = true;
894
-
895
- // For enter/leave events, we need the move handler
896
- const handler =
897
- type === 'pointerenter' || type === 'pointerleave'
898
- ? this._pointerHandlers.pointermove
899
- : this._pointerHandlers[type];
900
-
901
- if (handler) {
902
- this._canvas.addEventListener(
903
- type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
904
- handler
905
- );
906
- }
953
+ // A click fires where the DOM fires it: at the nearest common inclusive ancestor of
954
+ // what the press and the release picked, for the primary button only. The press pick
955
+ // may still be in flight - a quick tap resolves in pick order, not event order.
956
+ if (!downPick || event.button !== 0) return;
957
+ const downNode = await downPick;
958
+ if (!this._picker) return;
959
+
960
+ const clickElement = this._elementWithListener(commonAncestor(downNode, node), 'click');
961
+ if (clickElement) {
962
+ const click = new PointerEvent('click', event);
963
+
964
+ // The init above copied pointerup's `detail`, which the Pointer Events spec fixes
965
+ // at 0 - but click is exempt: its detail is the click count, chained here as the
966
+ // platform chains it (same target, within the double-click window). Overridden
967
+ // with defineProperty because an event instance used as an init dict cannot have
968
+ // single fields replaced.
969
+ const time = performance.now();
970
+ const last = this._lastClick;
971
+ const count =
972
+ last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
973
+ this._lastClick = { element: clickElement, time, count };
974
+ Object.defineProperty(click, 'detail', { value: count });
975
+
976
+ clickElement.dispatchEvent(click);
907
977
  }
908
978
  }
909
979
 
910
- private _onPointerListenerRemoved(type: string) {
911
- const hasListeners = Array.from(
912
- this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
913
- ).some((entity) => entity._hasListeners(type));
914
-
915
- if (!hasListeners && this._canvas) {
916
- this._hasPointerListeners[type] = false;
917
-
918
- const handler =
919
- type === 'pointerenter' || type === 'pointerleave'
920
- ? this._pointerHandlers.pointermove
921
- : this._pointerHandlers[type];
980
+ /**
981
+ * Attaches exactly the canvas listeners the tree's current element listeners need, and
982
+ * detaches the rest. Recomputed whenever a listener connects or disconnects anywhere under
983
+ * this element: several synthesized types can need the same canvas listener (enter, leave
984
+ * and move all ride the move pick; click rides the down/up pair), so one type's removal
985
+ * must not detach a listener another type still uses. Re-attaching an attached listener is
986
+ * a no-op by EventTarget semantics, so no attach state is kept.
987
+ */
988
+ private _syncCanvasListeners() {
989
+ const canvas = this._canvas;
990
+ if (!canvas) return; // not booted yet: _pickerCreate syncs once the handlers exist
922
991
 
923
- if (handler) {
924
- this._canvas.removeEventListener(
925
- type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
926
- handler
927
- );
992
+ const elements = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node'));
993
+ const needed = new Set<string>();
994
+ for (const type of SYNTHESIZED_EVENTS) {
995
+ if (elements.some((element) => element._hasListeners(type))) {
996
+ canvasEventsFor[type].forEach((canvasType) => needed.add(canvasType));
928
997
  }
929
998
  }
999
+ this._clickListened = elements.some((element) => element._hasListeners('click'));
1000
+
1001
+ Object.entries(this._pointerHandlers).forEach(([canvasType, handler]) => {
1002
+ if (!handler) return;
1003
+ if (needed.has(canvasType)) {
1004
+ canvas.addEventListener(canvasType, handler);
1005
+ } else {
1006
+ canvas.removeEventListener(canvasType, handler);
1007
+ }
1008
+ });
930
1009
  }
931
1010
 
932
1011
  /**
package/src/asset.ts CHANGED
@@ -26,7 +26,8 @@ const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
26
26
  ['tiled', SPRITE_RENDERMODE_TILED]
27
27
  ]);
28
28
 
29
- type AddressMode = 'repeat' | 'clamp' | 'mirror';
29
+ /** The addressing modes for a texture asset. */
30
+ export type AddressMode = 'repeat' | 'clamp' | 'mirror';
30
31
 
31
32
  const addressModes = new Map<AddressMode, number>([
32
33
  ['repeat', ADDRESS_REPEAT],
@@ -34,7 +35,8 @@ const addressModes = new Map<AddressMode, number>([
34
35
  ['mirror', ADDRESS_MIRRORED_REPEAT]
35
36
  ]);
36
37
 
37
- type MinFilterMode =
38
+ /** The minification filter modes for a texture asset. */
39
+ export type MinFilterMode =
38
40
  'nearest' | 'linear' | 'nearest-mip-nearest' | 'linear-mip-nearest' | 'nearest-mip-linear' | 'linear-mip-linear';
39
41
 
40
42
  const minFilterModes = new Map<MinFilterMode, number>([
@@ -46,8 +48,11 @@ const minFilterModes = new Map<MinFilterMode, number>([
46
48
  ['linear-mip-linear', FILTER_LINEAR_MIPMAP_LINEAR]
47
49
  ]);
48
50
 
49
- // Magnification has no mip variants - the engine (and the GPU) only accepts these two.
50
- type MagFilterMode = 'nearest' | 'linear';
51
+ /**
52
+ * The magnification filter modes for a texture asset. Magnification has no mip variants - the
53
+ * engine (and the GPU) only accepts these two.
54
+ */
55
+ export type MagFilterMode = 'nearest' | 'linear';
51
56
 
52
57
  const magFilterModes = new Map<MagFilterMode, number>([
53
58
  ['nearest', FILTER_NEAREST],
@@ -168,6 +173,10 @@ const processBufferView = (
168
173
  * Apart from `lazy` and the texture options, these attributes are read once when the asset is
169
174
  * created, so changing them later has no effect.
170
175
  *
176
+ * @elementSummary The `<pc-asset>` element declares an asset for the application to load — a model,
177
+ * texture, font, sound, script or JSON file — under an `id` that other elements reference. Must be
178
+ * a direct child of `<pc-app>`.
179
+ *
171
180
  * @attribute {string} id - The identifier used to reference the asset from other elements.
172
181
  * @attribute {string} src - The URL of the asset to load.
173
182
  * @attribute {string} type - The asset type. Inferred from the `src` file extension when omitted.
@@ -22,7 +22,9 @@ import { AnimComponentElement } from './anim-component';
22
22
  * named; in a multi-track source the track named `name` is chosen, falling back to the first
23
23
  * with a warning. The element becomes ready once its resolved track is assigned.
24
24
  *
25
- * @category Components
25
+ * @elementSummary The `<pc-anim-clip>` element declares one named animation clip on its parent
26
+ * `<pc-anim>`, taken from the `asset` it names or, without one, from the enclosing `<pc-model>`'s
27
+ * own animations. Must be a direct child of `<pc-anim>`.
26
28
  */
27
29
  class AnimClipElement extends AsyncElement {
28
30
  /**
@@ -48,6 +48,11 @@ type PlaybackState = {
48
48
  *
49
49
  * Engine component: {@link AnimComponent} (`anim`).
50
50
  *
51
+ * @elementSummary The `<pc-anim>` element plays animation clips over its entity's hierarchy, taken
52
+ * from `<pc-anim-clip>` children or from the enclosing `<pc-model>`'s own animations. The first
53
+ * clip plays automatically, and the `clip` attribute switches between them. Must be a child of a
54
+ * `<pc-entity>`, `<pc-model>` or `<pc-node>`.
55
+ *
51
56
  * @category Components
52
57
  */
53
58
  class AnimComponentElement extends ComponentElement {
@@ -10,6 +10,10 @@ import { ComponentElement } from './component';
10
10
  *
11
11
  * Engine component: {@link AudioListenerComponent} (`audiolistener`).
12
12
  *
13
+ * @elementSummary The `<pc-audio-listener>` element makes its entity the point from which
14
+ * positional sounds are heard, typically the entity holding the `<pc-camera>`. Must be a child of a
15
+ * `<pc-entity>`, `<pc-model>` or `<pc-node>`.
16
+ *
13
17
  * @category Components
14
18
  */
15
19
  class AudioListenerComponentElement extends ComponentElement {
@@ -19,6 +19,11 @@ const transitionModes = new Map<'tint' | 'sprite', number>([
19
19
  *
20
20
  * Engine component: {@link ButtonComponent} (`button`).
21
21
  *
22
+ * @elementSummary The `<pc-button>` element makes its entity respond to pointer input, tinting or
23
+ * swapping its image as the pointer hovers, presses and releases it. The entity also needs a
24
+ * `<pc-element>` with `use-input` set. Must be a child of a `<pc-entity>`, `<pc-model>` or
25
+ * `<pc-node>`.
26
+ *
22
27
  * @category Components
23
28
  */
24
29
  class ButtonComponentElement extends ComponentElement {
@@ -13,6 +13,7 @@ import {
13
13
  TONEMAP_ACES,
14
14
  TONEMAP_HEJL,
15
15
  TONEMAP_NONE,
16
+ XRTYPE_AR,
16
17
  XRTYPE_VR
17
18
  } from 'playcanvas';
18
19
 
@@ -43,6 +44,10 @@ const tonemaps = new Map<'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2
43
44
  *
44
45
  * Engine component: {@link CameraComponent} (`camera`).
45
46
  *
47
+ * @elementSummary The `<pc-camera>` element renders the scene from its entity's transform, with
48
+ * attributes for the projection, field of view, clip planes, clear color and tonemapping. Must be a
49
+ * child of a `<pc-entity>`, `<pc-model>` or `<pc-node>`.
50
+ *
46
51
  * @category Components
47
52
  */
48
53
  class CameraComponentElement extends ComponentElement {
@@ -50,9 +55,11 @@ class CameraComponentElement extends ComponentElement {
50
55
 
51
56
  private _clearColorBuffer = true;
52
57
 
58
+ private _clearDepth = 1;
59
+
53
60
  private _clearDepthBuffer = true;
54
61
 
55
- private _clearStencilBuffer = false;
62
+ private _clearStencilBuffer = true;
56
63
 
57
64
  private _cullFaces = true;
58
65
 
@@ -91,6 +98,7 @@ class CameraComponentElement extends ComponentElement {
91
98
  return {
92
99
  clearColor: this._clearColor,
93
100
  clearColorBuffer: this._clearColorBuffer,
101
+ clearDepth: this._clearDepth,
94
102
  clearDepthBuffer: this._clearDepthBuffer,
95
103
  clearStencilBuffer: this._clearStencilBuffer,
96
104
  cullFaces: this._cullFaces,
@@ -110,9 +118,33 @@ class CameraComponentElement extends ComponentElement {
110
118
  };
111
119
  }
112
120
 
113
- get xrAvailable() {
121
+ /**
122
+ * Whether immersive AR is available. Independent of {@link vrAvailable}: a device can offer
123
+ * either mode without the other.
124
+ * @returns Whether immersive AR is available.
125
+ */
126
+ get arAvailable(): boolean {
127
+ return this._available(XRTYPE_AR);
128
+ }
129
+
130
+ /**
131
+ * Whether immersive VR is available. Independent of {@link arAvailable}: a device can offer
132
+ * either mode without the other.
133
+ * @returns Whether immersive VR is available.
134
+ */
135
+ get vrAvailable(): boolean {
136
+ return this._available(XRTYPE_VR);
137
+ }
138
+
139
+ /**
140
+ * Whether one XR session type is available on this device.
141
+ *
142
+ * @param type - The XR session type to test.
143
+ * @returns Whether that type is available.
144
+ */
145
+ private _available(type: string): boolean {
114
146
  const xrManager = this.component?.system.app.xr;
115
- return xrManager && xrManager.supported && xrManager.isAvailable(XRTYPE_VR);
147
+ return Boolean(xrManager?.supported && xrManager.isAvailable(type));
116
148
  }
117
149
 
118
150
  /**
@@ -124,10 +156,12 @@ class CameraComponentElement extends ComponentElement {
124
156
  type: 'immersive-ar' | 'immersive-vr',
125
157
  space: 'bounded-floor' | 'local' | 'local-floor' | 'unbounded' | 'viewer'
126
158
  ) {
127
- if (this.component && this.xrAvailable) {
159
+ // Gated on the mode being started, not on XR in general: a device that offers only
160
+ // one of the two would otherwise accept a session it cannot serve
161
+ if (this.component && this._available(type)) {
128
162
  this.component.startXr(type, space, {
129
163
  callback: (err: any) => {
130
- if (err) console.error(`WebXR Immersive VR failed to start: ${err.message}`);
164
+ if (err) console.error(`WebXR ${type} failed to start: ${err.message}`);
131
165
  }
132
166
  });
133
167
  }
@@ -188,6 +222,25 @@ class CameraComponentElement extends ComponentElement {
188
222
  return this._clearColorBuffer;
189
223
  }
190
224
 
225
+ /**
226
+ * Sets the depth value the depth buffer is cleared to. Defaults to 1.
227
+ * @param value - The clear depth value.
228
+ */
229
+ set clearDepth(value: number) {
230
+ this._clearDepth = value;
231
+ if (this.component) {
232
+ this.component.clearDepth = value;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Gets the depth value the depth buffer is cleared to.
238
+ * @returns The clear depth value.
239
+ */
240
+ get clearDepth(): number {
241
+ return this._clearDepth;
242
+ }
243
+
191
244
  /**
192
245
  * Sets the clear depth buffer of the camera.
193
246
  * @param value - The clear depth buffer.
@@ -498,6 +551,7 @@ class CameraComponentElement extends ComponentElement {
498
551
  ...super.observedAttributes,
499
552
  'clear-color',
500
553
  'clear-color-buffer',
554
+ 'clear-depth',
501
555
  'clear-depth-buffer',
502
556
  'clear-stencil-buffer',
503
557
  'cull-faces',
@@ -527,11 +581,14 @@ class CameraComponentElement extends ComponentElement {
527
581
  case 'clear-color-buffer':
528
582
  this.clearColorBuffer = parseBool(newValue, true);
529
583
  break;
584
+ case 'clear-depth':
585
+ this.clearDepth = parseNumber(newValue, 1, name);
586
+ break;
530
587
  case 'clear-depth-buffer':
531
588
  this.clearDepthBuffer = parseBool(newValue, true);
532
589
  break;
533
590
  case 'clear-stencil-buffer':
534
- this.clearStencilBuffer = parseBool(newValue, false);
591
+ this.clearStencilBuffer = parseBool(newValue, true);
535
592
  break;
536
593
  case 'cull-faces':
537
594
  this.cullFaces = parseBool(newValue, true);
@@ -19,6 +19,10 @@ import { ComponentElement } from './component';
19
19
  *
20
20
  * Engine component: {@link CollisionComponent} (`collision`).
21
21
  *
22
+ * @elementSummary The `<pc-collision>` element gives its entity a collision shape — a box, sphere,
23
+ * capsule, cone, cylinder or mesh — for the physics simulation to collide against. Pair it with a
24
+ * `<pc-rigid-body>`. Must be a child of a `<pc-entity>`, `<pc-model>` or `<pc-node>`.
25
+ *
22
26
  * @category Components
23
27
  */
24
28
  class CollisionComponentElement extends ComponentElement {
@@ -19,6 +19,10 @@ import { ComponentElement } from './component';
19
19
  *
20
20
  * Engine component: {@link ElementComponent} (`element`).
21
21
  *
22
+ * @elementSummary The `<pc-element>` element gives its entity a 2D UI rectangle inside a
23
+ * `<pc-screen>` hierarchy, drawing an image, a line of text or nothing (`type="image"`, `"text"` or
24
+ * `"group"`). Must be a child of a `<pc-entity>`, `<pc-model>` or `<pc-node>`.
25
+ *
22
26
  * @category Components
23
27
  */
24
28
  class ElementComponentElement extends ComponentElement {
@@ -13,6 +13,10 @@ import { ComponentElement } from './component';
13
13
  *
14
14
  * Engine component: {@link GSplatComponent} (`gsplat`).
15
15
  *
16
+ * @elementSummary The `<pc-gsplat>` element renders the 3D Gaussian splats of a `gsplat` asset at
17
+ * its entity, with attributes for shadow casting and level of detail. Must be a child of a
18
+ * `<pc-entity>`, `<pc-model>` or `<pc-node>`.
19
+ *
16
20
  * @category Components
17
21
  */
18
22
  class GSplatComponentElement extends ComponentElement {
@@ -5,9 +5,14 @@ import { getEntity, parseBool, parseEnum, parseNumber, parseVec2, parseVec3 } fr
5
5
 
6
6
  import { ComponentElement } from './component';
7
7
 
8
- type JointType = 'fixed' | 'ball' | 'hinge' | 'slider' | '6dof';
8
+ /** The constraint types supported by the `<pc-joint>` element. */
9
+ export type JointType = 'fixed' | 'ball' | 'hinge' | 'slider' | '6dof';
9
10
 
10
- type MotionMode = 'locked' | 'limited' | 'free';
11
+ /**
12
+ * The motion modes for a single joint axis: fully constrained (`locked`), constrained within
13
+ * limits (`limited`) or unconstrained (`free`).
14
+ */
15
+ export type MotionMode = 'locked' | 'limited' | 'free';
11
16
 
12
17
  /**
13
18
  * The JointComponentElement interface provides properties and methods for manipulating
@@ -22,6 +27,11 @@ type MotionMode = 'locked' | 'limited' | 'free';
22
27
  * a rigid body component; leaving `entity-b` empty constrains `entity-a` to a fixed point in world
23
28
  * space. The underlying engine component is in alpha, so its API may change.
24
29
  *
30
+ * @elementSummary The `<pc-joint>` element constrains two rigid bodies to each other — a hinged
31
+ * door, a swinging chain, a sliding drawer. Its entity's transform is the joint frame, and
32
+ * `entity-a` and `entity-b` name the bodies. Must be a child of a `<pc-entity>`, `<pc-model>` or
33
+ * `<pc-node>`.
34
+ *
25
35
  * @fires {CustomEvent} break - Fired when the impulse on the joint exceeds `break-impulse` and the
26
36
  * constraint breaks. A broken joint no longer constrains its bodies; calling `refreshFrames()` on
27
37
  * the underlying component re-attaches it. Bubbles and is composed.
@@ -12,6 +12,10 @@ import { ComponentElement } from './component';
12
12
  *
13
13
  * Engine component: {@link LayoutChildComponent} (`layoutchild`).
14
14
  *
15
+ * @elementSummary The `<pc-layout-child>` element controls how its entity is sized by the
16
+ * `<pc-layout-group>` above it, through minimum and maximum sizes and fit proportions. Must be a
17
+ * child of a `<pc-entity>`, `<pc-model>` or `<pc-node>`.
18
+ *
15
19
  * @category Components
16
20
  */
17
21
  class LayoutChildComponentElement extends ComponentElement {
@@ -34,6 +34,10 @@ const fittings = new Map<'none' | 'stretch' | 'shrink' | 'both', number>([
34
34
  *
35
35
  * Engine component: {@link LayoutGroupComponent} (`layoutgroup`).
36
36
  *
37
+ * @elementSummary The `<pc-layout-group>` element arranges its entity's children in a row or
38
+ * column, with spacing, padding, alignment and fitting. Must be a child of a `<pc-entity>`,
39
+ * `<pc-model>` or `<pc-node>`.
40
+ *
37
41
  * @category Components
38
42
  */
39
43
  class LayoutGroupComponentElement extends ComponentElement {