create-playcanvas 0.6.0 → 0.6.2

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 (24) hide show
  1. package/package.json +1 -1
  2. package/skills/assemble-scene/references/web-components.md +2 -2
  3. package/skills/build-app/references/web-components.md +4 -4
  4. package/skills/calibrate-model/references/direct-engine.md +7 -4
  5. package/skills/calibrate-model/references/react.md +8 -3
  6. package/skills/calibrate-model/references/web-components.md +7 -4
  7. package/skills/configure-animation/references/web-components.md +18 -9
  8. package/skills/find-examples/references/web-components.md +1 -1
  9. package/skills/inspect-glb/SKILL.md +27 -18
  10. package/skills/inspect-glb/scripts/inspect.mjs +179 -50
  11. package/skills/reuse-scripts/references/web-components.md +3 -3
  12. package/templates/web-components/base/README.md +3 -3
  13. package/templates/web-components/base/package.json +1 -1
  14. package/templates/web-components/first-person-controller/index.html +26 -26
  15. package/templates/web-components/first-person-controller/src/main.ts +1 -1
  16. package/templates/web-components/interactive-sphere/src/main.ts +1 -1
  17. package/templates/web-components/model-viewer/src/main.ts +1 -1
  18. package/templates/web-components/physics-playground/index.html +3 -3
  19. package/templates/web-components/physics-playground/src/main.ts +1 -1
  20. package/templates/web-components/product-configurator/src/main.ts +3 -1
  21. package/templates/web-components/spinning-cube/src/main.ts +1 -1
  22. package/templates/web-components/splat-viewer/src/main.ts +1 -1
  23. package/templates/web-components/third-person-controller/index.html +15 -15
  24. package/templates/web-components/third-person-controller/src/main.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-playcanvas",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Scaffold a PlayCanvas project",
