diff --git a/examples/animation.html b/examples/animation.html
index a6d6acf..c78ae12 100644
--- a/examples/animation.html
+++ b/examples/animation.html
@@ -20,7 +20,7 @@
-
+
diff --git a/examples/basic-shapes.html b/examples/basic-shapes.html
index b9ed1b0..7db8c07 100644
--- a/examples/basic-shapes.html
+++ b/examples/basic-shapes.html
@@ -19,7 +19,7 @@
-
+
diff --git a/examples/car-configurator.html b/examples/car-configurator.html
index 300547f..950720f 100644
--- a/examples/car-configurator.html
+++ b/examples/car-configurator.html
@@ -23,7 +23,7 @@
-
+
diff --git a/examples/glb.html b/examples/glb.html
index 12ce866..234d227 100644
--- a/examples/glb.html
+++ b/examples/glb.html
@@ -20,7 +20,7 @@
-
+
diff --git a/examples/physics.html b/examples/physics.html
index 5a4c1b4..22640a2 100644
--- a/examples/physics.html
+++ b/examples/physics.html
@@ -20,9 +20,9 @@
-
+
-
+
diff --git a/examples/positional-sound.html b/examples/positional-sound.html
index ef2bde8..8cb77bc 100644
--- a/examples/positional-sound.html
+++ b/examples/positional-sound.html
@@ -19,7 +19,7 @@
-
+
diff --git a/examples/scripts/grid.mjs b/examples/scripts/grid.mjs
deleted file mode 100644
index be00275..0000000
--- a/examples/scripts/grid.mjs
+++ /dev/null
@@ -1,271 +0,0 @@
-import {
- ShaderMaterial,
- SEMANTIC_POSITION,
- SEMANTIC_TEXCOORD0,
- BLEND_NORMAL,
- CULLFACE_NONE,
- PlaneGeometry,
- Mesh,
- MeshInstance,
- Color,
- Script,
- Vec2
-} from 'playcanvas';
-
-const tmpVa = new Vec2();
-
-const EPISILON = 1e-3;
-
-const vertexCode = /* glsl */ `
- attribute vec3 vertex_position;
- attribute vec2 aUv0;
-
- uniform mat4 matrix_model;
- uniform mat4 matrix_viewProjection;
-
- varying vec2 uv0;
-
- void main(void) {
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
- uv0 = aUv0;
- }
-`;
-
-const fragmentCode = /* glsl */ `
- uniform vec2 uHalfExtents;
- uniform vec3 uColorX;
- uniform vec3 uColorZ;
-
- varying vec2 uv0;
-
- // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c
- float pristineGrid(in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) {
- vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y)));
- bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5);
- vec2 targetWidth = vec2(
- invertLine.x ? 1.0 - lineWidth.x : lineWidth.x,
- invertLine.y ? 1.0 - lineWidth.y : lineWidth.y
- );
- vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
- vec2 lineAA = uvDeriv * 1.5;
- vec2 gridUV = abs(fract(uv) * 2.0 - 1.0);
- gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x;
- gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y;
- vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
-
- grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
- grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0));
- grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x;
- grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y;
-
- return mix(grid2.x, 1.0, grid2.y);
- }
-
- void main(void) {
- vec2 uv = uv0;
-
- vec2 pos = (uv * 2.0 - 1.0) * uHalfExtents;
- vec2 ddx = dFdx(pos);
- vec2 ddy = dFdy(pos);
-
- float epsilon = 1.0 / 255.0;
-
- vec2 levelPos;
- float levelSize;
- float levelAlpha;
-
- levelPos = pos * 0.1;
- levelSize = 2.0 / 1000.0;
- levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2(levelSize));
- if (levelAlpha > epsilon) {
- vec3 color;
- if (abs(levelPos.x) < levelSize) {
- if (abs(levelPos.y) < levelSize) {
- color = vec3(1.0);
- } else {
- color = uColorZ;
- }
- } else if (abs(levelPos.y) < levelSize) {
- color = uColorX;
- } else {
- color = vec3(0.9);
- }
- gl_FragColor = vec4(color, levelAlpha);
- return;
- }
-
- levelPos = pos;
- levelSize = 1.0 / 100.0;
- levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2(levelSize));
- if (levelAlpha > epsilon) {
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
- return;
- }
-
- levelPos = pos * 10.0;
- levelSize = 1.0 / 100.0;
- levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2(levelSize));
- if (levelAlpha > epsilon) {
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
- return;
- }
-
- discard;
- }
-`;
-
-class Grid extends Script {
- /**
- * @type {ShaderMaterial}
- * @private
- */
- _material;
-
- /**
- * @type {MeshInstance}
- * @private
- */
- _meshInstance;
-
- /**
- * @type {Vec2}
- * @private
- */
- _halfExtents = new Vec2();
-
- /**
- * @type {Color}
- * @private
- */
- _colorX = new Color(1, 0.3, 0.3);
-
- /**
- * @type {Color}
- * @private
- */
- _colorZ = new Color(0.3, 0.3, 1);
-
- constructor(args) {
- super(args);
- const {
- colorX,
- colorZ
- } = args.attributes;
-
- // ensure the entity has a render component
- if (!this.entity.render) {
- this.entity.addComponent('render');
- }
-
- // create shader material
- this._material = new ShaderMaterial({
- uniqueName: 'grid-shader',
- vertexCode,
- fragmentCode,
- attributes: {
- vertex_position: SEMANTIC_POSITION,
- aUv0: SEMANTIC_TEXCOORD0
- }
- });
- this._material.blendType = BLEND_NORMAL;
- this._material.cull = CULLFACE_NONE;
- this._material.update();
-
- // create mesh
- const mesh = Mesh.fromGeometry(this.app.graphicsDevice, new PlaneGeometry());
- this._meshInstance = new MeshInstance(mesh, this._material);
- this.entity.render.meshInstances = [this._meshInstance];
-
- // set the initial values
- this.halfExtents = this._calcHalfExtents(tmpVa) ?? this.halfExtents;
- this.colorX = colorX ?? this.colorX;
- this.colorZ = colorZ ?? this.colorZ;
-
- // update the half extents when the entity scale changes
- this.app.on('prerender', () => {
- const halfExtents = this._calcHalfExtents(tmpVa);
- if (this.halfExtents.distance(halfExtents) > EPISILON) {
- this.halfExtents = halfExtents;
- }
- });
- }
-
- /**
- * @param {Vec2} vec - The vector to copy the half extents to.
- * @returns {Vec2} - The half extents.
- * @private
- */
- _calcHalfExtents(vec) {
- const scale = this.entity.getLocalScale();
- return vec.set(scale.x / 2, scale.z / 2);
- }
-
- /**
- * @param {string} name - The name of the parameter.
- * @param {Color|Vec2} value - The value of the parameter.
- * @private
- */
- _set(name, value) {
- if (value instanceof Color) {
- this._material.setParameter(name, [value.r, value.g, value.b]);
- }
-
- if (value instanceof Vec2) {
- this._material.setParameter(name, [value.x, value.y]);
- }
-
- this._material.update();
-
- this._meshInstance.material = this._material;
- }
-
- /**
- * @attribute
- * @type {Vec2}
- */
- set halfExtents(value) {
- if (!(value instanceof Vec2)) {
- return;
- }
- this._halfExtents.copy(value);
- this._set('uHalfExtents', this._halfExtents);
- }
-
- get halfExtents() {
- return this._halfExtents;
- }
-
- /**
- * @attribute
- * @type {Color}
- */
- set colorX(value) {
- if (!(value instanceof Color)) {
- return;
- }
- this._colorX.copy(value);
- this._set('uColorX', this._colorX);
- }
-
- get colorX() {
- return this._colorX;
- }
-
- /**
- * @attribute
- * @type {Color}
- */
- set colorZ(value) {
- if (!(value instanceof Color)) {
- return;
- }
- this._colorZ.copy(value);
- this._set('uColorZ', this._colorZ);
- }
-
- get colorZ() {
- return this._colorZ;
- }
-}
-
-export { Grid };
diff --git a/examples/scripts/xr-navigation.mjs b/examples/scripts/xr-navigation.mjs
deleted file mode 100644
index 451906a..0000000
--- a/examples/scripts/xr-navigation.mjs
+++ /dev/null
@@ -1,162 +0,0 @@
-import { Color, Quat, Script, Vec3 } from 'playcanvas';
-
-/** @import { XrInputSource } from 'playcanvas' */
-
-/**
- * Handles VR teleportation navigation by allowing users to point and teleport using either
- * hands or tracked controllers. Shows a visual ray and target indicator when the user holds
- * the select button (trigger) or makes a pinch gesture with hand tracking, and teleports to
- * the target location when released.
- *
- * This script should be attached to a parent entity of the camera entity used for the XR
- * session. Use it in conjunction with the `XrControllers` script to handle the rendering of
- * the controllers.
- */
-export class XrNavigation extends Script {
- /** @type {Set} */
- inputSources = new Set();
-
- /** @type {Map} */
- activePointers = new Map();
-
- validColor = new Color(0, 1, 0); // Green for valid teleport
-
- invalidColor = new Color(1, 0, 0); // Red for invalid teleport
-
- /** @type {Map} */
- inputHandlers = new Map();
-
- initialize() {
- const positionRoot = new Vec3();
- const rotationRoot = new Quat();
- const positionCamera = new Vec3();
- const rotationCamera = new Quat();
-
- this.app.xr.on('start', () => {
- positionRoot.copy(this.entity.getPosition());
- rotationRoot.copy(this.entity.getRotation());
-
- const camera = this.entity.findComponent('camera')?.entity;
- if (camera) {
- positionCamera.copy(camera.getPosition());
- rotationCamera.copy(camera.getRotation());
-
- this.entity.setPosition(positionCamera.x, 0, positionCamera.z);
- this.entity.lookAt(Vec3.ZERO, Vec3.UP);
- }
- });
-
- this.app.xr.on('end', () => {
- this.entity.setPosition(positionRoot);
- this.entity.setRotation(rotationRoot);
-
- const camera = this.entity.findComponent('camera')?.entity;
- if (camera) {
- camera.setPosition(positionCamera);
- camera.setRotation(rotationCamera);
- }
- });
-
- this.app.xr.input.on('add', (inputSource) => {
- const handleSelectStart = () => {
- this.activePointers.set(inputSource, true);
- };
-
- const handleSelectEnd = () => {
- this.activePointers.set(inputSource, false);
- this.tryTeleport(inputSource);
- };
-
- // Attach the handlers
- inputSource.on('selectstart', handleSelectStart);
- inputSource.on('selectend', handleSelectEnd);
-
- // Store the handlers in the map
- this.inputHandlers.set(inputSource, { handleSelectStart, handleSelectEnd });
- this.inputSources.add(inputSource);
- });
-
- this.app.xr.input.on('remove', (inputSource) => {
- const handlers = this.inputHandlers.get(inputSource);
- if (handlers) {
- inputSource.off('selectstart', handlers.handleSelectStart);
- inputSource.off('selectend', handlers.handleSelectEnd);
- this.inputHandlers.delete(inputSource);
- }
- this.activePointers.delete(inputSource);
- this.inputSources.delete(inputSource);
- });
- }
-
- findPlaneIntersection(origin, direction) {
- // Find intersection with y=0 plane
- if (Math.abs(direction.y) < 0.00001) return null; // Ray is parallel to plane
-
- const t = -origin.y / direction.y;
- if (t < 0) return null; // Intersection is behind the ray
-
- return new Vec3(
- origin.x + direction.x * t,
- 0,
- origin.z + direction.z * t
- );
- }
-
- tryTeleport(inputSource) {
- const origin = inputSource.getOrigin();
- const direction = inputSource.getDirection();
-
- const hitPoint = this.findPlaneIntersection(origin, direction);
- if (hitPoint) {
- const cameraY = this.entity.getPosition().y;
- hitPoint.y = cameraY;
- this.entity.setPosition(hitPoint);
- }
- }
-
- update() {
- for (const inputSource of this.inputSources) {
- // Only show ray when trigger is pressed
- if (!this.activePointers.get(inputSource)) continue;
-
- const start = inputSource.getOrigin();
- const direction = inputSource.getDirection();
-
- const hitPoint = this.findPlaneIntersection(start, direction);
-
- if (hitPoint) {
- // Draw line to intersection point
- this.app.drawLine(start, hitPoint, this.validColor);
- this.drawTeleportIndicator(hitPoint);
- } else {
- // Draw full length ray if no intersection
- const end = start.clone().add(
- direction.clone().mulScalar(100)
- );
- this.app.drawLine(start, end, this.invalidColor);
- }
- }
- }
-
- drawTeleportIndicator(point) {
- // Draw a circle at the teleport point
- const segments = 32;
- const radius = 0.2;
-
- for (let i = 0; i < segments; i++) {
- const angle1 = (i / segments) * Math.PI * 2;
- const angle2 = ((i + 1) / segments) * Math.PI * 2;
-
- const x1 = point.x + Math.cos(angle1) * radius;
- const z1 = point.z + Math.sin(angle1) * radius;
- const x2 = point.x + Math.cos(angle2) * radius;
- const z2 = point.z + Math.sin(angle2) * radius;
-
- this.app.drawLine(
- new Vec3(x1, 0.01, z1), // Slightly above ground to avoid z-fighting
- new Vec3(x2, 0.01, z2),
- this.validColor
- );
- }
- }
-}
diff --git a/examples/shoe-configurator.html b/examples/shoe-configurator.html
index 60b9fd1..ee57549 100644
--- a/examples/shoe-configurator.html
+++ b/examples/shoe-configurator.html
@@ -20,7 +20,7 @@
-
+
diff --git a/examples/splat.html b/examples/splat.html
index 0661f02..bcc45c5 100644
--- a/examples/splat.html
+++ b/examples/splat.html
@@ -19,7 +19,7 @@
-
+
diff --git a/examples/text3d.html b/examples/text3d.html
index e0d571b..ff5aa3a 100644
--- a/examples/text3d.html
+++ b/examples/text3d.html
@@ -20,10 +20,10 @@
-
+
-
+
diff --git a/examples/video-texture.html b/examples/video-texture.html
index fdf9cdb..25d6195 100644
--- a/examples/video-texture.html
+++ b/examples/video-texture.html
@@ -24,7 +24,7 @@
-
+
diff --git a/src/app.ts b/src/app.ts
index c3da1b2..214603e 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,4 +1,4 @@
-import { Application, CameraComponent, FILLMODE_FILL_WINDOW, Keyboard, Mouse, Picker, RESOLUTION_AUTO } from 'playcanvas';
+import { Application, CameraComponent, FILLMODE_FILL_WINDOW, GraphNode, Keyboard, Mouse, Picker, RESOLUTION_AUTO } from 'playcanvas';
import { AssetElement } from './asset';
import { AsyncElement } from './async-element';
@@ -209,13 +209,14 @@ class AppElement extends AsyncElement {
// Get the currently hovered entity by walking up the hierarchy
let newHoverEntity = null;
if (selection.length > 0) {
- let node = selection[0].node;
- while (node && !newHoverEntity) {
- const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
+ let currentNode: GraphNode | null = selection[0].node;
+ while (currentNode !== null) {
+ const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
if (entityElement) {
newHoverEntity = entityElement;
+ break;
}
- node = node.parent;
+ currentNode = currentNode.parent;
}
}
@@ -252,14 +253,14 @@ class AppElement extends AsyncElement {
const selection = this._picker.getSelection(x, y);
if (selection.length > 0) {
- let node = selection[0].node;
- while (node) {
- const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
+ let currentNode: GraphNode | null = selection[0].node;
+ while (currentNode !== null) {
+ const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
if (entityElement && entityElement.hasListeners('pointerdown')) {
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
break;
}
- node = node.parent;
+ currentNode = currentNode.parent;
}
}
}