@aethervtt/shard-controls 0.0.0-stage → 0.1.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/dist/drag.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { type Entity, type Vec3, type World } from '@aethervtt/shard-core';
2
+ import { type GestureEvent } from '@aethervtt/shard-input';
3
+ export interface DragMovedEvent {
4
+ entity: Entity;
5
+ /** World position now. */
6
+ position: Vec3;
7
+ /** Movement since the previous DragMoved (or the start). */
8
+ delta: Vec3;
9
+ }
10
+ export interface DragEndedEvent {
11
+ entity: Entity;
12
+ /** Where it ended: the start again when cancelled. */
13
+ position: Vec3;
14
+ start: Vec3;
15
+ cancelled: boolean;
16
+ }
17
+ export declare const DragMoved: import("@aethervtt/shard-core").EventDef<DragMovedEvent>;
18
+ export declare const DragEnded: import("@aethervtt/shard-core").EventDef<DragEndedEvent>;
19
+ export interface PlaneDragOptions {
20
+ entity: Entity;
21
+ /** The drag: its `drag-start` event (or that event's id). */
22
+ gesture: GestureEvent | number;
23
+ /**
24
+ * The plane the pointer moves on: 'grab' (default), through the grab point (or the entity, with
25
+ * no `grab`); or a height.
26
+ */
27
+ plane?: 'grab' | {
28
+ y: number;
29
+ };
30
+ /** The point the pointer took hold of, e.g. `PickHit.position`. Default: the start pixel's ray on the plane. */
31
+ grab?: ArrayLike<number>;
32
+ /** Adjusts each position in place, e.g. to a grid cell's center. */
33
+ snap?: (position: Vec3) => void;
34
+ /** The camera the pointer looks through. Default: the active camera drawn first. */
35
+ camera?: Entity;
36
+ }
37
+ /**
38
+ * Drags one entity at a time along a horizontal plane (0060). `begin` it from the host's own
39
+ * `drag-start` handler once `pick` says what's under the pointer and the host decides it moves.
40
+ * Cameras it projects through must be unparented (as controls' cameras are), and a dragged
41
+ * entity's parents may only translate it.
42
+ */
43
+ export declare class PlaneDragState {
44
+ /** @internal */ world: World | undefined;
45
+ /** The dragged entity, while a drag runs. */
46
+ entity: Entity | undefined;
47
+ /** @internal */ gesture: number;
48
+ /** @internal */ camera: Entity;
49
+ /** @internal */ planeY: number;
50
+ /** @internal */ snap: ((position: Vec3) => void) | undefined;
51
+ /** @internal World-space offset from the pointer's floor point to the entity. */
52
+ readonly offset: Float64Array<ArrayBuffer>;
53
+ /** @internal */ readonly startLocal: Float64Array<ArrayBuffer>;
54
+ /** @internal */ readonly startWorld: Float64Array<ArrayBuffer>;
55
+ /** @internal */ readonly position: Float64Array<ArrayBuffer>;
56
+ get active(): boolean;
57
+ /**
58
+ * Starts dragging `entity` with a drag gesture, taking the gesture from whoever had it (a camera
59
+ * control panning on it while the host picked). False when the drag already ended, or there's no
60
+ * camera to project through.
61
+ */
62
+ begin(options: PlaneDragOptions): boolean;
63
+ /** Ends the drag as cancelled: the entity goes back to where it started. */
64
+ cancel(): void;
65
+ /** The drag as data. For agents. */
66
+ describe(): {
67
+ active: boolean;
68
+ entity?: undefined;
69
+ gesture?: undefined;
70
+ camera?: undefined;
71
+ planeY?: undefined;
72
+ start?: undefined;
73
+ position?: undefined;
74
+ } | {
75
+ active: boolean;
76
+ entity: number;
77
+ gesture: number;
78
+ camera: number;
79
+ planeY: number;
80
+ start: number[];
81
+ position: number[];
82
+ };
83
+ /** @internal Moves the entity under CSS pixel (x, y). */
84
+ moveTo(x: number, y: number): void;
85
+ /** @internal */
86
+ finish(cancelled: boolean): void;
87
+ private place;
88
+ }
89
+ export declare const PlaneDrag: import("@aethervtt/shard-core").ResourceDef<PlaneDragState>;
90
+ /** Follows the drag's gesture: moves on `drag`, ends on `drag-end` (restoring when cancelled). */
91
+ export declare const updatePlaneDrag: import("@aethervtt/shard-core").SystemDef<import("@aethervtt/shard-core").EventReader<GestureEvent>>;
package/dist/drag.js ADDED
@@ -0,0 +1,230 @@
1
+ import {
2
+ ChildOf,
3
+ defineEvent,
4
+ defineResource,
5
+ defineSystem,
6
+ ShardError
7
+ } from "@aethervtt/shard-core";
8
+ import { Gesture, Gestures } from "@aethervtt/shard-input";
9
+ import { Camera3d } from "@aethervtt/shard-render";
10
+ import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
11
+ import { viewportOf } from "./controls.js";
12
+ import { createView, poseView, viewToPlane } from "./view.js";
13
+ const DragMoved = defineEvent("controls/DragMoved", {
14
+ description: "A PlaneDrag moved its entity: a preview, not a commit."
15
+ });
16
+ const DragEnded = defineEvent("controls/DragEnded", {
17
+ description: "A PlaneDrag ended: commit `position` unless `cancelled` (the entity is back at `start`)."
18
+ });
19
+ const OWNER = "controls/plane-drag";
20
+ const view = createView();
21
+ const hit = new Float64Array(3);
22
+ const pointer = new Float64Array(2);
23
+ const viewport = [1, 1];
24
+ function defaultCamera(world) {
25
+ let best;
26
+ let bestOrder = Number.POSITIVE_INFINITY;
27
+ for (const table of world.query({ with: [Camera3d, Transform] }).tables) {
28
+ const active = table.column(Camera3d, "active");
29
+ const order = table.column(Camera3d, "order");
30
+ for (let i = 0; i < table.count; i++) {
31
+ if (active[i] === 0 || order[i] >= bestOrder) continue;
32
+ best = table.entities[i];
33
+ bestOrder = order[i];
34
+ }
35
+ }
36
+ return best;
37
+ }
38
+ function pointOnPlane(world, camera, x, y, planeY, out) {
39
+ const table = world.entityTable(camera);
40
+ const row = world.entityRow(camera);
41
+ viewportOf(world, camera, viewport);
42
+ const t = table.column(Transform, "translation");
43
+ const r = table.column(Transform, "rotation");
44
+ poseView(
45
+ view,
46
+ t.subarray(row * 3, row * 3 + 3),
47
+ r.subarray(row * 4, row * 4 + 4),
48
+ table.column(Camera3d, "projection")[row] !== 0,
49
+ table.column(Camera3d, "fovY")[row],
50
+ table.column(Camera3d, "orthoHeight")[row],
51
+ viewport[0] / viewport[1]
52
+ );
53
+ return viewToPlane(view, x, y, viewport[0], viewport[1], planeY, out);
54
+ }
55
+ class PlaneDragState {
56
+ /** @internal */
57
+ world = void 0;
58
+ /** The dragged entity, while a drag runs. */
59
+ entity = void 0;
60
+ /** @internal */
61
+ gesture = 0;
62
+ /** @internal */
63
+ camera = 0;
64
+ /** @internal */
65
+ planeY = 0;
66
+ /** @internal */
67
+ snap = void 0;
68
+ /** @internal World-space offset from the pointer's floor point to the entity. */
69
+ offset = new Float64Array(3);
70
+ /** @internal */
71
+ startLocal = new Float64Array(3);
72
+ /** @internal */
73
+ startWorld = new Float64Array(3);
74
+ /** @internal */
75
+ position = new Float64Array(3);
76
+ get active() {
77
+ return this.entity !== void 0;
78
+ }
79
+ /**
80
+ * Starts dragging `entity` with a drag gesture, taking the gesture from whoever had it (a camera
81
+ * control panning on it while the host picked). False when the drag already ended, or there's no
82
+ * camera to project through.
83
+ */
84
+ begin(options) {
85
+ const world = this.world;
86
+ if (!world)
87
+ throw new ShardError("controls/no-plugin", "PlaneDrag needs controlsPlugin", {
88
+ hint: 'Add "controls" to plugins in shard.json (or app.addPlugin(controlsPlugin)).'
89
+ });
90
+ const id = typeof options.gesture === "number" ? options.gesture : options.gesture.id;
91
+ const gestures = world.resource(Gestures);
92
+ if (!gestures.live(id)) return false;
93
+ if (!world.isAlive(options.entity) || !world.has(options.entity, Transform)) {
94
+ throw new ShardError("controls/not-draggable", `Entity ${options.entity} has no Transform`, {
95
+ hint: "PlaneDrag moves an entity by its Transform."
96
+ });
97
+ }
98
+ const camera = options.camera ?? defaultCamera(world);
99
+ if (camera === void 0) return false;
100
+ if (this.entity !== void 0) this.finish(true);
101
+ gestures.take(id, OWNER);
102
+ const e = options.entity;
103
+ const local = world.get(e, Transform).translation;
104
+ this.startLocal.set(local);
105
+ const g = world.has(e, ChildOf) ? world.tryGet(e, GlobalTransform)?.matrix : void 0;
106
+ this.startWorld[0] = g ? g[3] : local[0];
107
+ this.startWorld[1] = g ? g[7] : local[1];
108
+ this.startWorld[2] = g ? g[11] : local[2];
109
+ const plane = options.plane ?? "grab";
110
+ this.planeY = plane === "grab" ? options.grab?.[1] ?? this.startWorld[1] : plane.y;
111
+ let gx = this.startWorld[0];
112
+ let gz = this.startWorld[2];
113
+ if (options.grab) {
114
+ gx = options.grab[0];
115
+ gz = options.grab[2];
116
+ } else {
117
+ const start = typeof options.gesture === "number" ? void 0 : options.gesture;
118
+ if (start && pointOnPlane(world, camera, start.startX, start.startY, this.planeY, hit)) {
119
+ gx = hit[0];
120
+ gz = hit[2];
121
+ }
122
+ }
123
+ this.offset[0] = this.startWorld[0] - gx;
124
+ this.offset[1] = 0;
125
+ this.offset[2] = this.startWorld[2] - gz;
126
+ this.position.set(this.startWorld);
127
+ this.entity = e;
128
+ this.gesture = id;
129
+ this.camera = camera;
130
+ this.snap = options.snap;
131
+ if (gestures.position(id, pointer)) this.moveTo(pointer[0], pointer[1]);
132
+ return true;
133
+ }
134
+ /** Ends the drag as cancelled: the entity goes back to where it started. */
135
+ cancel() {
136
+ if (this.entity !== void 0) this.finish(true);
137
+ }
138
+ /** The drag as data. For agents. */
139
+ describe() {
140
+ return this.entity === void 0 ? { active: false } : {
141
+ active: true,
142
+ entity: this.entity,
143
+ gesture: this.gesture,
144
+ camera: this.camera,
145
+ planeY: this.planeY,
146
+ start: Array.from(this.startWorld),
147
+ position: Array.from(this.position)
148
+ };
149
+ }
150
+ /** @internal Moves the entity under CSS pixel (x, y). */
151
+ moveTo(x, y) {
152
+ const world = this.world;
153
+ const e = this.entity;
154
+ if (!world.isAlive(this.camera)) return;
155
+ if (!pointOnPlane(world, this.camera, x, y, this.planeY, hit)) return;
156
+ const p = [hit[0] + this.offset[0], this.startWorld[1], hit[2] + this.offset[2]];
157
+ this.snap?.(p);
158
+ const delta = [
159
+ p[0] - this.position[0],
160
+ p[1] - this.position[1],
161
+ p[2] - this.position[2]
162
+ ];
163
+ if (delta[0] === 0 && delta[1] === 0 && delta[2] === 0) return;
164
+ this.position.set(p);
165
+ this.place(p);
166
+ world.send(DragMoved, { entity: e, position: p, delta });
167
+ }
168
+ /** @internal */
169
+ finish(cancelled) {
170
+ const world = this.world;
171
+ const e = this.entity;
172
+ this.entity = void 0;
173
+ const start = [this.startWorld[0], this.startWorld[1], this.startWorld[2]];
174
+ if (!world.isAlive(e)) return;
175
+ if (cancelled) {
176
+ this.position.set(start);
177
+ world.set(e, Transform, {
178
+ translation: [this.startLocal[0], this.startLocal[1], this.startLocal[2]]
179
+ });
180
+ }
181
+ world.resource(Gestures).release(this.gesture, OWNER);
182
+ world.send(DragEnded, {
183
+ entity: e,
184
+ position: [this.position[0], this.position[1], this.position[2]],
185
+ start,
186
+ cancelled
187
+ });
188
+ }
189
+ place(p) {
190
+ this.world.set(this.entity, Transform, {
191
+ translation: [
192
+ this.startLocal[0] + p[0] - this.startWorld[0],
193
+ this.startLocal[1] + p[1] - this.startWorld[1],
194
+ this.startLocal[2] + p[2] - this.startWorld[2]
195
+ ]
196
+ });
197
+ }
198
+ }
199
+ const PlaneDrag = defineResource("controls/PlaneDrag", {
200
+ description: "Drags one entity along a horizontal plane under the pointer (0060); see begin(). Emits DragMoved and DragEnded."
201
+ });
202
+ const updatePlaneDrag = defineSystem({
203
+ name: "controls/plane-drag",
204
+ description: "Moves the entity a PlaneDrag holds with its drag gesture, and ends the drag.",
205
+ setup: (world) => world.reader(Gesture),
206
+ run: (reader, world) => {
207
+ const events = reader.read();
208
+ const drag = world.resource(PlaneDrag);
209
+ if (drag.entity === void 0) return;
210
+ if (!world.isAlive(drag.entity)) {
211
+ drag.finish(true);
212
+ return;
213
+ }
214
+ for (let i = 0; i < events.length && drag.entity !== void 0; i++) {
215
+ const e = events[i];
216
+ if (e.id !== drag.gesture) continue;
217
+ if (e.kind === "drag-start" || e.kind === "drag") drag.moveTo(e.x, e.y);
218
+ else if (e.kind === "drag-end") drag.finish(e.cancelled);
219
+ }
220
+ const gestures = world.resource(Gestures);
221
+ if (drag.entity !== void 0 && gestures.owner(drag.gesture) !== OWNER) drag.finish(true);
222
+ }
223
+ });
224
+ export {
225
+ DragEnded,
226
+ DragMoved,
227
+ PlaneDrag,
228
+ PlaneDragState,
229
+ updatePlaneDrag
230
+ };
@@ -0,0 +1,6 @@
1
+ export { ControlsSettings, type ControlsSettingsValue, MapControls, type MapControlsValue, OrbitControls, type OrbitControlsValue, } from './components.js';
2
+ export { type ControlLive, ControlsState, type ControlsStateValue, snapControls, updateControls, viewportOf, } from './controls.js';
3
+ export { DragEnded, type DragEndedEvent, DragMoved, type DragMovedEvent, PlaneDrag, type PlaneDragOptions, PlaneDragState, updatePlaneDrag, } from './drag.js';
4
+ export { controlsMethods, controlsPlugin, describeControls } from './plugin.js';
5
+ export { syncViews } from './sync.js';
6
+ export { createView, mapView, orbitDirection, orbitView, poseView, type ViewBasis, viewProject, viewRay, viewToPlane, } from './view.js';
package/dist/index.js ADDED
@@ -0,0 +1,56 @@
1
+ import {
2
+ ControlsSettings,
3
+ MapControls,
4
+ OrbitControls
5
+ } from "./components.js";
6
+ import {
7
+ ControlsState,
8
+ snapControls,
9
+ updateControls,
10
+ viewportOf
11
+ } from "./controls.js";
12
+ import {
13
+ DragEnded,
14
+ DragMoved,
15
+ PlaneDrag,
16
+ PlaneDragState,
17
+ updatePlaneDrag
18
+ } from "./drag.js";
19
+ import { controlsMethods, controlsPlugin, describeControls } from "./plugin.js";
20
+ import { syncViews } from "./sync.js";
21
+ import {
22
+ createView,
23
+ mapView,
24
+ orbitDirection,
25
+ orbitView,
26
+ poseView,
27
+ viewProject,
28
+ viewRay,
29
+ viewToPlane
30
+ } from "./view.js";
31
+ export {
32
+ ControlsSettings,
33
+ ControlsState,
34
+ DragEnded,
35
+ DragMoved,
36
+ MapControls,
37
+ OrbitControls,
38
+ PlaneDrag,
39
+ PlaneDragState,
40
+ controlsMethods,
41
+ controlsPlugin,
42
+ createView,
43
+ describeControls,
44
+ mapView,
45
+ orbitDirection,
46
+ orbitView,
47
+ poseView,
48
+ snapControls,
49
+ syncViews,
50
+ updateControls,
51
+ updatePlaneDrag,
52
+ viewProject,
53
+ viewRay,
54
+ viewToPlane,
55
+ viewportOf
56
+ };
@@ -0,0 +1,85 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import { type AppMethod } from '@aethervtt/shard-runtime';
3
+ /** Every control's camera, fields, eased state and whether it moves, and the PlaneDrag. For agents. */
4
+ export declare function describeControls(world: World): {
5
+ controls: {
6
+ camera: number;
7
+ kind: "map" | "orbit";
8
+ active: boolean;
9
+ moving: boolean;
10
+ dragging: string | null;
11
+ fields: {
12
+ distance: number;
13
+ yaw: number;
14
+ pitch: number;
15
+ minPitch: number;
16
+ maxPitch: number;
17
+ minDistance: number;
18
+ maxDistance: number;
19
+ orbitButton: "left" | "middle" | "right";
20
+ panButton: "left" | "middle" | "right";
21
+ leftDrag: "none" | "orbit" | "pan";
22
+ rotateSpeed: number;
23
+ zoomSpeed: number;
24
+ autoRotate: number;
25
+ smoothing: number;
26
+ enabled: boolean;
27
+ target: number[];
28
+ } | {
29
+ zoom: number;
30
+ minZoom: number;
31
+ maxZoom: number;
32
+ height: number;
33
+ elevation: number;
34
+ pitch: number;
35
+ plane: "xy" | "xz";
36
+ bounded: boolean;
37
+ boundsMin: import("@aethervtt/shard-core").Vec2;
38
+ boundsMax: import("@aethervtt/shard-core").Vec2;
39
+ panButton: "left" | "middle" | "right";
40
+ leftDrag: "none" | "pan";
41
+ zoomSpeed: number;
42
+ smoothing: number;
43
+ enabled: boolean;
44
+ target: number[];
45
+ } | null;
46
+ current: {
47
+ target: number[];
48
+ distance: number;
49
+ yaw: number;
50
+ pitch: number;
51
+ zoom?: undefined;
52
+ } | {
53
+ distance?: undefined;
54
+ yaw?: undefined;
55
+ pitch?: undefined;
56
+ target: number[];
57
+ zoom: number;
58
+ };
59
+ }[];
60
+ drag: {
61
+ active: boolean;
62
+ entity?: undefined;
63
+ gesture?: undefined;
64
+ camera?: undefined;
65
+ planeY?: undefined;
66
+ start?: undefined;
67
+ position?: undefined;
68
+ } | {
69
+ active: boolean;
70
+ entity: number;
71
+ gesture: number;
72
+ camera: number;
73
+ planeY: number;
74
+ start: number[];
75
+ position: number[];
76
+ };
77
+ settings: import("./components.js").ControlsSettingsValue;
78
+ };
79
+ export declare const controlsMethods: AppMethod[];
80
+ /**
81
+ * Camera controls and object drag (0060): `OrbitControls`, `MapControls`, `PlaneDrag`, and the
82
+ * `controls.describe` method. Needs `gesturesPlugin` (and `inputPlugin` with a live source for
83
+ * real input), `TransformPlugin` and the render components.
84
+ */
85
+ export declare const controlsPlugin: import("@aethervtt/shard-runtime").Plugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,72 @@
1
+ import { defineSchema, PostUpdate } from "@aethervtt/shard-core";
2
+ import { definePlugin } from "@aethervtt/shard-runtime";
3
+ import { TransformSystems } from "@aethervtt/shard-transform";
4
+ import { ControlsSettings, MapControls, OrbitControls } from "./components.js";
5
+ import { ControlsState, updateControls } from "./controls.js";
6
+ import { DragEnded, DragMoved, PlaneDrag, PlaneDragState, updatePlaneDrag } from "./drag.js";
7
+ function describeControls(world) {
8
+ const state = world.resource(ControlsState);
9
+ const controls = [...state.live.values()].map((live) => {
10
+ const fields = live.kind === "orbit" ? world.tryGet(live.entity, OrbitControls) : world.tryGet(live.entity, MapControls);
11
+ return {
12
+ camera: live.entity,
13
+ kind: live.kind,
14
+ active: live.active,
15
+ moving: live.moving,
16
+ dragging: live.dragId !== 0 ? live.dragMode : live.twoId !== 0 ? "touch" : null,
17
+ fields: fields ? { ...fields, target: Array.from(fields.target) } : null,
18
+ current: live.kind === "orbit" ? {
19
+ target: Array.from(live.target),
20
+ distance: live.scale,
21
+ yaw: live.yaw,
22
+ pitch: live.pitch
23
+ } : { target: Array.from(live.target), zoom: live.scale }
24
+ };
25
+ });
26
+ return {
27
+ controls,
28
+ drag: world.resource(PlaneDrag).describe(),
29
+ settings: world.resource(ControlsSettings)
30
+ };
31
+ }
32
+ const controlsMethods = [
33
+ {
34
+ name: "controls.describe",
35
+ description: "Camera controls (0060): each control's camera, kind (orbit or map), whether it's active and moving, what it drags, its fields and its eased state, plus the PlaneDrag.",
36
+ params: defineSchema("controls/DescribeParams", {}),
37
+ handler: ({ world }) => JSON.parse(JSON.stringify(describeControls(world)))
38
+ }
39
+ ];
40
+ const controlsPlugin = definePlugin({
41
+ name: "controls",
42
+ dependencies: ["input/gestures", "core/transform"],
43
+ provides: [
44
+ OrbitControls,
45
+ MapControls,
46
+ ControlsSettings,
47
+ ControlsState,
48
+ PlaneDrag,
49
+ DragMoved,
50
+ DragEnded,
51
+ updateControls,
52
+ updatePlaneDrag
53
+ ],
54
+ build(app) {
55
+ app.world.initResource(ControlsSettings);
56
+ app.world.initResource(ControlsState);
57
+ const drag = new PlaneDragState();
58
+ drag.world = app.world;
59
+ app.insertResource(PlaneDrag, drag);
60
+ app.addSystems(
61
+ PostUpdate,
62
+ updateControls.before(TransformSystems),
63
+ updatePlaneDrag.after(updateControls).before(TransformSystems)
64
+ );
65
+ app.addMethod(...controlsMethods);
66
+ }
67
+ });
68
+ export {
69
+ controlsMethods,
70
+ controlsPlugin,
71
+ describeControls
72
+ };
package/dist/sync.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ /**
3
+ * Puts camera `to` over the same target as `from`, showing about the same floor area (0060): the
4
+ * orthographic height against the perspective distance × tan(fov/2) × 2. Between two orbit
5
+ * controls the yaw and pitch carry over too. `to` jumps there (no easing).
6
+ *
7
+ * The host calls it when switching views; it never runs by itself, so switching one reader's view
8
+ * can't change another's.
9
+ */
10
+ export declare function syncViews(world: World, from: Entity, to: Entity): void;
package/dist/sync.js ADDED
@@ -0,0 +1,46 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { Camera3d } from "@aethervtt/shard-render";
3
+ import { MapControls, OrbitControls } from "./components.js";
4
+ import { snapControls } from "./controls.js";
5
+ const DEG = Math.PI / 180;
6
+ function visibleHeight(world, entity) {
7
+ const map = world.tryGet(entity, MapControls);
8
+ if (map) return map.height / map.zoom;
9
+ const orbit = world.tryGet(entity, OrbitControls);
10
+ if (orbit) return 2 * orbit.distance * Math.tan(world.get(entity, Camera3d).fovY * DEG / 2);
11
+ throw new ShardError(
12
+ "controls/no-controls",
13
+ `Entity ${entity} has no MapControls or OrbitControls`,
14
+ {
15
+ hint: "syncViews takes two cameras that each have a control."
16
+ }
17
+ );
18
+ }
19
+ function syncViews(world, from, to) {
20
+ const height = visibleHeight(world, from);
21
+ const source = world.tryGet(from, MapControls) ?? world.get(from, OrbitControls);
22
+ const target = [source.target[0], source.target[1], source.target[2]];
23
+ const map = world.tryGet(to, MapControls);
24
+ if (map) {
25
+ const zoom = Math.min(Math.max(map.height / height, map.minZoom), map.maxZoom);
26
+ world.set(to, MapControls, { target, zoom });
27
+ } else {
28
+ const orbit = world.tryGet(to, OrbitControls);
29
+ if (!orbit) visibleHeight(world, to);
30
+ const fov = world.get(to, Camera3d).fovY * DEG;
31
+ const distance = Math.min(
32
+ Math.max(height / (2 * Math.tan(fov / 2)), orbit.minDistance),
33
+ orbit.maxDistance
34
+ );
35
+ const fromOrbit = world.tryGet(from, OrbitControls);
36
+ world.set(to, OrbitControls, {
37
+ target,
38
+ distance,
39
+ ...fromOrbit ? { yaw: fromOrbit.yaw, pitch: fromOrbit.pitch } : {}
40
+ });
41
+ }
42
+ snapControls(world, to);
43
+ }
44
+ export {
45
+ syncViews
46
+ };
package/dist/view.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ type Vec = {
2
+ [index: number]: number;
3
+ };
4
+ export interface ViewBasis {
5
+ eye: Float64Array;
6
+ /** Screen right, screen up and the look direction, unit and orthogonal. */
7
+ right: Float64Array;
8
+ up: Float64Array;
9
+ forward: Float64Array;
10
+ orthographic: boolean;
11
+ /** Perspective: tan(fovY / 2). Orthographic: half the view height. */
12
+ half: number;
13
+ /** Width over height. */
14
+ aspect: number;
15
+ }
16
+ export declare function createView(): ViewBasis;
17
+ /** The orbit camera's eye offset from the target, unit length. */
18
+ export declare function orbitDirection(yaw: number, pitch: number, out: Vec): void;
19
+ /** A perspective camera `distance` from `target`, looking at it from (yaw, pitch), Y up. */
20
+ export declare function orbitView(out: ViewBasis, target: ArrayLike<number>, distance: number, yaw: number, pitch: number, fovY: number, aspect: number): ViewBasis;
21
+ /**
22
+ * An orthographic camera `elevation` from `target`. On the 'xz' floor it looks down at `pitch`
23
+ * degrees (90: straight down, screen-up -Z; less: from the south, so wall faces show). On the 'xy'
24
+ * plane (2D) it looks straight along -Z, screen-up +Y.
25
+ */
26
+ export declare function mapView(out: ViewBasis, target: ArrayLike<number>, elevation: number, pitch: number, orthoHeight: number, aspect: number, plane?: 'xz' | 'xy'): ViewBasis;
27
+ /** Any camera from its position and rotation (a unit quaternion), as `Camera3d` sees it. */
28
+ export declare function poseView(out: ViewBasis, position: ArrayLike<number>, rotation: ArrayLike<number>, orthographic: boolean, fovY: number, orthoHeight: number, aspect: number): ViewBasis;
29
+ /** The ray under CSS pixel (x, y) of a `width` × `height` view: origin and unit direction. */
30
+ export declare function viewRay(v: ViewBasis, x: number, y: number, width: number, height: number, outOrigin: Vec, outDir: Vec): void;
31
+ /**
32
+ * Where the ray under CSS pixel (x, y) meets the plane `y = level` (`axis` 1, a floor) or
33
+ * `z = level` (`axis` 2, a 2D world). False when it doesn't: it runs parallel, the plane is behind
34
+ * the camera, or the hit is past `maxDistance`.
35
+ */
36
+ export declare function viewToPlane(v: ViewBasis, x: number, y: number, width: number, height: number, level: number, out: Vec, maxDistance?: number, axis?: 1 | 2): boolean;
37
+ /** Projects a world point to CSS pixels. False when it's behind the camera. */
38
+ export declare function viewProject(v: ViewBasis, p: ArrayLike<number>, width: number, height: number, out: Vec): boolean;
39
+ export {};