5
5
  "keywords": [
6
6
  "playcanvas",
@@ -4,7 +4,7 @@ Represent the whole gameplay object below one semantic root:
4
4
 
5
5
  ```html
6
6
  <pc-entity name="Enemy_1" position="4 0 -3" rotation="0 90 0">
7
- <pc-rigidbody type="dynamic" mass="1"></pc-rigidbody>
7
+ <pc-rigid-body type="dynamic" mass="1"></pc-rigid-body>
8
8
  <pc-collision type="capsule"></pc-collision>
9
9
  <pc-entity position="0 0.8 0" rotation="0 180 0" scale="0.01 0.01 0.01">
10
10
  <pc-model asset="enemy"></pc-model>
@@ -14,6 +14,6 @@ Represent the whole gameplay object below one semantic root:
14
14
 
15
15
  Keep component tags as direct children of the owning `<pc-entity>`. Register the model with a
16
16
  direct-child `<pc-asset>` under `<pc-app>`. Before using rigid bodies, load Ammo with a direct-child
17
- `<pc-module name="Ammo">` carrying its `glue`, `wasm`, and `fallback` attributes; `<pc-app>` awaits
17
+ `<pc-wasm name="Ammo">` carrying its `glue`, `wasm`, and `fallback` attributes; `<pc-app>` awaits
18
18
  its direct-child modules before creating the graphics device, so every body in the document depends
19
19
  on that one element. Add and remove the semantic root as one DOM subtree.
@@ -35,10 +35,10 @@ Build supported application structure declaratively:
35
35
  - Treat a present boolean attribute as true unless its value is `false`; remove it to restore the
36
36
  Engine default.
37
37
 
38
- Load Engine `Script` modules with `<pc-asset>`, then attach them through `<pc-scripts>` and
39
- `<pc-script>`. Match the element's `name` to the class's static `scriptName`. Extra kebab-case
40
- attributes map to camelCase properties; use the `attributes` JSON attribute for nested or reserved
41
- values.
38
+ Load Engine `Script` modules with `<pc-asset>`, then attach them through `<pc-script>` and
39
+ `<pc-script-instance>`. Match the instance element's `name` to the class's static `scriptName`.
40
+ Extra kebab-case attributes map to camelCase properties; use the `attributes` JSON attribute for
41
+ nested or reserved values.
42
42
 
43
43
  Use `whenReady` for occasional page-level Engine access:
44
44
 
@@ -11,12 +11,15 @@ const instance = (name: keyof typeof ASSET_TUNING) => {
11
11
  castShadows: true
12
12
  });
13
13
  model.setLocalScale(t.scale, t.scale, t.scale);
14
- model.setLocalPosition(0, t.y, 0);
15
- model.setLocalEulerAngles(0, t.yaw, 0);
14
+ model.setLocalPosition(-t.center[0] * t.scale, t.y, -t.center[2] * t.scale);
16
15
  const root = new Entity(name);
17
- root.addChild(model);
16
+ const yaw = new Entity(`${name}-yaw`);
17
+ yaw.setLocalEulerAngles(0, t.yaw, 0);
18
+ yaw.addChild(model);
19
+ root.addChild(yaw);
18
20
  return root;
19
21
  };
20
22
  ```
21
23
 
22
- Only position and rotate the returned root. Keep the calibration inside `instance()`.
24
+ Only position and rotate the returned root. Keep the yaw, scale, and pivot compensation inside
25
+ `instance()`.
@@ -15,13 +15,18 @@ const Model = ({ id, src, heading = 0, position = [0, 0, 0] }) => {
15
15
 
16
16
  return (
17
17
  <Entity name={id} position={position} rotation={[0, heading, 0]}>
18
- <Entity position={[0, t.y, 0]} rotation={[0, t.yaw, 0]} scale={[t.scale, t.scale, t.scale]}>
19
- <Render type="asset" asset={asset} />
18
+ <Entity rotation={[0, t.yaw, 0]}>
19
+ <Entity
20
+ position={[-t.center[0] * t.scale, t.y, -t.center[2] * t.scale]}
21
+ scale={[t.scale, t.scale, t.scale]}
22
+ >
23
+ <Render type="asset" asset={asset} />
24
+ </Entity>
20
25
  </Entity>
21
26
  </Entity>
22
27
  );
23
28
  };
24
29
  ```
25
30
 
26
- Add project-specific prop types without changing the two-level transform structure. Keep hooks
31
+ Add project-specific prop types without changing the three-level transform structure. Keep hooks
27
32
  unconditional and asset URLs stable.
@@ -7,13 +7,16 @@ Keep gameplay placement on the outer entity and calibration on a visual child:
7
7
  <pc-asset id="ship" src="/models/ship.glb"></pc-asset>
8
8
  <pc-scene>
9
9
  <pc-entity name="Ship" position="12 0 -4" rotation="0 90 0">
10
- <pc-entity position="0 1.2 0" rotation="0 180 0" scale="0.9 0.9 0.9">
11
- <pc-model asset="ship"></pc-model>
10
+ <pc-entity rotation="0 180 0">
11
+ <pc-entity position="-0.45 1.2 0.225" scale="0.9 0.9 0.9">
12
+ <pc-model asset="ship"></pc-model>
13
+ </pc-entity>
12
14
  </pc-entity>
13
15
  </pc-entity>
14
16
  </pc-scene>
15
17
  </pc-app>
16
18
  ```
17
19
 
18
- Generate repeated markup from one `{ scale, y, yaw }` record per asset. Keep all vector attributes
19
- space-separated and use degrees for rotations.
20
+ Generate repeated markup from one `{ scale, y, yaw, center }` record per asset. Calculate the visual
21
+ position as `-center.x * scale, y, -center.z * scale`. Keep vector attributes space-separated and use
22
+ degrees for rotations.
@@ -1,13 +1,22 @@
1
1
  # Web Components projects
2
2
 
3
- `<pc-model>` already covers the simplest case without any script: when the loaded container has
4
- animations it adds an `anim` component and assigns `animations[0]` under the state name `animation`.
5
- It always takes the first clip and has no attribute for choosing another.
3
+ Use `<pc-anim>` inside `<pc-model>` to animate the model hierarchy. Without `<pc-anim-clip>`
4
+ children, it assigns every animation from the model's container by track name and starts the first
5
+ clip. Select and cross-fade declaratively:
6
6
 
7
- There is no animation component beyond that. Put explicit clip selection, multiple clips, state
8
- graphs, and retargeting in a module-scoped Engine `Script`, load it with `<pc-asset>`, and attach it
9
- to the same `<pc-entity>` hierarchy as the rendered model using `<pc-scripts>` and `<pc-script>`.
7
+ ```html
8
+ <pc-entity>
9
+ <pc-model asset="character">
10
+ <pc-anim clip="Idle" transition-time="0.2"></pc-anim>
11
+ </pc-model>
12
+ </pc-entity>
13
+ ```
10
14
 
11
- Pass clip names and simple configuration as `<pc-script>` attributes. Use its `attributes` JSON
12
- attribute for nested state-graph configuration. Keep scale on a visual child `<pc-entity>`, not on
13
- skinned bones, and confirm playback after `pc-app` is ready.
15
+ Add `<pc-anim-clip name="...">` children to declare a subset, rename states, set per-clip `loop` or
16
+ `speed`, or source tracks through an explicit `asset`. Use `<pc-anim>` attributes for `activate`,
17
+ `clip`, `speed`, and `transition-time`; its `play`, `pause`, and `transition` methods cover imperative
18
+ playback.
19
+
20
+ Use a module-scoped Engine `Script` only for state graphs, retargeting, or orchestration beyond that
21
+ component contract. Keep scale on a visual child `<pc-entity>`, not on skinned bones, and confirm
22
+ playback after `pc-app` is ready.
@@ -9,7 +9,7 @@ update logic. Drop its graphics device and application bootstrap because `<pc-ap
9
9
  - Express supported entities and components with `pc-*` elements.
10
10
  - Register assets as direct children of `<pc-app>` with `<pc-asset>`.
11
11
  - Put per-entity behavior in an Engine `Script` loaded through `<pc-asset>` and attached with
12
- `<pc-scripts>` and `<pc-script>`.
12
+ `<pc-script>` and `<pc-script-instance>`.
13
13
  - Use `whenReady` for page-level glue or Engine APIs with no declarative element.
14
14
  - Fetch example assets from the same Engine tag as the source example.
15
15
 
@@ -19,20 +19,27 @@ The output provides:
19
19
  - `groundOffset` for the model wrapper's local Y position;
20
20
  - `center` for the pivot offset from the geometry centre: a non-zero X or Z means the root's
21
21
  position is not where the model appears, and Y feeds grounding;
22
- - `boundsSource` for proving how precisely the bounds were measured;
22
+ - `boundsSource` and `boundsPose` for proving how and at which pose the bounds were measured;
23
+ - `requiresRuntimeCheck` for animated morphs, skins, and incomplete decoding;
23
24
  - `nodePaths`, `clips`, `joints`, and `animationTargets` for animation setup and rig comparison;
24
- - `skinned` to flag bind-pose bounds that need runtime confirmation.
25
+ - `morphed`, `morphAnimated`, and `skinned` for pose-sensitive assets.
25
26
 
26
27
  ## Bounds precision
27
28
 
28
- `aabb`, `dims`, `center`, and `groundOffset` are decoded from actual vertex positions and transformed
29
- by each node's world matrix, so they are exact and can place meshes touching without a gap. Confirm
30
- `boundsSource` before relying on that precision:
29
+ `aabb`, `dims`, `center`, and `groundOffset` are decoded from actual vertex positions, default morph
30
+ weights, and each node's world matrix. Confirm both source and pose before relying on them:
31
31
 
32
- - `vertices` means every primitive was decoded and the bounds are exact.
32
+ - `vertices` means every primitive was decoded and the bounds are exact for `boundsPose`.
33
33
  - `accessor-minmax` means at least one primitive could not be decoded, so its bounds fall back to the
34
34
  node-transformed local box. That over-estimates a mesh which does not fill its own box on a node
35
35
  rotated off-axis, by up to 41 percent for a 45-degree yaw. `boundsNotes` gives the cause.
36
+ - `incomplete` means some visible default-pose geometry could not be measured; do not calibrate from
37
+ the reported bounds.
38
+ - `null` means no usable bounds could be recovered.
39
+
40
+ `static` and `default-morph` bounds describe visible geometry at its authored default pose. `bind`
41
+ describes skinned vertices before animation. When `requiresRuntimeCheck` is true, confirm grounding
42
+ and maximum animated extent in the running application even if `boundsSource` is `vertices`.
36
43
 
37
44
  Carry `boundsSource`, `aabb`, `dims`, `center`, and `groundOffset` into the asset tuning record.
38
45
  `groundOffset` seats the base in Y; `center.x` and `center.z` are the horizontal pivot offset that
@@ -41,28 +48,30 @@ authored pivot happens to be. Running the inspector alone is not placement evide
41
48
  broad-phase spacing, but cannot locate a non-planar support surface or attachment point. Use an
42
49
  authored mount point or confirm the support in the running application before placing another model.
43
50
 
44
- ## Extract from any container with gltf-transform
51
+ ## Extract from compressed containers
45
52
 
46
- The bundled inspector decodes plain vertex buffers only. When a primitive is draco- or
47
- meshopt-compressed, sparse, or in an external buffer it falls back to `accessor-minmax` or reports
48
- nothing, and `boundsNotes` names the cause. Use gltf-transform as the precursor that normalizes any
49
- container into a plain GLB the inspector can then measure exactly. Decompress a scratch copy with the
50
- passes that would alter geometry or hierarchy disabled, and inspect the copy rather than shipping it:
53
+ The bundled inspector decodes plain, interleaved, quantized, and sparse vertex buffers. For Draco,
54
+ Meshopt, or external buffers, `boundsNotes` names the cause. Parse and rewrite a scratch copy with
55
+ glTF Transform's lossless `copy` command, then inspect that copy rather than shipping it:
51
56
 
52
57
  ```sh
53
- npx @gltf-transform/cli optimize in.glb scratch.glb \
54
- --compress false --simplify false --flatten false --join false
58
+ npx --yes @gltf-transform/cli@4.4.2 copy in.glb scratch.glb
55
59
  node <skill-directory>/scripts/inspect.mjs scratch.glb
56
60
  ```
57
61
 
62
+ `copy` preserves decoded vertex values, scenes, nodes, and materials while removing Draco and
63
+ Meshopt compression. Existing lossy compression may already have quantized the source. Do not
64
+ substitute `optimize`: it enables additional transforms unless each one is understood and disabled.
65
+
58
66
  The Engine has no `EXT_meshopt_compression` support, so a meshopt asset will not render as shipped.
59
67
  If it must appear in the scene, transcode it to an uncompressed GLB the same way — subject to the
60
68
  project's rules on modifying assets — so it both loads and measures.
61
69
 
62
- These exact bounds intentionally disagree with the Engine at runtime. `MeshInstance.aabb` transforms
63
- the mesh's declared local box, so for a node rotated off-axis it reads larger than the geometry
64
- really is. Treat that difference as expected and place from these bounds. A skinned mesh node's own
65
- transform is ignored here because glTF requires it; the Engine resolves to the same result.
70
+ These geometry-fit bounds can intentionally disagree with the Engine's runtime culling AABB.
71
+ `MeshInstance.aabb` transforms the declared local box and expands it conservatively for morph
72
+ targets, so it can be larger than visible geometry. Place static models from exact inspected bounds;
73
+ confirm morph and skin extremes at runtime. A skinned mesh node's own transform is ignored here
74
+ because glTF requires it; the Engine resolves to the same result.
66
75
 
67
76
  Shortlist files first; do not dump an entire asset pack into context. Bounds cannot prove facing.
68
77
  glTF convention is +Z forward while PlayCanvas entities face -Z, and asset packs vary. Confirm each
@@ -2,11 +2,14 @@
2
2
  // zero-dep glb metadata inspector. bounds come from decoded vertex positions, not accessor min/max,
3
3
  // so a rotated node reports its true extent and models can be placed touching.
4
4
  import fs from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
5
6
 
6
7
  const GLB_MAGIC = 0x46546c67;
7
8
  const JSON_CHUNK = 0x4e4f534a;
8
9
  const BIN_CHUNK = 0x004e4942;
9
10
  const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
11
+ const MAX_ACCESSOR_VALUES = 30_000_000;
12
+ const TYPES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
10
13
 
11
14
  // componentType -> byte size, signedness and normalized divisor (glTF 2.0 table 3.1 / 5.1)
12
15
  const COMPONENTS = {
@@ -18,6 +21,14 @@ const COMPONENTS = {
18
21
  5126: { size: 4, signed: true, norm: 1, read: (b, o) => b.readFloatLE(o) }
19
22
  };
20
23
 
24
+ const tryCatch = (fn) => {
25
+ try {
26
+ return [null, fn()];
27
+ } catch (error) {
28
+ return [error];
29
+ }
30
+ };
31
+
21
32
  const mul = (a, b) => {
22
33
  const o = new Array(16);
23
34
  for (let c = 0; c < 4; c++) {
@@ -73,41 +84,99 @@ const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
73
84
 
74
85
  // buffer 0 of a GLB is the BIN chunk; other buffers are external files or data URIs
75
86
  const bufferBytes = (g, bin, index) => {
76
- const uri = g.buffers?.[index]?.uri;
77
- if (uri === undefined) return bin;
87
+ const buffer = g.buffers?.[index];
88
+ if (!buffer) return null;
89
+ const uri = buffer.uri;
90
+ if (uri === undefined) return index === 0 ? bin : null;
78
91
  const base64 = /^data:[^,]*;base64,(.*)$/s.exec(uri);
79
92
  return base64 ? Buffer.from(base64[1], 'base64') : null;
80
93
  };
81
94
 
82
- // widen min/max with every decoded POSITION transformed by matrix. returns null on success,
83
- // otherwise a reason the vertices could not be read so the caller can fall back and say so.
84
- const accumulate = (g, bin, accessor, matrix, min, max) => {
95
+ const readAccessor = (g, bin, accessor, type = 'VEC3') => {
85
96
  const acc = g.accessors?.[accessor];
86
- if (!acc) return 'no-position-accessor';
87
- if (acc.sparse) return 'sparse-accessor';
88
- if (acc.bufferView === undefined) return 'no-vertex-data';
97
+ if (!acc) return ['no-position-accessor'];
98
+ if (acc.bufferView === undefined && !acc.sparse) return ['no-vertex-data'];
89
99
  const comp = COMPONENTS[acc.componentType];
90
- if (!comp || acc.type !== 'VEC3') return 'unsupported-accessor';
91
- const view = g.bufferViews?.[acc.bufferView];
92
- if (!view) return 'unsupported-accessor';
93
- if (view.extensions?.EXT_meshopt_compression) return 'meshopt-compressed';
94
- const bytes = bufferBytes(g, bin, view.buffer ?? 0);
95
- if (!bytes) return 'external-buffer';
96
- const stride = view.byteStride ?? comp.size * 3;
97
- const base = (view.byteOffset ?? 0) + (acc.byteOffset ?? 0);
98
- if (!acc.count || base + (acc.count - 1) * stride + comp.size * 3 > bytes.length) return 'truncated-buffer';
100
+ const size = TYPES[acc.type];
101
+ if (!comp || !size || acc.type !== type || !Number.isInteger(acc.count) || acc.count < 1) {
102
+ return ['unsupported-accessor'];
103
+ }
104
+ if (acc.count * size > MAX_ACCESSOR_VALUES) return ['accessor-too-large'];
105
+ const data = new Float32Array(acc.count * size);
99
106
  const scale = acc.normalized ? comp.norm : 1;
100
- for (let v = 0; v < acc.count; v++) {
101
- const o = base + v * stride;
102
- let x = comp.read(bytes, o) / scale;
103
- let y = comp.read(bytes, o + comp.size) / scale;
104
- let z = comp.read(bytes, o + comp.size * 2) / scale;
105
- if (acc.normalized && comp.signed) {
106
- if (x < -1) x = -1;
107
- if (y < -1) y = -1;
108
- if (z < -1) z = -1;
107
+ const read = (bytes, offset) => {
108
+ const value = comp.read(bytes, offset) / scale;
109
+ return acc.normalized && comp.signed && value < -1 ? -1 : value;
110
+ };
111
+
112
+ if (acc.bufferView !== undefined) {
113
+ const view = g.bufferViews?.[acc.bufferView];
114
+ if (!view || !Number.isInteger(view.byteLength)) return ['unsupported-accessor'];
115
+ if (view.extensions?.EXT_meshopt_compression) return ['meshopt-compressed'];
116
+ const bytes = bufferBytes(g, bin, view.buffer ?? 0);
117
+ if (!bytes) return ['external-buffer'];
118
+ const width = comp.size * size;
119
+ const stride = view.byteStride ?? width;
120
+ const base = (view.byteOffset ?? 0) + (acc.byteOffset ?? 0);
121
+ const used = (acc.byteOffset ?? 0) + (acc.count - 1) * stride + width;
122
+ if (stride < width || base < 0 || used > view.byteLength || base + (acc.count - 1) * stride + width > bytes.length) {
123
+ return ['truncated-buffer'];
124
+ }
125
+ for (let i = 0; i < acc.count; i++) {
126
+ for (let k = 0; k < size; k++) data[i * size + k] = read(bytes, base + i * stride + k * comp.size);
127
+ }
128
+ }
129
+
130
+ if (acc.sparse) {
131
+ const sparse = acc.sparse;
132
+ const idx = sparse.indices;
133
+ const values = sparse.values;
134
+ const idxComp = idx && [5121, 5123, 5125].includes(idx.componentType) ? COMPONENTS[idx.componentType] : null;
135
+ const idxView = g.bufferViews?.[idx?.bufferView];
136
+ const valueView = g.bufferViews?.[values?.bufferView];
137
+ if (!Number.isInteger(sparse.count) || sparse.count < 1 || sparse.count > acc.count || !idxComp ||
138
+ !Number.isInteger(idxView?.byteLength) || !Number.isInteger(valueView?.byteLength)) {
139
+ return ['invalid-sparse-accessor'];
140
+ }
141
+ if (idxView.extensions?.EXT_meshopt_compression || valueView.extensions?.EXT_meshopt_compression) {
142
+ return ['meshopt-compressed'];
143
+ }
144
+ const idxBytes = bufferBytes(g, bin, idxView.buffer ?? 0);
145
+ const valueBytes = bufferBytes(g, bin, valueView.buffer ?? 0);
146
+ if (!idxBytes || !valueBytes) return ['external-buffer'];
147
+ const idxBase = (idxView.byteOffset ?? 0) + (idx.byteOffset ?? 0);
148
+ const valueBase = (valueView.byteOffset ?? 0) + (values.byteOffset ?? 0);
149
+ const valueWidth = comp.size * size;
150
+ const idxUsed = (idx.byteOffset ?? 0) + sparse.count * idxComp.size;
151
+ const valueUsed = (values.byteOffset ?? 0) + sparse.count * valueWidth;
152
+ if (idxBase < 0 || valueBase < 0 || idxUsed > idxView.byteLength || valueUsed > valueView.byteLength ||
153
+ idxBase + sparse.count * idxComp.size > idxBytes.length || valueBase + sparse.count * valueWidth > valueBytes.length) {
154
+ return ['truncated-buffer'];
155
+ }
156
+ let prev = -1;
157
+ for (let i = 0; i < sparse.count; i++) {
158
+ const index = idxComp.read(idxBytes, idxBase + i * idxComp.size);
159
+ if (index <= prev || index >= acc.count) return ['invalid-sparse-accessor'];
160
+ prev = index;
161
+ for (let k = 0; k < size; k++) {
162
+ data[index * size + k] = read(valueBytes, valueBase + i * valueWidth + k * comp.size);
163
+ }
164
+ }
165
+ }
166
+
167
+ return [null, data];
168
+ };
169
+
170
+ const accumulate = (data, matrix, min, max, morphs = []) => {
171
+ for (let i = 0; i < data.length; i += 3) {
172
+ let x = data[i];
173
+ let y = data[i + 1];
174
+ let z = data[i + 2];
175
+ for (const [weight, delta] of morphs) {
176
+ x += delta[i] * weight;
177
+ y += delta[i + 1] * weight;
178
+ z += delta[i + 2] * weight;
109
179
  }
110
- // inlined to avoid allocating a vector per vertex on million-vertex meshes
111
180
  const px = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
112
181
  const py = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
113
182
  const pz = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
@@ -118,13 +187,12 @@ const accumulate = (g, bin, accessor, matrix, min, max) => {
118
187
  if (py > max[1]) max[1] = py;
119
188
  if (pz > max[2]) max[2] = pz;
120
189
  }
121
- return null;
122
190
  };
123
191
 
124
192
  // loose fallback: bound the 8 corners of the accessor's local box. over-estimates a mesh that does
125
193
  // not fill its box when the node rotation is not a multiple of 90 degrees.
126
194
  const accumulateCorners = (acc, matrix, min, max) => {
127
- if (!acc?.min || !acc?.max) return;
195
+ if (!acc?.min || !acc?.max) return false;
128
196
  for (let c = 0; c < 8; c++) {
129
197
  const point = xform(matrix, [
130
198
  c & 1 ? acc.max[0] : acc.min[0],
@@ -136,35 +204,55 @@ const accumulateCorners = (acc, matrix, min, max) => {
136
204
  if (point[k] > max[k]) max[k] = point[k];
137
205
  }
138
206
  }
207
+ return true;
139
208
  };
140
209
 
141
210
  export const inspectGlb = (buf) => {
142
211
  if (buf.length < 20 || buf.readUInt32LE(0) !== GLB_MAGIC || buf.readUInt32LE(16) !== JSON_CHUNK) {
143
212
  throw new Error('not a GLB (or JSON chunk missing)');
144
213
  }
214
+ if (buf.readUInt32LE(4) !== 2) throw new Error('unsupported GLB version');
215
+ if (buf.readUInt32LE(8) !== buf.length) throw new Error('invalid GLB length');
145
216
  const jsonLength = buf.readUInt32LE(12);
217
+ if (20 + jsonLength > buf.length) throw new Error('invalid JSON chunk length');
146
218
  const g = JSON.parse(buf.toString('utf8', 20, 20 + jsonLength));
147
219
  const binAt = 20 + jsonLength;
148
- const bin =
149
- binAt + 8 <= buf.length && buf.readUInt32LE(binAt + 4) === BIN_CHUNK
150
- ? buf.subarray(binAt + 8, binAt + 8 + buf.readUInt32LE(binAt))
151
- : null;
220
+ let bin = null;
221
+ if (binAt < buf.length) {
222
+ if (binAt + 8 > buf.length || buf.readUInt32LE(binAt + 4) !== BIN_CHUNK) {
223
+ throw new Error('invalid BIN chunk');
224
+ }
225
+ const length = buf.readUInt32LE(binAt);
226
+ if (binAt + 8 + length !== buf.length) throw new Error('invalid BIN chunk length');
227
+ bin = buf.subarray(binAt + 8);
228
+ }
152
229
  const nodes = g.nodes ?? [];
153
230
  const nodeName = (i) => nodes[i]?.name ?? `node_${i}`;
154
231
  const parents = new Array(nodes.length).fill(-1);
155
232
  for (let i = 0; i < nodes.length; i++) {
156
- for (const child of nodes[i].children ?? []) parents[child] = i;
233
+ for (const child of nodes[i].children ?? []) {
234
+ if (!Number.isInteger(child) || !nodes[child] || parents[child] !== -1) throw new Error('invalid node graph');
235
+ parents[child] = i;
236
+ }
157
237
  }
158
238
  const nodePath = (i) => {
159
239
  const names = [];
160
- for (; i >= 0; i = parents[i]) names.unshift(nodeName(i));
240
+ const seen = new Set();
241
+ for (; i >= 0; i = parents[i]) {
242
+ if (seen.has(i)) throw new Error('invalid node graph');
243
+ seen.add(i);
244
+ names.unshift(nodeName(i));
245
+ }
161
246
  return names.join('/');
162
247
  };
163
248
  const children = new Set(nodes.flatMap((n) => n.children ?? []));
164
249
  const roots = g.scenes?.[g.scene ?? 0]?.nodes ?? nodes.map((_, i) => i).filter((i) => !children.has(i));
165
250
 
166
251
  const world = new Array(nodes.length);
252
+ const seen = new Set();
167
253
  const walk = (i, parent) => {
254
+ if (!Number.isInteger(i) || !nodes[i] || seen.has(i)) throw new Error('invalid node graph');
255
+ seen.add(i);
168
256
  const n = nodes[i];
169
257
  const local = n.matrix ?? compose(n.translation ?? [0, 0, 0], n.rotation ?? [0, 0, 0, 1], n.scale ?? [1, 1, 1]);
170
258
  world[i] = mul(parent, local);
@@ -176,28 +264,67 @@ export const inspectGlb = (buf) => {
176
264
  const max = [-Infinity, -Infinity, -Infinity];
177
265
  const notes = new Set();
178
266
  let meshNodes = 0;
267
+ let morphed = false;
268
+ let fallback = false;
269
+ let incomplete = false;
179
270
  for (let i = 0; i < nodes.length; i++) {
180
271
  if (nodes[i].mesh === undefined || !world[i]) continue;
181
272
  meshNodes++;
182
273
  // glTF 2.0: "the transform of the skinned mesh node MUST be ignored" - joints drive the pose
183
274
  const matrix = nodes[i].skin === undefined ? world[i] : IDENTITY;
184
- for (const prim of g.meshes?.[nodes[i].mesh]?.primitives ?? []) {
185
- const note = prim.extensions?.KHR_draco_mesh_compression
186
- ? 'draco-compressed'
187
- : accumulate(g, bin, prim.attributes?.POSITION, matrix, min, max);
188
- if (!note) continue;
189
- notes.add(note);
190
- accumulateCorners(g.accessors?.[prim.attributes?.POSITION], matrix, min, max);
275
+ const mesh = g.meshes?.[nodes[i].mesh];
276
+ for (const prim of mesh?.primitives ?? []) {
277
+ const [error, data] = prim.extensions?.KHR_draco_mesh_compression
278
+ ? ['draco-compressed']
279
+ : readAccessor(g, bin, prim.attributes?.POSITION);
280
+ if (error) {
281
+ notes.add(error);
282
+ if (error === 'meshopt-compressed' || !accumulateCorners(g.accessors?.[prim.attributes?.POSITION], matrix, min, max)) {
283
+ incomplete = true;
284
+ } else {
285
+ fallback = true;
286
+ }
287
+ continue;
288
+ }
289
+ const morphs = [];
290
+ if (prim.targets?.length) {
291
+ morphed = true;
292
+ const weights = nodes[i].weights ?? mesh.weights ?? [];
293
+ for (let k = 0; k < prim.targets.length; k++) {
294
+ const weight = weights[k] ?? 0;
295
+ if (!Number.isFinite(weight)) {
296
+ notes.add('invalid-morph-weight');
297
+ incomplete = true;
298
+ continue;
299
+ }
300
+ const accessor = prim.targets[k].POSITION;
301
+ if (!weight || accessor === undefined) continue;
302
+ const [morphError, delta] = readAccessor(g, bin, accessor);
303
+ if (morphError || delta.length !== data.length) {
304
+ notes.add(`morph-${morphError ?? 'accessor-size'}`);
305
+ incomplete = true;
306
+ continue;
307
+ }
308
+ morphs.push([weight, delta]);
309
+ }
310
+ }
311
+ accumulate(data, matrix, min, max, morphs);
191
312
  }
192
313
  }
193
314
 
194
315
  const ok = min[0] !== Infinity;
316
+ const morphAnimated = (g.animations ?? []).some((a) =>
317
+ (a.channels ?? []).some((c) => c.target?.path === 'weights')
318
+ );
319
+ const skinned = !!g.skins?.length;
195
320
  return {
196
321
  aabb: ok ? { min: min.map(round), max: max.map(round) } : null,
197
322
  dims: ok ? max.map((v, i) => round(v - min[i])) : null,
198
323
  center: ok ? max.map((v, i) => round((v + min[i]) / 2)) : null,
199
324
  groundOffset: ok ? round(-min[1]) : null,
200
- boundsSource: ok ? (notes.size ? 'accessor-minmax' : 'vertices') : null,
325
+ boundsSource: ok ? (incomplete ? 'incomplete' : fallback ? 'accessor-minmax' : 'vertices') : null,
326
+ boundsPose: skinned ? 'bind' : morphed ? 'default-morph' : 'static',
327
+ requiresRuntimeCheck: skinned || morphAnimated || notes.size > 0,
201
328
  ...(notes.size ? { boundsNotes: [...notes].sort() } : {}),
202
329
  nodes: nodes.length,
203
330
  nodePaths: nodes.map((_, i) => nodePath(i)).sort(),
@@ -217,11 +344,15 @@ export const inspectGlb = (buf) => {
217
344
  )
218
345
  )
219
346
  ].sort(),
220
- skinned: !!g.skins?.length
347
+ morphed,
348
+ morphAnimated,
349
+ skinned
221
350
  };
222
351
  };
223
352
 
224
- if (import.meta.main ?? import.meta.filename === process.argv[1]) {
353
+ const main = import.meta.main ?? (process.argv[1] &&
354
+ fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(process.argv[1]));
355
+ if (main) {
225
356
  const files = process.argv.slice(2);
226
357
  if (!files.length) {
227
358
  console.error('usage: node inspect.mjs <file.glb> [more.glb ...]');
@@ -229,11 +360,9 @@ if (import.meta.main ?? import.meta.filename === process.argv[1]) {
229
360
  }
230
361
  // report per-file failures so one unreadable file cannot discard a whole glob
231
362
  const out = files.map((file) => {
232
- try {
233
- return { file, ...inspectGlb(fs.readFileSync(file)) };
234
- } catch (e) {
235
- return { file, error: e.message };
236
- }
363
+ const [error, result] = tryCatch(() => inspectGlb(fs.readFileSync(file)));
364
+ return error ? { file, error: error instanceof Error ? error.message : String(error) } : { file, ...result };
237
365
  });
366
+ if (out.some((result) => result.error)) process.exitCode = 1;
238
367
  console.log(JSON.stringify(out.length === 1 ? out[0] : out, null, 2));
239
368
  }
@@ -8,9 +8,9 @@ Expose an Engine script as an ES module and register it as an asset:
8
8
  <pc-scene>
9
9
  <pc-entity name="Camera">
10
10
  <pc-camera></pc-camera>
11
- <pc-scripts>
12
- <pc-script name="cameraControls" move-speed="4"></pc-script>
13
- </pc-scripts>
11
+ <pc-script>
12
+ <pc-script-instance name="cameraControls" move-speed="4"></pc-script-instance>
13
+ </pc-script>
14
14
  </pc-entity>
15
15
  </pc-scene>
16
16
  </pc-app>
@@ -39,9 +39,9 @@ so `document.querySelector` reaches them and `setAttribute` changes them.
39
39
 
40
40
  `src/main.ts` covers the two things HTML alone cannot do:
41
41
 
42
- - **Attaching engine scripts.** `<pc-script name="...">` resolves scripts that `<pc-asset>` fetched at
43
- runtime. Scripts imported through the bundler — like `CameraControls` and `Grid` here — are attached
44
- through the entity instead, reached via `whenReady()` and the element's `.entity` property.
42
+ - **Attaching engine scripts.** `<pc-script-instance name="...">` resolves scripts that `<pc-asset>`
43
+ fetched at runtime. Scripts imported through the bundler — like `CameraControls` and `Grid` here — are
44
+ attached through the entity instead, reached via `whenReady()` and the element's `.entity` property.
45
45
  - **Responding to pointer input.** `<pc-app>` picks whatever is under the pointer and dispatches an
46
46
  ordinary `PointerEvent` on the `<pc-entity>` it hit, so `addEventListener('pointerup', ...)` works on a
47
47
  3D entity exactly as it would on a `<button>`. Events are only generated for entities that have a
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@playcanvas/web-components": "^0.15.0",
18
+ "@playcanvas/web-components": "^0.17.0",
19
19
  "playcanvas": "^2.21.4"
20
20
  },
21
21
  "devDependencies": {
@@ -14,12 +14,12 @@
14
14
  <pc-material id="green-material" diffuse="#2e6638" emissive="#09140b" gloss="0.2"></pc-material>
15
15
  <pc-material id="wood-material" diffuse="#7a451a" emissive="#180e05" gloss="0.2"></pc-material>
16
16
  <pc-material id="metal-material" diffuse="#475461" emissive="#0e1113" gloss="0.5"></pc-material>
17
- <pc-module
17
+ <pc-wasm
18
18
  name="Ammo"
19
19
  glue="/ammo/ammo.wasm.js"
20
20
  wasm="/ammo/ammo.wasm.wasm"
21
21
  fallback="/ammo/ammo.js"
22
- ></pc-module>
22
+ ></pc-wasm>
23
23
  <pc-scene>
24
24
  <pc-entity name="light" rotation="50 30 0"
25
25
  ><pc-light
@@ -36,121 +36,121 @@
36
36
  <pc-entity name="floor" position="0 -0.1 0" scale="18 0.2 18"
37
37
  ><pc-render type="box" material="floor-material"></pc-render
38
38
  ><pc-collision type="box" half-extents="9 0.1 9"></pc-collision
39
- ><pc-rigidbody type="static"></pc-rigidbody
39
+ ><pc-rigid-body type="static"></pc-rigid-body
40
40
  ></pc-entity>
41
41
  <pc-entity name="back-wall" position="0 1.5 -9" scale="18 3 0.3"
42
42
  ><pc-render type="box" material="wall-material"></pc-render
43
43
  ><pc-collision type="box" half-extents="9 1.5 0.15"></pc-collision
44
- ><pc-rigidbody type="static"></pc-rigidbody
44
+ ><pc-rigid-body type="static"></pc-rigid-body
45
45
  ></pc-entity>
46
46
  <pc-entity name="front-wall" position="0 1.5 9" scale="18 3 0.3"
47
47
  ><pc-render type="box" material="wall-material"></pc-render
48
48
  ><pc-collision type="box" half-extents="9 1.5 0.15"></pc-collision
49
- ><pc-rigidbody type="static"></pc-rigidbody
49
+ ><pc-rigid-body type="static"></pc-rigid-body
50
50
  ></pc-entity>
51
51
  <pc-entity name="left-wall" position="-9 1.5 0" scale="0.3 3 18"
52
52
  ><pc-render type="box" material="wall-material"></pc-render
53
53
  ><pc-collision type="box" half-extents="0.15 1.5 9"></pc-collision
54
- ><pc-rigidbody type="static"></pc-rigidbody
54
+ ><pc-rigid-body type="static"></pc-rigid-body
55
55
  ></pc-entity>
56
56
  <pc-entity name="right-wall" position="9 1.5 0" scale="0.3 3 18"
57
57
  ><pc-render type="box" material="wall-material"></pc-render
58
58
  ><pc-collision type="box" half-extents="0.15 1.5 9"></pc-collision
59
- ><pc-rigidbody type="static"></pc-rigidbody
59
+ ><pc-rigid-body type="static"></pc-rigid-body
60
60
  ></pc-entity>
61
61
  <pc-entity name="blue-crate" position="-2 0.75 -3" scale="1.5 1.5 1.5"
62
62
  ><pc-render type="box" material="blue-material"></pc-render
63
63
  ><pc-collision type="box" half-extents="0.75 0.75 0.75"></pc-collision
64
- ><pc-rigidbody type="static"></pc-rigidbody
64
+ ><pc-rigid-body type="static"></pc-rigid-body
65
65
  ></pc-entity>
66
66
  <pc-entity name="green-crate" position="3 0.5 1" scale="2.5 1 1"
67
67
  ><pc-render type="box" material="green-material"></pc-render
68
68
  ><pc-collision type="box" half-extents="1.25 0.5 0.5"></pc-collision
69
- ><pc-rigidbody type="static"></pc-rigidbody
69
+ ><pc-rigid-body type="static"></pc-rigid-body
70
70
  ></pc-entity>
71
71
  <pc-entity name="cargo-left-0" position="-5.5 0.75 -5.5" scale="2 1.5 2"
72
72
  ><pc-render type="box" material="wood-material"></pc-render
73
73
  ><pc-collision type="box" half-extents="1 0.75 1"></pc-collision
74
- ><pc-rigidbody type="static"></pc-rigidbody
74
+ ><pc-rigid-body type="static"></pc-rigid-body
75
75
  ></pc-entity>
76
76
  <pc-entity name="cargo-left-1" position="-5.5 2 -5.5" scale="1.5 1 1.5"
77
77
  ><pc-render type="box" material="blue-material"></pc-render
78
78
  ><pc-collision type="box" half-extents="0.75 0.5 0.75"></pc-collision
79
- ><pc-rigidbody type="static"></pc-rigidbody
79
+ ><pc-rigid-body type="static"></pc-rigid-body
80
80
  ></pc-entity>
81
81
  <pc-entity name="cargo-right-0" position="5.3 0.6 -4.8" scale="2.4 1.2 2"
82
82
  ><pc-render type="box" material="green-material"></pc-render
83
83
  ><pc-collision type="box" half-extents="1.2 0.6 1"></pc-collision
84
- ><pc-rigidbody type="static"></pc-rigidbody
84
+ ><pc-rigid-body type="static"></pc-rigid-body
85
85
  ></pc-entity>
86
86
  <pc-entity name="cargo-right-1" position="5.3 1.7 -4.8" scale="1.5 1 1.5"
87
87
  ><pc-render type="box" material="wood-material"></pc-render
88
88
  ><pc-collision type="box" half-extents="0.75 0.5 0.75"></pc-collision
89
- ><pc-rigidbody type="static"></pc-rigidbody
89
+ ><pc-rigid-body type="static"></pc-rigid-body
90
90
  ></pc-entity>
91
91
  <pc-entity name="cargo-center" position="0 0.6 -6.5" scale="2.2 1.2 1.8"
92
92
  ><pc-render type="box" material="blue-material"></pc-render
93
93
  ><pc-collision type="box" half-extents="1.1 0.6 0.9"></pc-collision
94
- ><pc-rigidbody type="static"></pc-rigidbody
94
+ ><pc-rigid-body type="static"></pc-rigid-body
95
95
  ></pc-entity>
96
96
  <pc-entity name="shelf-left-post-0" position="-6.8 1.4 -3.5" scale="0.25 2.8 0.25"
97
97
  ><pc-render type="box" material="metal-material"></pc-render
98
98
  ><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
99
- ><pc-rigidbody type="static"></pc-rigidbody
99
+ ><pc-rigid-body type="static"></pc-rigid-body
100
100
  ></pc-entity>
101
101
  <pc-entity name="shelf-left-post-1" position="-6.8 1.4 0" scale="0.25 2.8 0.25"
102
102
  ><pc-render type="box" material="metal-material"></pc-render
103
103
  ><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
104
- ><pc-rigidbody type="static"></pc-rigidbody
104
+ ><pc-rigid-body type="static"></pc-rigid-body
105
105
  ></pc-entity>
106
106
  <pc-entity name="shelf-left-post-2" position="-6.8 1.4 3.5" scale="0.25 2.8 0.25"
107
107
  ><pc-render type="box" material="metal-material"></pc-render
108
108
  ><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
109
- ><pc-rigidbody type="static"></pc-rigidbody
109
+ ><pc-rigid-body type="static"></pc-rigid-body
110
110
  ></pc-entity>
111
111
  <pc-entity name="shelf-right-post-0" position="6.8 1.4 -3.5" scale="0.25 2.8 0.25"
112
112
  ><pc-render type="box" material="metal-material"></pc-render
113
113
  ><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
114
- ><pc-rigidbody type="static"></pc-rigidbody
114
+ ><pc-rigid-body type="static"></pc-rigid-body
115
115
  ></pc-entity>
116
116
  <pc-entity name="shelf-right-post-1" position="6.8 1.4 0" scale="0.25 2.8 0.25"
117
117
  ><pc-render type="box" material="metal-material"></pc-render
118
118
  ><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
119
- ><pc-rigidbody type="static"></pc-rigidbody
119
+ ><pc-rigid-body type="static"></pc-rigid-body
120
120
  ></pc-entity>
121
121
  <pc-entity name="shelf-right-post-2" position="6.8 1.4 3.5" scale="0.25 2.8 0.25"
122
122
  ><pc-render type="box" material="metal-material"></pc-render
123
123
  ><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
124
- ><pc-rigidbody type="static"></pc-rigidbody
124
+ ><pc-rigid-body type="static"></pc-rigid-body
125
125
  ></pc-entity>
126
126
  <pc-entity name="shelf-left-low" position="-6.8 0.65 0" scale="1.1 0.18 7.5"
127
127
  ><pc-render type="box" material="metal-material"></pc-render
128
128
  ><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
129
- ><pc-rigidbody type="static"></pc-rigidbody
129
+ ><pc-rigid-body type="static"></pc-rigid-body
130
130
  ></pc-entity>
131
131
  <pc-entity name="shelf-left-high" position="-6.8 2.1 0" scale="1.1 0.18 7.5"
132
132
  ><pc-render type="box" material="metal-material"></pc-render
133
133
  ><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
134
- ><pc-rigidbody type="static"></pc-rigidbody
134
+ ><pc-rigid-body type="static"></pc-rigid-body
135
135
  ></pc-entity>
136
136
  <pc-entity name="shelf-right-low" position="6.8 0.65 0" scale="1.1 0.18 7.5"
137
137
  ><pc-render type="box" material="metal-material"></pc-render
138
138
  ><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
139
- ><pc-rigidbody type="static"></pc-rigidbody
139
+ ><pc-rigid-body type="static"></pc-rigid-body
140
140
  ></pc-entity>
141
141
  <pc-entity name="shelf-right-high" position="6.8 2.1 0" scale="1.1 0.18 7.5"
142
142
  ><pc-render type="box" material="metal-material"></pc-render
143
143
  ><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
144
- ><pc-rigidbody type="static"></pc-rigidbody
144
+ ><pc-rigid-body type="static"></pc-rigid-body
145
145
  ></pc-entity>
146
146
  <pc-entity name="pallet" position="-2.5 0.15 2.5" scale="3 0.3 2"
147
147
  ><pc-render type="box" material="wood-material"></pc-render
148
148
  ><pc-collision type="box" half-extents="1.5 0.15 1"></pc-collision
149
- ><pc-rigidbody type="static"></pc-rigidbody
149
+ ><pc-rigid-body type="static"></pc-rigid-body
150
150
  ></pc-entity>
151
151
  <pc-entity name="player" position="0 1 5">
152
152
  <pc-collision type="capsule" radius="0.45" height="1.8"></pc-collision>
153
- <pc-rigidbody type="dynamic" mass="80" angular-factor="0 0 0"></pc-rigidbody>
153
+ <pc-rigid-body type="dynamic" mass="80" angular-factor="0 0 0"></pc-rigid-body>
154
154
  <pc-entity name="camera" position="0 0.65 0"
155
155
  ><pc-camera clear-color="#61a3db" fov="80"></pc-camera
156
156
  ></pc-entity>
@@ -6,7 +6,7 @@ import './starter.css';
6
6
 
7
7
  await Promise.all([
8
8
  whenReady('pc-entity[name="player"] > pc-collision'),
9
- whenReady('pc-entity[name="player"] > pc-rigidbody')
9
+ whenReady('pc-entity[name="player"] > pc-rigid-body')
10
10
  ]);
11
11
  const [player, camera] = await Promise.all([
12
12
  whenReady<EntityElement>('pc-entity[name="player"]'),
@@ -47,7 +47,7 @@ if (skyboxLayer) {
47
47
  }
48
48
 
49
49
  // Engine scripts are resolved by the bundler, so they are attached through the entity rather than
50
- // with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
50
+ // with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
51
51
  const camera = cameraComponent.closestEntity;
52
52
  camera?.entity?.addComponent('script');
53
53
  camera?.entity?.script?.create(CameraControls, { properties: { sceneSize: 3 } });
@@ -14,7 +14,7 @@ const [cameraComponent, sky] = await Promise.all([
14
14
  ]);
15
15
 
16
16
  // Engine scripts are resolved by the bundler, so they are attached through the entity rather than
17
- // with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
17
+ // with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
18
18
  const camera = cameraComponent.closestEntity;
19
19
  camera?.entity?.addComponent('script');
20
20
  camera?.entity?.script?.create(CameraControls, { properties: { sceneSize: 2.5 } });
@@ -8,12 +8,12 @@
8
8
  </head>
9
9
  <body>
10
10
  <pc-app>
11
- <pc-module
11
+ <pc-wasm
12
12
  name="Ammo"
13
13
  glue="/ammo/ammo.wasm.js"
14
14
  wasm="/ammo/ammo.wasm.wasm"
15
15
  fallback="/ammo/ammo.js"
16
- ></pc-module>
16
+ ></pc-wasm>
17
17
  <pc-material id="floor-material" diffuse="#2e333d" gloss="0.2"></pc-material>
18
18
  <pc-material id="cyan-material" diffuse="#29ade6" gloss="0.45"></pc-material>
19
19
  <pc-material id="coral-material" diffuse="#f25738" gloss="0.45"></pc-material>
@@ -34,7 +34,7 @@
34
34
  <pc-entity name="floor" position="0 -0.1 0" scale="10 0.2 10"
35
35
  ><pc-render type="box" material="floor-material"></pc-render
36
36
  ><pc-collision type="box" half-extents="5 0.1 5"></pc-collision
37
- ><pc-rigidbody type="static"></pc-rigidbody
37
+ ><pc-rigid-body type="static"></pc-rigid-body
38
38
  ></pc-entity>
39
39
  <pc-entity id="bodies"></pc-entity>
40
40
  </pc-scene>
@@ -18,7 +18,7 @@ const spawn = () => {
18
18
  const type = TYPES[i % TYPES.length];
19
19
  bodies.insertAdjacentHTML(
20
20
  'beforeend',
21
- `<pc-entity data-body position="${((i % 4) - 1.5) * 1.1} ${0.7 + Math.floor(i / 4) * 1.1} ${(Math.floor(i / 4) - 1) * 1.4}" rotation="${i * 13} ${i * 29} 0" scale="${SCALES[i % SCALES.length]}"><pc-render type="${type}" material="${MATERIALS[i % MATERIALS.length]}"></pc-render><pc-collision type="${type}"></pc-collision><pc-rigidbody type="dynamic" mass="1" restitution="0.35"></pc-rigidbody></pc-entity>`
21
+ `<pc-entity data-body position="${((i % 4) - 1.5) * 1.1} ${0.7 + Math.floor(i / 4) * 1.1} ${(Math.floor(i / 4) - 1) * 1.4}" rotation="${i * 13} ${i * 29} 0" scale="${SCALES[i % SCALES.length]}"><pc-render type="${type}" material="${MATERIALS[i % MATERIALS.length]}"></pc-render><pc-collision type="${type}"></pc-collision><pc-rigid-body type="dynamic" mass="1" restitution="0.35"></pc-rigid-body></pc-entity>`
22
22
  );
23
23
  };
24
24
  const reset = () => {
@@ -58,8 +58,10 @@ ground.addComponent('render', {
58
58
  receiveShadows: true
59
59
  });
60
60
 
61
+ // The element's entity is a host whose transform composes with the GLB's authored root rotation
62
+ // (a -90° X up-axis fix), so only the yaw is set here
61
63
  const entity = model.entity!;
62
- entity.setLocalEulerAngles(-90, -25, 0);
64
+ entity.setLocalEulerAngles(0, -25, 0);
63
65
  entity.root.syncHierarchy();
64
66
  const meshes = (entity.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
65
67
  const bounds = meshes[0].aabb.clone();
@@ -14,7 +14,7 @@ class Rotate extends Script {
14
14
  }
15
15
 
16
16
  // Engine scripts are resolved by the bundler, so they are attached through the entity rather than
17
- // with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
17
+ // with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
18
18
  const cube = await whenReady<EntityElement>('pc-entity[name="cube"]');
19
19
 
20
20
  cube.entity?.addComponent('script');
@@ -9,7 +9,7 @@ import './style.css';
9
9
  const cameraComponent = await whenReady('pc-camera');
10
10
 
11
11
  // Engine scripts are resolved by the bundler, so they are attached through the entity rather than
12
- // with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
12
+ // with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
13
13
  const camera = cameraComponent.closestEntity;
14
14
  camera?.entity?.addComponent('script');
15
15
  camera?.entity?.script?.create(CameraControls, { properties: { sceneSize: 2 } });
@@ -16,12 +16,12 @@
16
16
  <pc-material id="shirt-material" diffuse="#268bd2"></pc-material>
17
17
  <pc-material id="skin-material" diffuse="#c78759"></pc-material>
18
18
  <pc-material id="pants-material" diffuse="#2e4a85"></pc-material>
19
- <pc-module
19
+ <pc-wasm
20
20
  name="Ammo"
21
21
  glue="/ammo/ammo.wasm.js"
22
22
  wasm="/ammo/ammo.wasm.wasm"
23
23
  fallback="/ammo/ammo.js"
24
- ></pc-module>
24
+ ></pc-wasm>
25
25
  <pc-scene>
26
26
  <pc-entity name="camera" position="0 3 6"><pc-camera clear-color="#7ab8e6"></pc-camera></pc-entity>
27
27
  <pc-entity name="light" rotation="45 30 0"
@@ -36,32 +36,32 @@
36
36
  <pc-entity name="ground" position="0 -0.1 0" scale="20 0.2 20"
37
37
  ><pc-render type="box" material="ground-material"></pc-render
38
38
  ><pc-collision type="box" half-extents="10 0.1 10"></pc-collision
39
- ><pc-rigidbody type="static"></pc-rigidbody
39
+ ><pc-rigid-body type="static"></pc-rigid-body
40
40
  ></pc-entity>
41
41
  <pc-entity name="block-0" position="-4.5 0.5 -3.5" scale="2.4 1 2.4"
42
42
  ><pc-render type="box" material="block-material"></pc-render
43
43
  ><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
44
- ><pc-rigidbody type="static"></pc-rigidbody
44
+ ><pc-rigid-body type="static"></pc-rigid-body
45
45
  ></pc-entity>
46
46
  <pc-entity name="block-1" position="4.2 0.5 -2.8" rotation="0 25 0" scale="2.4 1 2.4"
47
47
  ><pc-render type="box" material="block-material"></pc-render
48
48
  ><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
49
- ><pc-rigidbody type="static"></pc-rigidbody
49
+ ><pc-rigid-body type="static"></pc-rigid-body
50
50
  ></pc-entity>
51
51
  <pc-entity name="block-2" position="-3.7 0.5 3.2" rotation="0 50 0" scale="2.4 1 2.4"
52
52
  ><pc-render type="box" material="block-material"></pc-render
53
53
  ><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
54
- ><pc-rigidbody type="static"></pc-rigidbody
54
+ ><pc-rigid-body type="static"></pc-rigid-body
55
55
  ></pc-entity>
56
56
  <pc-entity name="block-3" position="3.8 0.5 3.6" rotation="0 75 0" scale="2.4 1 2.4"
57
57
  ><pc-render type="box" material="block-material"></pc-render
58
58
  ><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
59
- ><pc-rigidbody type="static"></pc-rigidbody
59
+ ><pc-rigid-body type="static"></pc-rigid-body
60
60
  ></pc-entity>
61
61
  <pc-entity name="tree-0-trunk" position="-7 0.99 -6" scale="0.418 1.98 0.418"
62
62
  ><pc-render type="box" material="wood-material"></pc-render
63
63
  ><pc-collision type="box" half-extents="0.209 0.99 0.209"></pc-collision
64
- ><pc-rigidbody type="static"></pc-rigidbody
64
+ ><pc-rigid-body type="static"></pc-rigid-body
65
65
  ></pc-entity>
66
66
  <pc-entity name="tree-0-crown-0" position="-7 2.09 -6" scale="2.31 1.98 2.31"
67
67
  ><pc-render type="cone" material="leaves-material"></pc-render
@@ -75,7 +75,7 @@
75
75
  <pc-entity name="tree-1-trunk" position="6.5 1.125 -6.5" scale="0.475 2.25 0.475"
76
76
  ><pc-render type="box" material="wood-material"></pc-render
77
77
  ><pc-collision type="box" half-extents="0.2375 1.125 0.2375"></pc-collision
78
- ><pc-rigidbody type="static"></pc-rigidbody
78
+ ><pc-rigid-body type="static"></pc-rigid-body
79
79
  ></pc-entity>
80
80
  <pc-entity name="tree-1-crown-0" position="6.5 2.375 -6.5" scale="2.625 2.25 2.625"
81
81
  ><pc-render type="cone" material="leaves-material"></pc-render
@@ -89,7 +89,7 @@
89
89
  <pc-entity name="tree-2-trunk" position="-7.5 0.81 5" scale="0.342 1.62 0.342"
90
90
  ><pc-render type="box" material="wood-material"></pc-render
91
91
  ><pc-collision type="box" half-extents="0.171 0.81 0.171"></pc-collision
92
- ><pc-rigidbody type="static"></pc-rigidbody
92
+ ><pc-rigid-body type="static"></pc-rigid-body
93
93
  ></pc-entity>
94
94
  <pc-entity name="tree-2-crown-0" position="-7.5 1.71 5" scale="1.89 1.62 1.89"
95
95
  ><pc-render type="cone" material="leaves-material"></pc-render
@@ -103,7 +103,7 @@
103
103
  <pc-entity name="tree-3-trunk" position="7 1.035 5.5" scale="0.437 2.07 0.437"
104
104
  ><pc-render type="box" material="wood-material"></pc-render
105
105
  ><pc-collision type="box" half-extents="0.2185 1.035 0.2185"></pc-collision
106
- ><pc-rigidbody type="static"></pc-rigidbody
106
+ ><pc-rigid-body type="static"></pc-rigid-body
107
107
  ></pc-entity>
108
108
  <pc-entity name="tree-3-crown-0" position="7 2.185 5.5" scale="2.415 2.07 2.415"
109
109
  ><pc-render type="cone" material="leaves-material"></pc-render
@@ -117,7 +117,7 @@
117
117
  <pc-entity name="tree-4-trunk" position="-2.2 0.765 -8" scale="0.323 1.53 0.323"
118
118
  ><pc-render type="box" material="wood-material"></pc-render
119
119
  ><pc-collision type="box" half-extents="0.1615 0.765 0.1615"></pc-collision
120
- ><pc-rigidbody type="static"></pc-rigidbody
120
+ ><pc-rigid-body type="static"></pc-rigid-body
121
121
  ></pc-entity>
122
122
  <pc-entity name="tree-4-crown-0" position="-2.2 1.615 -8" scale="1.785 1.53 1.785"
123
123
  ><pc-render type="cone" material="leaves-material"></pc-render
@@ -131,16 +131,16 @@
131
131
  <pc-entity name="fallen-log" position="0 0.35 6" scale="3.8 0.7 0.7"
132
132
  ><pc-render type="box" material="wood-material"></pc-render
133
133
  ><pc-collision type="box" half-extents="1.9 0.35 0.35"></pc-collision
134
- ><pc-rigidbody type="static"></pc-rigidbody
134
+ ><pc-rigid-body type="static"></pc-rigid-body
135
135
  ></pc-entity>
136
136
  <pc-entity name="stump" position="-1.8 0.4 -2.2" scale="0.9 0.8 0.9"
137
137
  ><pc-render type="box" material="wood-material"></pc-render
138
138
  ><pc-collision type="box" half-extents="0.45 0.4 0.45"></pc-collision
139
- ><pc-rigidbody type="static"></pc-rigidbody
139
+ ><pc-rigid-body type="static"></pc-rigid-body
140
140
  ></pc-entity>
141
141
  <pc-entity name="player" position="0 1.1 0">
142
142
  <pc-collision type="capsule" radius="0.5" height="2.4" linear-offset="0 0.1 0"></pc-collision>
143
- <pc-rigidbody type="dynamic" mass="70" angular-factor="0 0 0"></pc-rigidbody>
143
+ <pc-rigid-body type="dynamic" mass="70" angular-factor="0 0 0"></pc-rigid-body>
144
144
  <pc-entity name="character" position="0 0.2 0">
145
145
  <pc-entity name="body" scale="0.56 0.88 0.38"
146
146
  ><pc-render type="box" material="shirt-material"></pc-render
@@ -6,7 +6,7 @@ import './starter.css';
6
6
 
7
7
  await Promise.all([
8
8
  whenReady('pc-entity[name="player"] > pc-collision'),
9
- whenReady('pc-entity[name="player"] > pc-rigidbody')
9
+ whenReady('pc-entity[name="player"] > pc-rigid-body')
10
10
  ]);
11
11
  const [player, camera, model] = await Promise.all([
12
12
  whenReady<EntityElement>('pc-entity[name="player"]'),