@waica/engine 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/assets/glb-clip-names.d.ts +8 -0
- package/dist/assets/glb-clip-names.js +49 -0
- package/dist/camera-rig.d.ts +22 -1
- package/dist/camera-rig.js +41 -5
- package/dist/game.d.ts +1 -1
- package/dist/game.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/pointer-raycast.d.ts +24 -6
- package/dist/pointer-raycast.js +44 -14
- package/dist/runtime-view-snapshot.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,14 @@ A `Game` draws through three's `WebGPURenderer` (ADR 0025): WebGPU when the brow
|
|
|
25
25
|
- **Tests under happy-dom.** happy-dom has no GPU: a project test that builds a `Game` replaces `WebGPURenderer` from `three/webgpu` with a fake (it previously replaced `WebGLRenderer` from `three`), whose `init()` resolves.
|
|
26
26
|
- **Run Sessions.** A Run Session waits for the Render Backend before it reports ready, steps or screenshots, and its snapshots carry `backend`. A renderer that cannot initialize ends `start_project` with a runtime error that names the failure.
|
|
27
27
|
|
|
28
|
+
## Migrating to 0.28.0: 3D editor
|
|
29
|
+
|
|
30
|
+
The editor edits 3D scenes (issue #173, ADR 0030). Scene files and game code are untouched; three engine seams become public, all additive:
|
|
31
|
+
|
|
32
|
+
- **`raycastPick(camera, at, entities, extra?)`** is exported with its types (`PickTarget`, `PickedTarget`, `RaycastPick`, `ViewportFraction`). It is the raycast the Pointer uses in a 3D scene: every live Model root and the optional `extra` targets (`{ object, entity }`, where `entity` is an `Entity` or `'camera'`, tested whatever their layers), nearest hit first, with the ground plane y = 0 as the fallback point. `Pointer` behaves as before.
|
|
33
|
+
- **`glbClipNames(bytes)`** returns a glb's animation clip names in file order (an unnamed clip is `animation_<index>`, as three names it), `[]` with none, and `null` for bytes that are not a glb or a JSON chunk that does not parse. It reads only the header and the JSON chunk, with no Node or DOM API beyond `TextDecoder`.
|
|
34
|
+
- **`CameraRig.setViewCamera(camera | null)` and `CameraRig.sceneCamera`.** A host can draw a 3D scene through a camera of its own: while one is set, `game.camera` returns it, so the frame, the Pointer and `game.worldToScreen` follow it; `sceneCamera` keeps returning the camera the scene JSON places, and `setSceneCamera` keeps updating it. A 2D scene ignores the call with one warning. A game never sets one; the editor sets it only in edit mode, where its Game does not simulate.
|
|
35
|
+
|
|
28
36
|
## Migrating to 0.27.0: 3D animation
|
|
29
37
|
|
|
30
38
|
A glTF's clips play (issue #166, ADR 0029): a `Model` keeps its `handle`, an `Animator` beside it plays the clips, a `StateMachine` drives either kind of Clip Target through one seam, and a 3D character turns to face its walk. Every 2D scene file loads as it did; the seam is additive, one type changes (`TriggerEnv.clipFinished`) and two behaviours change (`play` restarts a finished clip, a `CharacterMotor` turns by default):
|
|
@@ -269,7 +277,7 @@ A scene declares its space (ADR 0027): `render.space: '2d'` (the default) or `'3
|
|
|
269
277
|
- **Contacts and sensors.** Components on an entity with a solid `Collider` get `onBodyContact({ entity, other, normal, point })` once per step for each entity they touch, the normal pointing from the entity to the other; kinematic bodies included, so a character hears the floor and the walls it touches. A `Collider{ sensor: true }` is the 3D `Hitbox`: it fires `onCollide(other)` every step it overlaps another collider, on each side whose `collidesWith` names the other's `layer` (ADR 0016); a solid collider is told only if it declares its own mask, and a sensor with the default mask (`['*']`) is not told about fixed solid colliders (the level's walls and floors), as a Hitbox never sees a Solid — a mask naming their layer still is.
|
|
270
278
|
- **Queries.** `game.query` gains 3D forms told apart by argument shape: `ray(origin, direction, maxDistance, filter?)` returns the first solid collider (`{ entity, collider, distance, point, normal }`, sensors never block it), `area({ shape: 'box', center, size } | { shape: 'sphere', center, radius }, filter?)` and `point(at, filter?)` return the entities whose sensor colliders intersect or contain them, `nearest(at, filter?)` the closest transform with `z`. The filter object is the 2D one; its `where` runs after Rapier has answered, so it may throw or change the scene, and `area` answers `[]` for a volume that is not a finite box or sphere. A form in the other space's scene throws an error naming the space. Rapier updates what queries read inside the step, so the engine refreshes it when bodies came or went.
|
|
271
279
|
- **Runtime Snapshot.** `space`, `view` (`{ kind: 'orthographic', position, zoom }` or `{ kind: 'perspective', position, target, fov }`) and, in a 3D scene, `lighting.sun[]` and `lighting.pointLights[]`, and `physics: { state: 'loading' | 'ready' | 'failed', gravity, bodies: [{ entity, id, type: 'fixed' | 'dynamic' | 'kinematic', velocity, grounded }] }` (bodies in spawn order, never filtered).
|
|
272
|
-
- **Editor.** A 3D scene
|
|
280
|
+
- **Editor.** A 3D scene is edited through the Editor Camera (ADR 0030): in edit mode the viewport draws, picks and projects through an editor-owned perspective camera set with `game.cameraRig.setViewCamera(camera)` (`game.camera` returns it while set; `game.cameraRig.sceneCamera` keeps the one the JSON places; `null` restores it; a 2D scene ignores it with a warning). It starts at the scene camera's pose, orbits (middle button or Alt+left), pans (right button or Shift+middle), zooms (wheel), steps with the arrows, Shift+arrows, `+` and `-` on the focused viewport, frames the selection with `F`, and is remembered per scene; Play draws through the scene camera. A click picks through `raycastPick` (Models, and the icons of Suns, PointLights, meshless entities and the scene camera); `TransformControls` move, rotate and scale the selection (W, E, R; World or Local; Ctrl snaps to per-user steps), one undo per drag; a prefab drops where the ray meets a Model or the ground; an XZ grid and the helpers (the camera's frustum, each Sun's direction, each PointLight's range, each Collider's shape) draw in edit mode only. The inspector edits X/Y/Z, rotation and scale and a perspective camera's position, target and fov, and the Animator's clip fields list the glb's clips, read by `glbClipNames(bytes)`.
|
|
273
281
|
|
|
274
282
|
## State machines
|
|
275
283
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names of a glb's animation clips, in file order, as three's loader
|
|
3
|
+
* names them: an unnamed clip is `animation_<index>`. Only the header and the
|
|
4
|
+
* JSON chunk are read, so bytes cut off after the JSON chunk still answer.
|
|
5
|
+
* `[]` for a glb with no animations; null for bytes that are not a glb or a
|
|
6
|
+
* JSON chunk that does not parse.
|
|
7
|
+
*/
|
|
8
|
+
export declare function glbClipNames(bytes: Uint8Array): string[] | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The clip names a glb carries, read from its JSON chunk alone (issue #173
|
|
2
|
+
// CA-3): what the editor offers in its clip dropdowns and the MCP validates
|
|
3
|
+
// clip references against, without loading the file. Pure: no Node or DOM API
|
|
4
|
+
// beyond TextDecoder, so it runs in the browser, in Node and in a worker.
|
|
5
|
+
/** The 12-byte glb header (magic, version, total length), then the 8-byte header of the first chunk, which must be JSON. */
|
|
6
|
+
const HEADER_BYTES = 12;
|
|
7
|
+
const CHUNK_HEADER_BYTES = 8;
|
|
8
|
+
/** 'glTF' and 'JSON', read as little-endian uint32. */
|
|
9
|
+
const GLB_MAGIC = 0x46546c67;
|
|
10
|
+
const JSON_CHUNK = 0x4e4f534a;
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/** The glb's JSON document, or null when the bytes are not a glb or its JSON chunk is cut short or does not parse to an object. */
|
|
15
|
+
function glbDocument(bytes) {
|
|
16
|
+
const start = HEADER_BYTES + CHUNK_HEADER_BYTES;
|
|
17
|
+
if (bytes.byteLength < start)
|
|
18
|
+
return null;
|
|
19
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
20
|
+
if (view.getUint32(0, true) !== GLB_MAGIC || view.getUint32(HEADER_BYTES + 4, true) !== JSON_CHUNK)
|
|
21
|
+
return null;
|
|
22
|
+
const length = view.getUint32(HEADER_BYTES, true);
|
|
23
|
+
if (start + length > bytes.byteLength)
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes.subarray(start, start + length)));
|
|
27
|
+
return isRecord(parsed) ? parsed : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The names of a glb's animation clips, in file order, as three's loader
|
|
35
|
+
* names them: an unnamed clip is `animation_<index>`. Only the header and the
|
|
36
|
+
* JSON chunk are read, so bytes cut off after the JSON chunk still answer.
|
|
37
|
+
* `[]` for a glb with no animations; null for bytes that are not a glb or a
|
|
38
|
+
* JSON chunk that does not parse.
|
|
39
|
+
*/
|
|
40
|
+
export function glbClipNames(bytes) {
|
|
41
|
+
const document = glbDocument(bytes);
|
|
42
|
+
if (!document)
|
|
43
|
+
return null;
|
|
44
|
+
const animations = Array.isArray(document.animations) ? document.animations : [];
|
|
45
|
+
return animations.map((animation, index) => {
|
|
46
|
+
const name = isRecord(animation) ? animation.name : undefined;
|
|
47
|
+
return typeof name === 'string' && name ? name : `animation_${index}`;
|
|
48
|
+
});
|
|
49
|
+
}
|
package/dist/camera-rig.d.ts
CHANGED
|
@@ -17,17 +17,38 @@ export interface CameraFollowHost {
|
|
|
17
17
|
/**
|
|
18
18
|
* The Game's cameras. The orthographic one is built with the Game and frames
|
|
19
19
|
* every 2D scene; a 3D scene swaps in a perspective one (built on first use).
|
|
20
|
-
* `
|
|
20
|
+
* `sceneCamera` is whichever the loaded scene uses (ADR 0027); `camera` is
|
|
21
|
+
* that one, or the host's view camera over a 3D scene while one is set.
|
|
21
22
|
*/
|
|
22
23
|
export declare class CameraRig {
|
|
23
24
|
readonly orthographic: THREE.OrthographicCamera;
|
|
24
25
|
private perspective;
|
|
25
26
|
private current;
|
|
27
|
+
/** A host-owned camera drawn through instead of the scene's (the editor's, issue #173). */
|
|
28
|
+
private view;
|
|
29
|
+
/** Set once the 2D-scene warning of setViewCamera was logged. */
|
|
30
|
+
private warnedFlatView;
|
|
26
31
|
private aspect;
|
|
27
32
|
/** How far the perspective camera's declared target was from its declared position. */
|
|
28
33
|
private distance;
|
|
29
34
|
constructor();
|
|
35
|
+
/**
|
|
36
|
+
* The camera the frame draws, the Pointer picks and `worldToScreen`
|
|
37
|
+
* projects through: the view camera over a 3D scene while one is set,
|
|
38
|
+
* else the scene camera.
|
|
39
|
+
*/
|
|
30
40
|
get camera(): GameCamera;
|
|
41
|
+
/** The camera placed from the scene's JSON, whatever view camera is set; `adopt` keeps updating it. */
|
|
42
|
+
get sceneCamera(): GameCamera;
|
|
43
|
+
/**
|
|
44
|
+
* Draws a 3D scene through `camera` instead of the scene camera, until
|
|
45
|
+
* cleared with null (issue #173 CA-1): the editor's own camera, which game
|
|
46
|
+
* code never sees because the editor's Game does not simulate. The rig fits
|
|
47
|
+
* its aspect to the view and lets it see the Emissive layer, as it does the
|
|
48
|
+
* scene camera. Over a 2D scene (or none yet) it has no effect and warns
|
|
49
|
+
* once; the camera is still kept, so it takes over when a 3D scene loads.
|
|
50
|
+
*/
|
|
51
|
+
setViewCamera(camera: THREE.PerspectiveCamera | null): void;
|
|
31
52
|
/** The distance from the perspective camera to the point it was aimed at; a snapshot reports its target from it. */
|
|
32
53
|
get lookDistance(): number;
|
|
33
54
|
/**
|
package/dist/camera-rig.js
CHANGED
|
@@ -5,12 +5,17 @@ import { placePerspectiveCamera, resolvePerspectiveCamera } from './scene-camera
|
|
|
5
5
|
/**
|
|
6
6
|
* The Game's cameras. The orthographic one is built with the Game and frames
|
|
7
7
|
* every 2D scene; a 3D scene swaps in a perspective one (built on first use).
|
|
8
|
-
* `
|
|
8
|
+
* `sceneCamera` is whichever the loaded scene uses (ADR 0027); `camera` is
|
|
9
|
+
* that one, or the host's view camera over a 3D scene while one is set.
|
|
9
10
|
*/
|
|
10
11
|
export class CameraRig {
|
|
11
12
|
orthographic = new THREE.OrthographicCamera();
|
|
12
13
|
perspective = null;
|
|
13
14
|
current = this.orthographic;
|
|
15
|
+
/** A host-owned camera drawn through instead of the scene's (the editor's, issue #173). */
|
|
16
|
+
view = null;
|
|
17
|
+
/** Set once the 2D-scene warning of setViewCamera was logged. */
|
|
18
|
+
warnedFlatView = false;
|
|
14
19
|
aspect = 1;
|
|
15
20
|
/** How far the perspective camera's declared target was from its declared position. */
|
|
16
21
|
distance = 1;
|
|
@@ -19,9 +24,38 @@ export class CameraRig {
|
|
|
19
24
|
// Emissive drawables sit on their own layer; an unlit frame draws it in place.
|
|
20
25
|
this.orthographic.layers.enable(EMISSIVE_LAYER);
|
|
21
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The camera the frame draws, the Pointer picks and `worldToScreen`
|
|
29
|
+
* projects through: the view camera over a 3D scene while one is set,
|
|
30
|
+
* else the scene camera.
|
|
31
|
+
*/
|
|
22
32
|
get camera() {
|
|
33
|
+
return this.view && this.current === this.perspective ? this.view : this.current;
|
|
34
|
+
}
|
|
35
|
+
/** The camera placed from the scene's JSON, whatever view camera is set; `adopt` keeps updating it. */
|
|
36
|
+
get sceneCamera() {
|
|
23
37
|
return this.current;
|
|
24
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Draws a 3D scene through `camera` instead of the scene camera, until
|
|
41
|
+
* cleared with null (issue #173 CA-1): the editor's own camera, which game
|
|
42
|
+
* code never sees because the editor's Game does not simulate. The rig fits
|
|
43
|
+
* its aspect to the view and lets it see the Emissive layer, as it does the
|
|
44
|
+
* scene camera. Over a 2D scene (or none yet) it has no effect and warns
|
|
45
|
+
* once; the camera is still kept, so it takes over when a 3D scene loads.
|
|
46
|
+
*/
|
|
47
|
+
setViewCamera(camera) {
|
|
48
|
+
if (camera && this.current !== this.perspective && !this.warnedFlatView) {
|
|
49
|
+
console.warn('[waica] setViewCamera has no effect over a 2D scene or none: its camera is orthographic');
|
|
50
|
+
this.warnedFlatView = true;
|
|
51
|
+
}
|
|
52
|
+
this.view = camera;
|
|
53
|
+
if (!camera)
|
|
54
|
+
return;
|
|
55
|
+
camera.layers.enable(EMISSIVE_LAYER);
|
|
56
|
+
camera.aspect = this.aspect;
|
|
57
|
+
camera.updateProjectionMatrix();
|
|
58
|
+
}
|
|
25
59
|
/** The distance from the perspective camera to the point it was aimed at; a snapshot reports its target from it. */
|
|
26
60
|
get lookDistance() {
|
|
27
61
|
return this.distance;
|
|
@@ -58,10 +92,12 @@ export class CameraRig {
|
|
|
58
92
|
camera.top = halfH;
|
|
59
93
|
camera.bottom = -halfH;
|
|
60
94
|
camera.updateProjectionMatrix();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
95
|
+
for (const lens of [this.perspective, this.view]) {
|
|
96
|
+
if (!lens)
|
|
97
|
+
continue;
|
|
98
|
+
lens.aspect = aspect;
|
|
99
|
+
lens.updateProjectionMatrix();
|
|
100
|
+
}
|
|
65
101
|
}
|
|
66
102
|
/** One Simulation Step of the orthographic follow camera (deadzone, lookahead, smoothing, limits). */
|
|
67
103
|
follow(cam, host, dt) {
|
package/dist/game.d.ts
CHANGED
|
@@ -255,7 +255,7 @@ export declare class Game {
|
|
|
255
255
|
get projection(): 'isometric' | null;
|
|
256
256
|
/** The live scene's space (`render.space`); `'2d'` with no scene (ADR 0027). */
|
|
257
257
|
get space(): SceneSpace;
|
|
258
|
-
/** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones. */
|
|
258
|
+
/** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones, or a host's view camera over a 3D one (`cameraRig.setViewCamera`). */
|
|
259
259
|
get camera(): GameCamera;
|
|
260
260
|
/**
|
|
261
261
|
* Where a logical point falls on the canvas, in CSS px from its top-left
|
package/dist/game.js
CHANGED
|
@@ -469,7 +469,7 @@ export class Game {
|
|
|
469
469
|
get space() {
|
|
470
470
|
return this.sceneSpace;
|
|
471
471
|
}
|
|
472
|
-
/** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones. */
|
|
472
|
+
/** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones, or a host's view camera over a 3D one (`cameraRig.setViewCamera`). */
|
|
473
473
|
get camera() {
|
|
474
474
|
return this.cameraRig.camera;
|
|
475
475
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { AssetLoader } from './assets/asset-loader.js';
|
|
|
4
4
|
export type { AssetStatus } from './assets/asset-loader.js';
|
|
5
5
|
export type { TextureBackend } from './assets/texture-backend.js';
|
|
6
6
|
export { GltfModelBackend } from './assets/model-backend.js';
|
|
7
|
+
export { glbClipNames } from './assets/glb-clip-names.js';
|
|
7
8
|
export type { LoadedModel, ModelBackend } from './assets/model-backend.js';
|
|
8
9
|
export type { ModelHandle, ModelOutcome } from './assets/model-cache.js';
|
|
9
10
|
export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
@@ -49,6 +50,8 @@ export type { ActionName, InjectedActionOperation, InputBindings, InputOptions }
|
|
|
49
50
|
export { ACTION_HELD_THRESHOLD, firstStandardPadValues, GAMEPAD_CODE_PREFIX, gamepadControl, } from './gamepad.js';
|
|
50
51
|
export type { GamepadControl } from './gamepad.js';
|
|
51
52
|
export { Pointer } from './pointer.js';
|
|
53
|
+
export { raycastPick } from './pointer-raycast.js';
|
|
54
|
+
export type { PickedTarget, PickTarget, RaycastPick, ViewportFraction } from './pointer-raycast.js';
|
|
52
55
|
export type { PointerCamera, PointerDeps, PointerPick, PointerResolution } from './pointer.js';
|
|
53
56
|
export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
54
57
|
export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeBridgeFailure, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
2
|
export { AssetLoader } from './assets/asset-loader.js';
|
|
3
3
|
export { GltfModelBackend } from './assets/model-backend.js';
|
|
4
|
+
export { glbClipNames } from './assets/glb-clip-names.js';
|
|
4
5
|
export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
5
6
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
6
7
|
export { isClipTarget } from './animation/clip-target.js';
|
|
@@ -23,6 +24,7 @@ export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
|
23
24
|
export { Input, DEFAULT_BINDINGS } from './input.js';
|
|
24
25
|
export { ACTION_HELD_THRESHOLD, firstStandardPadValues, GAMEPAD_CODE_PREFIX, gamepadControl, } from './gamepad.js';
|
|
25
26
|
export { Pointer } from './pointer.js';
|
|
27
|
+
export { raycastPick } from './pointer-raycast.js';
|
|
26
28
|
export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
27
29
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
28
30
|
export { Stats } from './stats.js';
|
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import * as THREE from 'three/webgpu';
|
|
2
2
|
import type { Entity } from './entity.js';
|
|
3
|
-
import type { PointerPick } from './pointer.js';
|
|
4
3
|
/** A position inside the letterboxed game viewport, as fractions of its width and height. */
|
|
5
4
|
export interface ViewportFraction {
|
|
6
5
|
nx: number;
|
|
7
6
|
ny: number;
|
|
8
7
|
}
|
|
8
|
+
/** What an extra target picks: an entity (an editor's icon for it), or the scene camera's icon. */
|
|
9
|
+
export type PickedTarget = Entity | 'camera';
|
|
10
|
+
/** An object a ray is also tested against, and what a hit on it picks (issue #173 CA-2). */
|
|
11
|
+
export interface PickTarget<Picked extends PickedTarget = PickedTarget> {
|
|
12
|
+
readonly object: THREE.Object3D;
|
|
13
|
+
readonly entity: Picked;
|
|
14
|
+
}
|
|
15
|
+
/** A 3D pick: the hit's world point, and the entity (or extra target) hit, or null on the ground. */
|
|
16
|
+
export interface RaycastPick<Picked extends PickedTarget = Entity> {
|
|
17
|
+
readonly point: {
|
|
18
|
+
readonly x: number;
|
|
19
|
+
readonly y: number;
|
|
20
|
+
readonly z: number;
|
|
21
|
+
};
|
|
22
|
+
readonly entity: Entity | Picked | null;
|
|
23
|
+
}
|
|
9
24
|
/**
|
|
10
25
|
* The pick for a click on a 3D scene (issue #154 CA-12): a ray through the
|
|
11
26
|
* letterboxed point, intersected with the roots of every live Model
|
|
12
|
-
* (recursively, so a glTF's meshes count
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
27
|
+
* (recursively, so a glTF's meshes count, posed where its skeleton is, and a
|
|
28
|
+
* glTF still loading does not) and with the `extra` targets a host adds (the
|
|
29
|
+
* editor's icons, issue #173 CA-2; one that is hidden, or whose entity is
|
|
30
|
+
* dead, is skipped). The nearest hit wins, with its
|
|
31
|
+
* world-space point. With no hit the pick has no entity and the point where
|
|
32
|
+
* the ray meets the ground plane y = 0; a ray that never meets it (it looks
|
|
33
|
+
* at the sky) yields no pick.
|
|
16
34
|
*/
|
|
17
|
-
export declare function raycastPick(camera: THREE.PerspectiveCamera, at: ViewportFraction, entities: readonly Entity[]):
|
|
35
|
+
export declare function raycastPick<Picked extends PickedTarget = Entity>(camera: THREE.PerspectiveCamera, at: ViewportFraction, entities: readonly Entity[], extra?: readonly PickTarget<Picked>[]): RaycastPick<Picked> | null;
|
package/dist/pointer-raycast.js
CHANGED
|
@@ -16,33 +16,63 @@ function modelRoots(entities) {
|
|
|
16
16
|
}
|
|
17
17
|
return roots;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
function ownerOf(object,
|
|
19
|
+
/** What `object` picks: the target or Model root it hangs under, nearest first. */
|
|
20
|
+
function ownerOf(object, owners) {
|
|
21
21
|
for (let node = object; node; node = node.parent) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
return
|
|
22
|
+
const owner = owners.get(node);
|
|
23
|
+
if (owner)
|
|
24
|
+
return owner;
|
|
25
25
|
}
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
+
/** Whether `target` is drawn and still picks something: itself and every ancestor visible, and its entity alive. */
|
|
29
|
+
function isPickable(target) {
|
|
30
|
+
if (target.entity !== 'camera' && !target.entity.alive)
|
|
31
|
+
return false;
|
|
32
|
+
for (let node = target.object; node; node = node.parent) {
|
|
33
|
+
if (!node.visible)
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/** The extra targets' hits: tested whatever their layers, since an editor overlay lives on the Emissive layer alone. */
|
|
39
|
+
function extraHits(targets) {
|
|
40
|
+
if (targets.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const own = raycaster.layers.mask;
|
|
43
|
+
raycaster.layers.enableAll();
|
|
44
|
+
try {
|
|
45
|
+
return raycaster.intersectObjects(targets.filter(isPickable).map((target) => target.object), true);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
raycaster.layers.mask = own;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
28
51
|
/**
|
|
29
52
|
* The pick for a click on a 3D scene (issue #154 CA-12): a ray through the
|
|
30
53
|
* letterboxed point, intersected with the roots of every live Model
|
|
31
|
-
* (recursively, so a glTF's meshes count
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
54
|
+
* (recursively, so a glTF's meshes count, posed where its skeleton is, and a
|
|
55
|
+
* glTF still loading does not) and with the `extra` targets a host adds (the
|
|
56
|
+
* editor's icons, issue #173 CA-2; one that is hidden, or whose entity is
|
|
57
|
+
* dead, is skipped). The nearest hit wins, with its
|
|
58
|
+
* world-space point. With no hit the pick has no entity and the point where
|
|
59
|
+
* the ray meets the ground plane y = 0; a ray that never meets it (it looks
|
|
60
|
+
* at the sky) yields no pick.
|
|
35
61
|
*/
|
|
36
|
-
|
|
62
|
+
// eslint-disable-next-line max-params -- the signature spec #173 fixes: the Pointer's three inputs plus the optional extra targets; bundling them would be an unrelated bag
|
|
63
|
+
export function raycastPick(camera, at, entities, extra = []) {
|
|
37
64
|
camera.updateMatrixWorld();
|
|
38
65
|
raycaster.setFromCamera(new THREE.Vector2(at.nx * 2 - 1, 1 - at.ny * 2), camera);
|
|
39
66
|
const roots = modelRoots(entities);
|
|
67
|
+
const owners = new Map(roots);
|
|
68
|
+
for (const target of extra)
|
|
69
|
+
owners.set(target.object, target.entity);
|
|
40
70
|
// A click can land before the first frame has placed anything.
|
|
41
|
-
for (const
|
|
42
|
-
|
|
43
|
-
const [hit] = raycaster.intersectObjects([...roots.keys()], true);
|
|
71
|
+
for (const object of owners.keys())
|
|
72
|
+
object.updateWorldMatrix(true, true);
|
|
73
|
+
const [hit] = [...raycaster.intersectObjects([...roots.keys()], true), ...extraHits(extra)].sort((a, b) => a.distance - b.distance);
|
|
44
74
|
if (hit)
|
|
45
|
-
return { entity: ownerOf(hit.object,
|
|
75
|
+
return { entity: ownerOf(hit.object, owners), point: { x: hit.point.x, y: hit.point.y, z: hit.point.z } };
|
|
46
76
|
const ground = raycaster.ray.intersectPlane(GROUND, new THREE.Vector3());
|
|
47
77
|
return ground ? { entity: null, point: { x: ground.x, y: ground.y, z: ground.z } } : null;
|
|
48
78
|
}
|
|
@@ -6,7 +6,8 @@ function vector(value) {
|
|
|
6
6
|
return [clean(value.x), clean(value.y), clean(value.z)];
|
|
7
7
|
}
|
|
8
8
|
export function viewSnapshot(game) {
|
|
9
|
-
|
|
9
|
+
// The scene's own camera: a host's view camera is not part of the simulated game, and `lookDistance` is the scene camera's.
|
|
10
|
+
const camera = game.cameraRig.sceneCamera;
|
|
10
11
|
if (!isPerspectiveCamera(camera)) {
|
|
11
12
|
return { kind: 'orthographic', position: [clean(camera.position.x), clean(camera.position.y)], zoom: game.view };
|
|
12
13
|
}
|