@scenography/plugin-focus 0.2.0 → 0.3.1

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/index.cjs CHANGED
@@ -60,29 +60,59 @@ var CameraController = class {
60
60
  resolve;
61
61
  emit;
62
62
  groundHeight;
63
+ getStart;
63
64
  tween = null;
64
65
  watch = null;
65
- constructor(camera, getRoam, resolve, emit, groundHeight) {
66
+ constructor(camera, getRoam, resolve, emit, groundHeight, getStart) {
66
67
  this.camera = camera;
67
68
  this.getRoam = getRoam;
68
69
  this.resolve = resolve;
69
70
  this.emit = emit;
70
71
  this.groundHeight = groundHeight;
72
+ this.getStart = getStart;
71
73
  }
72
74
  /** True while a framing tween is in progress (e.g. to suppress click-to-select). */
73
75
  isAnimating() {
74
76
  return this.tween !== null;
75
77
  }
78
+ /** True once a `focusArtwork` framing lands and the piece stays selected. */
79
+ isFocused() {
80
+ return this.watch !== null;
81
+ }
76
82
  focusArtwork(id, options = {}) {
77
- this.start(id, options.centerX ?? .5, options.widthFraction ?? 1, options.duration ?? 900, true);
83
+ this.start(id, options.centerX ?? .5, options.widthFraction ?? 1, options.duration ?? 900, true, options.minDistance ?? 0);
78
84
  }
79
85
  centerArtwork(id, options = {}) {
80
- this.start(id, .5, 1, options.duration ?? 900, false);
86
+ this.start(id, .5, 1, options.duration ?? 900, false, options.minDistance ?? 0);
81
87
  }
82
88
  release() {
83
89
  this.watch = null;
84
90
  }
85
- start(id, centerX, widthFraction, duration, watch) {
91
+ /** Tween the camera home — back to the scene's start position and orientation. */
92
+ reset() {
93
+ const roam = this.getRoam();
94
+ if (!roam) return;
95
+ const { position, yaw } = this.getStart();
96
+ const [sx = 0, , sz = 0] = position;
97
+ const toPos = new three.Vector3(sx, this.groundHeight(sx, sz) + roam.getEyeHeight(), sz);
98
+ const from = roam.getPose();
99
+ roam.setEnabled(false);
100
+ this.watch = null;
101
+ this.tween = {
102
+ id: "",
103
+ fromPos: from.position,
104
+ toPos,
105
+ fromPhi: from.phi,
106
+ dPhi: shortestAngle(yaw - from.phi),
107
+ fromTheta: from.theta,
108
+ toTheta: 0,
109
+ elapsed: 0,
110
+ duration: 700,
111
+ watch: false
112
+ };
113
+ this.emit("camera:reset");
114
+ }
115
+ start(id, centerX, widthFraction, duration, watch, minDistance) {
86
116
  const roam = this.getRoam();
87
117
  if (!roam) return;
88
118
  const object = this.resolve(id);
@@ -95,7 +125,7 @@ var CameraController = class {
95
125
  const tanH = tanV * this.camera.aspect;
96
126
  const dWidth = w / (widthFraction * 2 * tanH * FIT_MARGIN);
97
127
  const dHeight = h / (2 * tanV * FIT_MARGIN);
98
- const distance = Math.max(dWidth, dHeight);
128
+ const distance = Math.max(dWidth, dHeight, minDistance);
99
129
  const tangent = new three.Vector3(n.z, 0, -n.x);
100
130
  const lateral = -((centerX - .5) * 2) * distance * tanH;
101
131
  const px = center.x + n.x * distance + tangent.x * lateral;
@@ -177,9 +207,16 @@ const focus = () => ({
177
207
  setup: (ctx) => {
178
208
  const { camera, capabilities, emit, resolve } = ctx;
179
209
  const groundHeight = (x, z) => capabilities.get(_scenography_engine.TERRAIN)?.heightAt(x, z) ?? 0;
180
- const controller = new CameraController(camera, () => capabilities.get(_scenography_engine.ROAM_CONTROL), resolve, emit, groundHeight);
210
+ let start = ctx.scene.camera.start;
211
+ const controller = new CameraController(camera, () => capabilities.get(_scenography_engine.ROAM_CONTROL), resolve, emit, groundHeight, () => ({
212
+ position: start.position,
213
+ yaw: start.yaw ?? 0
214
+ }));
181
215
  return {
182
216
  update: (deltaMs) => controller.update(deltaMs),
217
+ setScene: (s) => {
218
+ start = s.camera.start;
219
+ },
183
220
  api: controller
184
221
  };
185
222
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Vector3","TERRAIN","ROAM_CONTROL"],"sources":["../src/easing.ts","../src/CameraController.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Sample a CSS-style cubic-bezier easing `(p1x, p1y, p2x, p2y)` with anchors at\n * (0,0) and (1,1). Returns a function mapping linear progress `x` in `[0,1]` to\n * eased progress `y`. Used to give camera moves a smooth ease-in-out curve.\n */\nexport function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number): (x: number) => number {\n const cx = 3 * p1x;\n const bx = 3 * (p2x - p1x) - cx;\n const ax = 1 - cx - bx;\n const cy = 3 * p1y;\n const by = 3 * (p2y - p1y) - cy;\n const ay = 1 - cy - by;\n\n const sampleX = (t: number): number => ((ax * t + bx) * t + cx) * t;\n const sampleY = (t: number): number => ((ay * t + by) * t + cy) * t;\n const slopeX = (t: number): number => (3 * ax * t + 2 * bx) * t + cx;\n\n // Solve x(t) = x for t via Newton-Raphson, then evaluate y(t).\n const solveT = (x: number): number => {\n let t = x;\n for (let i = 0; i < 8; i++) {\n const dx = sampleX(t) - x;\n if (Math.abs(dx) < 1e-5) return t;\n const d = slopeX(t);\n if (Math.abs(d) < 1e-6) break;\n t -= dx / d;\n }\n return t;\n };\n\n return (x: number): number => {\n if (x <= 0) return 0;\n if (x >= 1) return 1;\n return sampleY(solveT(x));\n };\n}\n\n/** Standard smooth ease-in-out, the default for camera framing moves. */\nexport const easeInOut = cubicBezier(0.42, 0, 0.58, 1);\n","import { Vector3, type Object3D, type PerspectiveCamera } from \"three\";\nimport type { CameraApi, FocusOptions, RoamControl } from \"@scenography/engine\";\n\nimport { easeInOut } from \"./easing\";\n\n/** Max up/down framing pitch (radians) — keeps the framed view from flipping. */\nconst MAX_PITCH = Math.PI / 3;\n\nconst TAU = Math.PI * 2;\nconst FIT_MARGIN = 0.85;\n// Thresholds past which the user is considered to have taken control back.\nconst TAKEOVER_DISTANCE = 20; // world units of XZ movement\nconst TAKEOVER_YAW = Math.PI / 2; // 90° of turn\n\nconst shortestAngle = (delta: number): number => ((((delta % TAU) + TAU + Math.PI) % TAU) - Math.PI);\nconst clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\ninterface Tween {\n id: string;\n fromPos: Vector3;\n toPos: Vector3;\n fromPhi: number;\n dPhi: number;\n fromTheta: number;\n toTheta: number;\n elapsed: number;\n duration: number;\n /** Whether to watch for user takeover once the tween lands. */\n watch: boolean;\n}\n\n/**\n * Drives the camera for artwork framing: it computes a target pose that fits a\n * piece into a region of the screen, then tweens there with an ease-in-out\n * curve. The eye height is read live from the free-roam controls — only XZ\n * position and rotation change. While a tween runs, free-roam input is disabled;\n * when it lands, control returns to the user and (for `focusArtwork`) the\n * controller watches for the user moving/turning away, emitting `artwork:released`.\n */\nexport class CameraController implements CameraApi {\n private tween: Tween | null = null;\n private watch: { id: string; pos: Vector3; phi: number } | null = null;\n\n constructor(\n private readonly camera: PerspectiveCamera,\n /** The roaming camera to suspend/resume while framing — from the `roamControl` capability. */\n private readonly getRoam: () => RoamControl | undefined,\n private readonly resolve: (id: string) => Object3D | undefined,\n private readonly emit: (event: string, payload?: unknown) => void,\n /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */\n private readonly groundHeight: (x: number, z: number) => number,\n ) {}\n\n /** True while a framing tween is in progress (e.g. to suppress click-to-select). */\n isAnimating(): boolean {\n return this.tween !== null;\n }\n\n focusArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, options.centerX ?? 0.5, options.widthFraction ?? 1, options.duration ?? 900, true);\n }\n\n centerArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, 0.5, 1, options.duration ?? 900, false);\n }\n\n release(): void {\n this.watch = null;\n }\n\n private start(id: string, centerX: number, widthFraction: number, duration: number, watch: boolean): void {\n // No roaming camera to hand off from/to: nothing to frame.\n const roam = this.getRoam();\n if (!roam) return;\n const object = this.resolve(id);\n if (!object) return;\n const size = (object.userData.size as [number, number] | undefined) ?? [100, 100];\n const [w, h] = size;\n\n const yaw = object.rotation.y;\n const n = new Vector3(Math.sin(yaw), 0, Math.cos(yaw)); // outward normal\n const center = object.position;\n\n const tanV = Math.tan(((this.camera.fov * Math.PI) / 180) / 2);\n const tanH = tanV * this.camera.aspect;\n const dWidth = w / (widthFraction * 2 * tanH * FIT_MARGIN);\n const dHeight = h / (2 * tanV * FIT_MARGIN);\n const distance = Math.max(dWidth, dHeight);\n\n // Offset the camera *position* sideways (not its aim) so the view stays\n // square-on to the wall and the piece lands at `centerX` on screen. Moving\n // the look target instead would yaw the camera and leave it skewed.\n const tangent = new Vector3(n.z, 0, -n.x);\n const ndcX = (centerX - 0.5) * 2;\n const lateral = -ndcX * distance * tanH;\n // Eye stands a fixed height above the ground at the framing spot, so it\n // follows the (possibly uneven) terrain instead of a fixed world Y. The eye\n // height is read live from the controls: it may have been recalibrated\n // after the floor's heightmap loaded.\n const px = center.x + n.x * distance + tangent.x * lateral;\n const pz = center.z + n.z * distance + tangent.z * lateral;\n const toPos = new Vector3(px, this.groundHeight(px, pz) + roam.getEyeHeight(), pz);\n\n // Aim straight ahead horizontally (perpendicular to the wall), tilting only\n // to center the piece vertically since the eye height is locked.\n const target = new Vector3(center.x + tangent.x * lateral, center.y, center.z + tangent.z * lateral);\n const f = target.sub(toPos).normalize();\n const toTheta = clamp(Math.asin(clamp(f.y, -1, 1)), -MAX_PITCH, MAX_PITCH);\n const toPhi = Math.atan2(-f.x, -f.z);\n\n const from = roam.getPose();\n roam.setEnabled(false);\n this.watch = null;\n this.tween = {\n id,\n fromPos: from.position,\n toPos,\n fromPhi: from.phi,\n dPhi: shortestAngle(toPhi - from.phi),\n fromTheta: from.theta,\n toTheta,\n elapsed: 0,\n duration,\n watch,\n };\n }\n\n /** Advance any running tween, and watch for user takeover once landed. */\n update(deltaMs: number): void {\n const roam = this.getRoam();\n const tween = this.tween;\n if (tween) {\n tween.elapsed += deltaMs;\n const t = clamp(tween.elapsed / tween.duration, 0, 1);\n const k = easeInOut(t);\n const phi = tween.fromPhi + tween.dPhi * k;\n const theta = tween.fromTheta + (tween.toTheta - tween.fromTheta) * k;\n // Y interpolates between two terrain-relative heights (start → target);\n // don't pin it to a fixed eye height or uphill framing flies in flat.\n const pos = tween.fromPos.clone().lerp(tween.toPos, k);\n roam?.setPose(pos, phi, theta);\n this.applyPose(pos, phi, theta);\n\n if (t >= 1) {\n roam?.setEnabled(true);\n this.watch = tween.watch ? { id: tween.id, pos: tween.toPos.clone(), phi } : null;\n this.tween = null;\n }\n return;\n }\n\n if (this.watch && roam) {\n const pose = roam.getPose();\n const moved = Math.hypot(pose.position.x - this.watch.pos.x, pose.position.z - this.watch.pos.z);\n const turned = Math.abs(shortestAngle(pose.phi - this.watch.phi));\n if (moved > TAKEOVER_DISTANCE || turned > TAKEOVER_YAW) {\n const id = this.watch.id;\n this.watch = null;\n this.emit(\"artwork:released\", { id });\n }\n }\n }\n\n /** Write a pose straight to the camera (used while we own it during a tween). */\n private applyPose(position: Vector3, phi: number, theta: number): void {\n this.camera.position.copy(position);\n const forward = new Vector3(-Math.cos(theta) * Math.sin(phi), Math.sin(theta), -Math.cos(theta) * Math.cos(phi));\n this.camera.lookAt(position.clone().add(forward));\n }\n}\n","import {\n ROAM_CONTROL,\n TERRAIN,\n type CameraApi,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\n\nimport { CameraController } from \"./CameraController\";\n\n/**\n * Camera framing — a *system* plugin. On selecting an artwork it animates the\n * camera to frame the piece (offset beside an info panel), and centers it when\n * the panel closes, handing control back to free-roam afterwards. Its api\n * surfaces as `ctx.camera`. Cooperates with the roaming camera through the\n * `roamControl` capability (suspends it during a tween); frames on the terrain\n * via `terrain`.\n */\nexport const focus = (): SystemPlugin<CameraApi> => ({\n name: \"focus\",\n setup: (ctx) => {\n const { camera, capabilities, emit, resolve } = ctx;\n const groundHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n\n const controller = new CameraController(\n camera,\n () => capabilities.get<RoamControl>(ROAM_CONTROL),\n resolve,\n emit,\n groundHeight,\n );\n\n return {\n update: (deltaMs) => controller.update(deltaMs),\n api: controller,\n };\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["Vector3","TERRAIN","ROAM_CONTROL"],"sources":["../src/easing.ts","../src/CameraController.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Sample a CSS-style cubic-bezier easing `(p1x, p1y, p2x, p2y)` with anchors at\n * (0,0) and (1,1). Returns a function mapping linear progress `x` in `[0,1]` to\n * eased progress `y`. Used to give camera moves a smooth ease-in-out curve.\n */\nexport function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number): (x: number) => number {\n const cx = 3 * p1x;\n const bx = 3 * (p2x - p1x) - cx;\n const ax = 1 - cx - bx;\n const cy = 3 * p1y;\n const by = 3 * (p2y - p1y) - cy;\n const ay = 1 - cy - by;\n\n const sampleX = (t: number): number => ((ax * t + bx) * t + cx) * t;\n const sampleY = (t: number): number => ((ay * t + by) * t + cy) * t;\n const slopeX = (t: number): number => (3 * ax * t + 2 * bx) * t + cx;\n\n // Solve x(t) = x for t via Newton-Raphson, then evaluate y(t).\n const solveT = (x: number): number => {\n let t = x;\n for (let i = 0; i < 8; i++) {\n const dx = sampleX(t) - x;\n if (Math.abs(dx) < 1e-5) return t;\n const d = slopeX(t);\n if (Math.abs(d) < 1e-6) break;\n t -= dx / d;\n }\n return t;\n };\n\n return (x: number): number => {\n if (x <= 0) return 0;\n if (x >= 1) return 1;\n return sampleY(solveT(x));\n };\n}\n\n/** Standard smooth ease-in-out, the default for camera framing moves. */\nexport const easeInOut = cubicBezier(0.42, 0, 0.58, 1);\n","import { Vector3, type Object3D, type PerspectiveCamera } from \"three\";\nimport type { CameraApi, FocusOptions, RoamControl } from \"@scenography/engine\";\n\nimport { easeInOut } from \"./easing\";\n\n/** Max up/down framing pitch (radians) — keeps the framed view from flipping. */\nconst MAX_PITCH = Math.PI / 3;\n\nconst TAU = Math.PI * 2;\nconst FIT_MARGIN = 0.85;\n// Thresholds past which the user is considered to have taken control back.\nconst TAKEOVER_DISTANCE = 20; // world units of XZ movement\nconst TAKEOVER_YAW = Math.PI / 2; // 90° of turn\n\nconst shortestAngle = (delta: number): number => ((((delta % TAU) + TAU + Math.PI) % TAU) - Math.PI);\nconst clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\ninterface Tween {\n id: string;\n fromPos: Vector3;\n toPos: Vector3;\n fromPhi: number;\n dPhi: number;\n fromTheta: number;\n toTheta: number;\n elapsed: number;\n duration: number;\n /** Whether to watch for user takeover once the tween lands. */\n watch: boolean;\n}\n\n/**\n * Drives the camera for artwork framing: it computes a target pose that fits a\n * piece into a region of the screen, then tweens there with an ease-in-out\n * curve. The eye height is read live from the free-roam controls — only XZ\n * position and rotation change. While a tween runs, free-roam input is disabled;\n * when it lands, control returns to the user and (for `focusArtwork`) the\n * controller watches for the user moving/turning away, emitting `artwork:released`.\n */\nexport class CameraController implements CameraApi {\n private tween: Tween | null = null;\n private watch: { id: string; pos: Vector3; phi: number } | null = null;\n\n constructor(\n private readonly camera: PerspectiveCamera,\n /** The roaming camera to suspend/resume while framing — from the `roamControl` capability. */\n private readonly getRoam: () => RoamControl | undefined,\n private readonly resolve: (id: string) => Object3D | undefined,\n private readonly emit: (event: string, payload?: unknown) => void,\n /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */\n private readonly groundHeight: (x: number, z: number) => number,\n /** The scene's start pose, read live (it can change on a scene hot-swap). */\n private readonly getStart: () => { position: number[]; yaw: number },\n ) {}\n\n /** True while a framing tween is in progress (e.g. to suppress click-to-select). */\n isAnimating(): boolean {\n return this.tween !== null;\n }\n\n /** True once a `focusArtwork` framing lands and the piece stays selected. */\n isFocused(): boolean {\n return this.watch !== null;\n }\n\n focusArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, options.centerX ?? 0.5, options.widthFraction ?? 1, options.duration ?? 900, true, options.minDistance ?? 0);\n }\n\n centerArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, 0.5, 1, options.duration ?? 900, false, options.minDistance ?? 0);\n }\n\n release(): void {\n this.watch = null;\n }\n\n /** Tween the camera home — back to the scene's start position and orientation. */\n reset(): void {\n const roam = this.getRoam();\n if (!roam) return;\n const { position, yaw } = this.getStart();\n const [sx = 0, , sz = 0] = position;\n // Land on the terrain at the start XZ (its authored Y may predate the floor's\n // heightmap); roam re-derives Y afterwards anyway.\n const toPos = new Vector3(sx, this.groundHeight(sx, sz) + roam.getEyeHeight(), sz);\n const from = roam.getPose();\n roam.setEnabled(false);\n this.watch = null;\n this.tween = {\n id: \"\",\n fromPos: from.position,\n toPos,\n fromPhi: from.phi,\n dPhi: shortestAngle(yaw - from.phi),\n fromTheta: from.theta,\n toTheta: 0,\n elapsed: 0,\n duration: 700,\n watch: false,\n };\n // Going home ends any selection. Overlays keyed to a piece (the info panel)\n // close on this, whether or not a focus tween was still in flight.\n this.emit(\"camera:reset\");\n }\n\n private start(\n id: string,\n centerX: number,\n widthFraction: number,\n duration: number,\n watch: boolean,\n minDistance: number,\n ): void {\n // No roaming camera to hand off from/to: nothing to frame.\n const roam = this.getRoam();\n if (!roam) return;\n const object = this.resolve(id);\n if (!object) return;\n const size = (object.userData.size as [number, number] | undefined) ?? [100, 100];\n const [w, h] = size;\n\n const yaw = object.rotation.y;\n const n = new Vector3(Math.sin(yaw), 0, Math.cos(yaw)); // outward normal\n const center = object.position;\n\n const tanV = Math.tan(((this.camera.fov * Math.PI) / 180) / 2);\n const tanH = tanV * this.camera.aspect;\n const dWidth = w / (widthFraction * 2 * tanH * FIT_MARGIN);\n const dHeight = h / (2 * tanV * FIT_MARGIN);\n // Fit the piece, but never closer than `minDistance` (e.g. so closing the\n // panel on a small piece doesn't leave the visitor pinned to the wall).\n const distance = Math.max(dWidth, dHeight, minDistance);\n\n // Offset the camera *position* sideways (not its aim) so the view stays\n // square-on to the wall and the piece lands at `centerX` on screen. Moving\n // the look target instead would yaw the camera and leave it skewed.\n const tangent = new Vector3(n.z, 0, -n.x);\n const ndcX = (centerX - 0.5) * 2;\n const lateral = -ndcX * distance * tanH;\n // Eye stands a fixed height above the ground at the framing spot, so it\n // follows the (possibly uneven) terrain instead of a fixed world Y. The eye\n // height is read live from the controls: it may have been recalibrated\n // after the floor's heightmap loaded.\n const px = center.x + n.x * distance + tangent.x * lateral;\n const pz = center.z + n.z * distance + tangent.z * lateral;\n const toPos = new Vector3(px, this.groundHeight(px, pz) + roam.getEyeHeight(), pz);\n\n // Aim straight ahead horizontally (perpendicular to the wall), tilting only\n // to center the piece vertically since the eye height is locked.\n const target = new Vector3(center.x + tangent.x * lateral, center.y, center.z + tangent.z * lateral);\n const f = target.sub(toPos).normalize();\n const toTheta = clamp(Math.asin(clamp(f.y, -1, 1)), -MAX_PITCH, MAX_PITCH);\n const toPhi = Math.atan2(-f.x, -f.z);\n\n const from = roam.getPose();\n roam.setEnabled(false);\n this.watch = null;\n this.tween = {\n id,\n fromPos: from.position,\n toPos,\n fromPhi: from.phi,\n dPhi: shortestAngle(toPhi - from.phi),\n fromTheta: from.theta,\n toTheta,\n elapsed: 0,\n duration,\n watch,\n };\n }\n\n /** Advance any running tween, and watch for user takeover once landed. */\n update(deltaMs: number): void {\n const roam = this.getRoam();\n const tween = this.tween;\n if (tween) {\n tween.elapsed += deltaMs;\n const t = clamp(tween.elapsed / tween.duration, 0, 1);\n const k = easeInOut(t);\n const phi = tween.fromPhi + tween.dPhi * k;\n const theta = tween.fromTheta + (tween.toTheta - tween.fromTheta) * k;\n // Y interpolates between two terrain-relative heights (start → target);\n // don't pin it to a fixed eye height or uphill framing flies in flat.\n const pos = tween.fromPos.clone().lerp(tween.toPos, k);\n roam?.setPose(pos, phi, theta);\n this.applyPose(pos, phi, theta);\n\n if (t >= 1) {\n roam?.setEnabled(true);\n this.watch = tween.watch ? { id: tween.id, pos: tween.toPos.clone(), phi } : null;\n this.tween = null;\n }\n return;\n }\n\n if (this.watch && roam) {\n const pose = roam.getPose();\n const moved = Math.hypot(pose.position.x - this.watch.pos.x, pose.position.z - this.watch.pos.z);\n const turned = Math.abs(shortestAngle(pose.phi - this.watch.phi));\n if (moved > TAKEOVER_DISTANCE || turned > TAKEOVER_YAW) {\n const id = this.watch.id;\n this.watch = null;\n this.emit(\"artwork:released\", { id });\n }\n }\n }\n\n /** Write a pose straight to the camera (used while we own it during a tween). */\n private applyPose(position: Vector3, phi: number, theta: number): void {\n this.camera.position.copy(position);\n const forward = new Vector3(-Math.cos(theta) * Math.sin(phi), Math.sin(theta), -Math.cos(theta) * Math.cos(phi));\n this.camera.lookAt(position.clone().add(forward));\n }\n}\n","import {\n ROAM_CONTROL,\n TERRAIN,\n type CameraApi,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\n\nimport { CameraController } from \"./CameraController\";\n\n/**\n * Camera framing — a *system* plugin. On selecting an artwork it animates the\n * camera to frame the piece (offset beside an info panel), and centers it when\n * the panel closes, handing control back to free-roam afterwards. Its api\n * surfaces as `ctx.camera`. Cooperates with the roaming camera through the\n * `roamControl` capability (suspends it during a tween); frames on the terrain\n * via `terrain`.\n */\nexport const focus = (): SystemPlugin<CameraApi> => ({\n name: \"focus\",\n setup: (ctx) => {\n const { camera, capabilities, emit, resolve } = ctx;\n const groundHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n\n // The start pose backs `reset()`; keep it live so a scene hot-swap moves home.\n let start = ctx.scene.camera.start;\n const controller = new CameraController(\n camera,\n () => capabilities.get<RoamControl>(ROAM_CONTROL),\n resolve,\n emit,\n groundHeight,\n () => ({ position: start.position, yaw: start.yaw ?? 0 }),\n );\n\n return {\n update: (deltaMs) => controller.update(deltaMs),\n setScene: (s) => {\n start = s.camera.start;\n },\n api: controller,\n };\n },\n});\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -29,18 +29,29 @@ declare class CameraController implements CameraApi {
29
29
  private readonly emit;
30
30
  /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */
31
31
  private readonly groundHeight;
32
+ /** The scene's start pose, read live (it can change on a scene hot-swap). */
33
+ private readonly getStart;
32
34
  private tween;
33
35
  private watch;
34
36
  constructor(camera: PerspectiveCamera, /** The roaming camera to suspend/resume while framing — from the `roamControl` capability. */
35
37
 
36
38
  getRoam: () => RoamControl | undefined, resolve: (id: string) => Object3D | undefined, emit: (event: string, payload?: unknown) => void, /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */
37
39
 
38
- groundHeight: (x: number, z: number) => number);
40
+ groundHeight: (x: number, z: number) => number, /** The scene's start pose, read live (it can change on a scene hot-swap). */
41
+
42
+ getStart: () => {
43
+ position: number[];
44
+ yaw: number;
45
+ });
39
46
  /** True while a framing tween is in progress (e.g. to suppress click-to-select). */
40
47
  isAnimating(): boolean;
48
+ /** True once a `focusArtwork` framing lands and the piece stays selected. */
49
+ isFocused(): boolean;
41
50
  focusArtwork(id: string, options?: FocusOptions): void;
42
51
  centerArtwork(id: string, options?: FocusOptions): void;
43
52
  release(): void;
53
+ /** Tween the camera home — back to the scene's start position and orientation. */
54
+ reset(): void;
44
55
  private start;
45
56
  /** Advance any running tween, and watch for user takeover once landed. */
46
57
  update(deltaMs: number): void;
package/dist/index.d.mts CHANGED
@@ -29,18 +29,29 @@ declare class CameraController implements CameraApi {
29
29
  private readonly emit;
30
30
  /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */
31
31
  private readonly groundHeight;
32
+ /** The scene's start pose, read live (it can change on a scene hot-swap). */
33
+ private readonly getStart;
32
34
  private tween;
33
35
  private watch;
34
36
  constructor(camera: PerspectiveCamera, /** The roaming camera to suspend/resume while framing — from the `roamControl` capability. */
35
37
 
36
38
  getRoam: () => RoamControl | undefined, resolve: (id: string) => Object3D | undefined, emit: (event: string, payload?: unknown) => void, /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */
37
39
 
38
- groundHeight: (x: number, z: number) => number);
40
+ groundHeight: (x: number, z: number) => number, /** The scene's start pose, read live (it can change on a scene hot-swap). */
41
+
42
+ getStart: () => {
43
+ position: number[];
44
+ yaw: number;
45
+ });
39
46
  /** True while a framing tween is in progress (e.g. to suppress click-to-select). */
40
47
  isAnimating(): boolean;
48
+ /** True once a `focusArtwork` framing lands and the piece stays selected. */
49
+ isFocused(): boolean;
41
50
  focusArtwork(id: string, options?: FocusOptions): void;
42
51
  centerArtwork(id: string, options?: FocusOptions): void;
43
52
  release(): void;
53
+ /** Tween the camera home — back to the scene's start position and orientation. */
54
+ reset(): void;
44
55
  private start;
45
56
  /** Advance any running tween, and watch for user takeover once landed. */
46
57
  update(deltaMs: number): void;
package/dist/index.mjs CHANGED
@@ -59,29 +59,59 @@ var CameraController = class {
59
59
  resolve;
60
60
  emit;
61
61
  groundHeight;
62
+ getStart;
62
63
  tween = null;
63
64
  watch = null;
64
- constructor(camera, getRoam, resolve, emit, groundHeight) {
65
+ constructor(camera, getRoam, resolve, emit, groundHeight, getStart) {
65
66
  this.camera = camera;
66
67
  this.getRoam = getRoam;
67
68
  this.resolve = resolve;
68
69
  this.emit = emit;
69
70
  this.groundHeight = groundHeight;
71
+ this.getStart = getStart;
70
72
  }
71
73
  /** True while a framing tween is in progress (e.g. to suppress click-to-select). */
72
74
  isAnimating() {
73
75
  return this.tween !== null;
74
76
  }
77
+ /** True once a `focusArtwork` framing lands and the piece stays selected. */
78
+ isFocused() {
79
+ return this.watch !== null;
80
+ }
75
81
  focusArtwork(id, options = {}) {
76
- this.start(id, options.centerX ?? .5, options.widthFraction ?? 1, options.duration ?? 900, true);
82
+ this.start(id, options.centerX ?? .5, options.widthFraction ?? 1, options.duration ?? 900, true, options.minDistance ?? 0);
77
83
  }
78
84
  centerArtwork(id, options = {}) {
79
- this.start(id, .5, 1, options.duration ?? 900, false);
85
+ this.start(id, .5, 1, options.duration ?? 900, false, options.minDistance ?? 0);
80
86
  }
81
87
  release() {
82
88
  this.watch = null;
83
89
  }
84
- start(id, centerX, widthFraction, duration, watch) {
90
+ /** Tween the camera home — back to the scene's start position and orientation. */
91
+ reset() {
92
+ const roam = this.getRoam();
93
+ if (!roam) return;
94
+ const { position, yaw } = this.getStart();
95
+ const [sx = 0, , sz = 0] = position;
96
+ const toPos = new Vector3(sx, this.groundHeight(sx, sz) + roam.getEyeHeight(), sz);
97
+ const from = roam.getPose();
98
+ roam.setEnabled(false);
99
+ this.watch = null;
100
+ this.tween = {
101
+ id: "",
102
+ fromPos: from.position,
103
+ toPos,
104
+ fromPhi: from.phi,
105
+ dPhi: shortestAngle(yaw - from.phi),
106
+ fromTheta: from.theta,
107
+ toTheta: 0,
108
+ elapsed: 0,
109
+ duration: 700,
110
+ watch: false
111
+ };
112
+ this.emit("camera:reset");
113
+ }
114
+ start(id, centerX, widthFraction, duration, watch, minDistance) {
85
115
  const roam = this.getRoam();
86
116
  if (!roam) return;
87
117
  const object = this.resolve(id);
@@ -94,7 +124,7 @@ var CameraController = class {
94
124
  const tanH = tanV * this.camera.aspect;
95
125
  const dWidth = w / (widthFraction * 2 * tanH * FIT_MARGIN);
96
126
  const dHeight = h / (2 * tanV * FIT_MARGIN);
97
- const distance = Math.max(dWidth, dHeight);
127
+ const distance = Math.max(dWidth, dHeight, minDistance);
98
128
  const tangent = new Vector3(n.z, 0, -n.x);
99
129
  const lateral = -((centerX - .5) * 2) * distance * tanH;
100
130
  const px = center.x + n.x * distance + tangent.x * lateral;
@@ -176,9 +206,16 @@ const focus = () => ({
176
206
  setup: (ctx) => {
177
207
  const { camera, capabilities, emit, resolve } = ctx;
178
208
  const groundHeight = (x, z) => capabilities.get(TERRAIN)?.heightAt(x, z) ?? 0;
179
- const controller = new CameraController(camera, () => capabilities.get(ROAM_CONTROL), resolve, emit, groundHeight);
209
+ let start = ctx.scene.camera.start;
210
+ const controller = new CameraController(camera, () => capabilities.get(ROAM_CONTROL), resolve, emit, groundHeight, () => ({
211
+ position: start.position,
212
+ yaw: start.yaw ?? 0
213
+ }));
180
214
  return {
181
215
  update: (deltaMs) => controller.update(deltaMs),
216
+ setScene: (s) => {
217
+ start = s.camera.start;
218
+ },
182
219
  api: controller
183
220
  };
184
221
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/easing.ts","../src/CameraController.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Sample a CSS-style cubic-bezier easing `(p1x, p1y, p2x, p2y)` with anchors at\n * (0,0) and (1,1). Returns a function mapping linear progress `x` in `[0,1]` to\n * eased progress `y`. Used to give camera moves a smooth ease-in-out curve.\n */\nexport function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number): (x: number) => number {\n const cx = 3 * p1x;\n const bx = 3 * (p2x - p1x) - cx;\n const ax = 1 - cx - bx;\n const cy = 3 * p1y;\n const by = 3 * (p2y - p1y) - cy;\n const ay = 1 - cy - by;\n\n const sampleX = (t: number): number => ((ax * t + bx) * t + cx) * t;\n const sampleY = (t: number): number => ((ay * t + by) * t + cy) * t;\n const slopeX = (t: number): number => (3 * ax * t + 2 * bx) * t + cx;\n\n // Solve x(t) = x for t via Newton-Raphson, then evaluate y(t).\n const solveT = (x: number): number => {\n let t = x;\n for (let i = 0; i < 8; i++) {\n const dx = sampleX(t) - x;\n if (Math.abs(dx) < 1e-5) return t;\n const d = slopeX(t);\n if (Math.abs(d) < 1e-6) break;\n t -= dx / d;\n }\n return t;\n };\n\n return (x: number): number => {\n if (x <= 0) return 0;\n if (x >= 1) return 1;\n return sampleY(solveT(x));\n };\n}\n\n/** Standard smooth ease-in-out, the default for camera framing moves. */\nexport const easeInOut = cubicBezier(0.42, 0, 0.58, 1);\n","import { Vector3, type Object3D, type PerspectiveCamera } from \"three\";\nimport type { CameraApi, FocusOptions, RoamControl } from \"@scenography/engine\";\n\nimport { easeInOut } from \"./easing\";\n\n/** Max up/down framing pitch (radians) — keeps the framed view from flipping. */\nconst MAX_PITCH = Math.PI / 3;\n\nconst TAU = Math.PI * 2;\nconst FIT_MARGIN = 0.85;\n// Thresholds past which the user is considered to have taken control back.\nconst TAKEOVER_DISTANCE = 20; // world units of XZ movement\nconst TAKEOVER_YAW = Math.PI / 2; // 90° of turn\n\nconst shortestAngle = (delta: number): number => ((((delta % TAU) + TAU + Math.PI) % TAU) - Math.PI);\nconst clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\ninterface Tween {\n id: string;\n fromPos: Vector3;\n toPos: Vector3;\n fromPhi: number;\n dPhi: number;\n fromTheta: number;\n toTheta: number;\n elapsed: number;\n duration: number;\n /** Whether to watch for user takeover once the tween lands. */\n watch: boolean;\n}\n\n/**\n * Drives the camera for artwork framing: it computes a target pose that fits a\n * piece into a region of the screen, then tweens there with an ease-in-out\n * curve. The eye height is read live from the free-roam controls — only XZ\n * position and rotation change. While a tween runs, free-roam input is disabled;\n * when it lands, control returns to the user and (for `focusArtwork`) the\n * controller watches for the user moving/turning away, emitting `artwork:released`.\n */\nexport class CameraController implements CameraApi {\n private tween: Tween | null = null;\n private watch: { id: string; pos: Vector3; phi: number } | null = null;\n\n constructor(\n private readonly camera: PerspectiveCamera,\n /** The roaming camera to suspend/resume while framing — from the `roamControl` capability. */\n private readonly getRoam: () => RoamControl | undefined,\n private readonly resolve: (id: string) => Object3D | undefined,\n private readonly emit: (event: string, payload?: unknown) => void,\n /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */\n private readonly groundHeight: (x: number, z: number) => number,\n ) {}\n\n /** True while a framing tween is in progress (e.g. to suppress click-to-select). */\n isAnimating(): boolean {\n return this.tween !== null;\n }\n\n focusArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, options.centerX ?? 0.5, options.widthFraction ?? 1, options.duration ?? 900, true);\n }\n\n centerArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, 0.5, 1, options.duration ?? 900, false);\n }\n\n release(): void {\n this.watch = null;\n }\n\n private start(id: string, centerX: number, widthFraction: number, duration: number, watch: boolean): void {\n // No roaming camera to hand off from/to: nothing to frame.\n const roam = this.getRoam();\n if (!roam) return;\n const object = this.resolve(id);\n if (!object) return;\n const size = (object.userData.size as [number, number] | undefined) ?? [100, 100];\n const [w, h] = size;\n\n const yaw = object.rotation.y;\n const n = new Vector3(Math.sin(yaw), 0, Math.cos(yaw)); // outward normal\n const center = object.position;\n\n const tanV = Math.tan(((this.camera.fov * Math.PI) / 180) / 2);\n const tanH = tanV * this.camera.aspect;\n const dWidth = w / (widthFraction * 2 * tanH * FIT_MARGIN);\n const dHeight = h / (2 * tanV * FIT_MARGIN);\n const distance = Math.max(dWidth, dHeight);\n\n // Offset the camera *position* sideways (not its aim) so the view stays\n // square-on to the wall and the piece lands at `centerX` on screen. Moving\n // the look target instead would yaw the camera and leave it skewed.\n const tangent = new Vector3(n.z, 0, -n.x);\n const ndcX = (centerX - 0.5) * 2;\n const lateral = -ndcX * distance * tanH;\n // Eye stands a fixed height above the ground at the framing spot, so it\n // follows the (possibly uneven) terrain instead of a fixed world Y. The eye\n // height is read live from the controls: it may have been recalibrated\n // after the floor's heightmap loaded.\n const px = center.x + n.x * distance + tangent.x * lateral;\n const pz = center.z + n.z * distance + tangent.z * lateral;\n const toPos = new Vector3(px, this.groundHeight(px, pz) + roam.getEyeHeight(), pz);\n\n // Aim straight ahead horizontally (perpendicular to the wall), tilting only\n // to center the piece vertically since the eye height is locked.\n const target = new Vector3(center.x + tangent.x * lateral, center.y, center.z + tangent.z * lateral);\n const f = target.sub(toPos).normalize();\n const toTheta = clamp(Math.asin(clamp(f.y, -1, 1)), -MAX_PITCH, MAX_PITCH);\n const toPhi = Math.atan2(-f.x, -f.z);\n\n const from = roam.getPose();\n roam.setEnabled(false);\n this.watch = null;\n this.tween = {\n id,\n fromPos: from.position,\n toPos,\n fromPhi: from.phi,\n dPhi: shortestAngle(toPhi - from.phi),\n fromTheta: from.theta,\n toTheta,\n elapsed: 0,\n duration,\n watch,\n };\n }\n\n /** Advance any running tween, and watch for user takeover once landed. */\n update(deltaMs: number): void {\n const roam = this.getRoam();\n const tween = this.tween;\n if (tween) {\n tween.elapsed += deltaMs;\n const t = clamp(tween.elapsed / tween.duration, 0, 1);\n const k = easeInOut(t);\n const phi = tween.fromPhi + tween.dPhi * k;\n const theta = tween.fromTheta + (tween.toTheta - tween.fromTheta) * k;\n // Y interpolates between two terrain-relative heights (start → target);\n // don't pin it to a fixed eye height or uphill framing flies in flat.\n const pos = tween.fromPos.clone().lerp(tween.toPos, k);\n roam?.setPose(pos, phi, theta);\n this.applyPose(pos, phi, theta);\n\n if (t >= 1) {\n roam?.setEnabled(true);\n this.watch = tween.watch ? { id: tween.id, pos: tween.toPos.clone(), phi } : null;\n this.tween = null;\n }\n return;\n }\n\n if (this.watch && roam) {\n const pose = roam.getPose();\n const moved = Math.hypot(pose.position.x - this.watch.pos.x, pose.position.z - this.watch.pos.z);\n const turned = Math.abs(shortestAngle(pose.phi - this.watch.phi));\n if (moved > TAKEOVER_DISTANCE || turned > TAKEOVER_YAW) {\n const id = this.watch.id;\n this.watch = null;\n this.emit(\"artwork:released\", { id });\n }\n }\n }\n\n /** Write a pose straight to the camera (used while we own it during a tween). */\n private applyPose(position: Vector3, phi: number, theta: number): void {\n this.camera.position.copy(position);\n const forward = new Vector3(-Math.cos(theta) * Math.sin(phi), Math.sin(theta), -Math.cos(theta) * Math.cos(phi));\n this.camera.lookAt(position.clone().add(forward));\n }\n}\n","import {\n ROAM_CONTROL,\n TERRAIN,\n type CameraApi,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\n\nimport { CameraController } from \"./CameraController\";\n\n/**\n * Camera framing — a *system* plugin. On selecting an artwork it animates the\n * camera to frame the piece (offset beside an info panel), and centers it when\n * the panel closes, handing control back to free-roam afterwards. Its api\n * surfaces as `ctx.camera`. Cooperates with the roaming camera through the\n * `roamControl` capability (suspends it during a tween); frames on the terrain\n * via `terrain`.\n */\nexport const focus = (): SystemPlugin<CameraApi> => ({\n name: \"focus\",\n setup: (ctx) => {\n const { camera, capabilities, emit, resolve } = ctx;\n const groundHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n\n const controller = new CameraController(\n camera,\n () => capabilities.get<RoamControl>(ROAM_CONTROL),\n resolve,\n emit,\n groundHeight,\n );\n\n return {\n update: (deltaMs) => controller.update(deltaMs),\n api: controller,\n };\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/easing.ts","../src/CameraController.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Sample a CSS-style cubic-bezier easing `(p1x, p1y, p2x, p2y)` with anchors at\n * (0,0) and (1,1). Returns a function mapping linear progress `x` in `[0,1]` to\n * eased progress `y`. Used to give camera moves a smooth ease-in-out curve.\n */\nexport function cubicBezier(p1x: number, p1y: number, p2x: number, p2y: number): (x: number) => number {\n const cx = 3 * p1x;\n const bx = 3 * (p2x - p1x) - cx;\n const ax = 1 - cx - bx;\n const cy = 3 * p1y;\n const by = 3 * (p2y - p1y) - cy;\n const ay = 1 - cy - by;\n\n const sampleX = (t: number): number => ((ax * t + bx) * t + cx) * t;\n const sampleY = (t: number): number => ((ay * t + by) * t + cy) * t;\n const slopeX = (t: number): number => (3 * ax * t + 2 * bx) * t + cx;\n\n // Solve x(t) = x for t via Newton-Raphson, then evaluate y(t).\n const solveT = (x: number): number => {\n let t = x;\n for (let i = 0; i < 8; i++) {\n const dx = sampleX(t) - x;\n if (Math.abs(dx) < 1e-5) return t;\n const d = slopeX(t);\n if (Math.abs(d) < 1e-6) break;\n t -= dx / d;\n }\n return t;\n };\n\n return (x: number): number => {\n if (x <= 0) return 0;\n if (x >= 1) return 1;\n return sampleY(solveT(x));\n };\n}\n\n/** Standard smooth ease-in-out, the default for camera framing moves. */\nexport const easeInOut = cubicBezier(0.42, 0, 0.58, 1);\n","import { Vector3, type Object3D, type PerspectiveCamera } from \"three\";\nimport type { CameraApi, FocusOptions, RoamControl } from \"@scenography/engine\";\n\nimport { easeInOut } from \"./easing\";\n\n/** Max up/down framing pitch (radians) — keeps the framed view from flipping. */\nconst MAX_PITCH = Math.PI / 3;\n\nconst TAU = Math.PI * 2;\nconst FIT_MARGIN = 0.85;\n// Thresholds past which the user is considered to have taken control back.\nconst TAKEOVER_DISTANCE = 20; // world units of XZ movement\nconst TAKEOVER_YAW = Math.PI / 2; // 90° of turn\n\nconst shortestAngle = (delta: number): number => ((((delta % TAU) + TAU + Math.PI) % TAU) - Math.PI);\nconst clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\ninterface Tween {\n id: string;\n fromPos: Vector3;\n toPos: Vector3;\n fromPhi: number;\n dPhi: number;\n fromTheta: number;\n toTheta: number;\n elapsed: number;\n duration: number;\n /** Whether to watch for user takeover once the tween lands. */\n watch: boolean;\n}\n\n/**\n * Drives the camera for artwork framing: it computes a target pose that fits a\n * piece into a region of the screen, then tweens there with an ease-in-out\n * curve. The eye height is read live from the free-roam controls — only XZ\n * position and rotation change. While a tween runs, free-roam input is disabled;\n * when it lands, control returns to the user and (for `focusArtwork`) the\n * controller watches for the user moving/turning away, emitting `artwork:released`.\n */\nexport class CameraController implements CameraApi {\n private tween: Tween | null = null;\n private watch: { id: string; pos: Vector3; phi: number } | null = null;\n\n constructor(\n private readonly camera: PerspectiveCamera,\n /** The roaming camera to suspend/resume while framing — from the `roamControl` capability. */\n private readonly getRoam: () => RoamControl | undefined,\n private readonly resolve: (id: string) => Object3D | undefined,\n private readonly emit: (event: string, payload?: unknown) => void,\n /** Ground height at `(x, z)` — so framing sits on the terrain, not a fixed Y. */\n private readonly groundHeight: (x: number, z: number) => number,\n /** The scene's start pose, read live (it can change on a scene hot-swap). */\n private readonly getStart: () => { position: number[]; yaw: number },\n ) {}\n\n /** True while a framing tween is in progress (e.g. to suppress click-to-select). */\n isAnimating(): boolean {\n return this.tween !== null;\n }\n\n /** True once a `focusArtwork` framing lands and the piece stays selected. */\n isFocused(): boolean {\n return this.watch !== null;\n }\n\n focusArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, options.centerX ?? 0.5, options.widthFraction ?? 1, options.duration ?? 900, true, options.minDistance ?? 0);\n }\n\n centerArtwork(id: string, options: FocusOptions = {}): void {\n this.start(id, 0.5, 1, options.duration ?? 900, false, options.minDistance ?? 0);\n }\n\n release(): void {\n this.watch = null;\n }\n\n /** Tween the camera home — back to the scene's start position and orientation. */\n reset(): void {\n const roam = this.getRoam();\n if (!roam) return;\n const { position, yaw } = this.getStart();\n const [sx = 0, , sz = 0] = position;\n // Land on the terrain at the start XZ (its authored Y may predate the floor's\n // heightmap); roam re-derives Y afterwards anyway.\n const toPos = new Vector3(sx, this.groundHeight(sx, sz) + roam.getEyeHeight(), sz);\n const from = roam.getPose();\n roam.setEnabled(false);\n this.watch = null;\n this.tween = {\n id: \"\",\n fromPos: from.position,\n toPos,\n fromPhi: from.phi,\n dPhi: shortestAngle(yaw - from.phi),\n fromTheta: from.theta,\n toTheta: 0,\n elapsed: 0,\n duration: 700,\n watch: false,\n };\n // Going home ends any selection. Overlays keyed to a piece (the info panel)\n // close on this, whether or not a focus tween was still in flight.\n this.emit(\"camera:reset\");\n }\n\n private start(\n id: string,\n centerX: number,\n widthFraction: number,\n duration: number,\n watch: boolean,\n minDistance: number,\n ): void {\n // No roaming camera to hand off from/to: nothing to frame.\n const roam = this.getRoam();\n if (!roam) return;\n const object = this.resolve(id);\n if (!object) return;\n const size = (object.userData.size as [number, number] | undefined) ?? [100, 100];\n const [w, h] = size;\n\n const yaw = object.rotation.y;\n const n = new Vector3(Math.sin(yaw), 0, Math.cos(yaw)); // outward normal\n const center = object.position;\n\n const tanV = Math.tan(((this.camera.fov * Math.PI) / 180) / 2);\n const tanH = tanV * this.camera.aspect;\n const dWidth = w / (widthFraction * 2 * tanH * FIT_MARGIN);\n const dHeight = h / (2 * tanV * FIT_MARGIN);\n // Fit the piece, but never closer than `minDistance` (e.g. so closing the\n // panel on a small piece doesn't leave the visitor pinned to the wall).\n const distance = Math.max(dWidth, dHeight, minDistance);\n\n // Offset the camera *position* sideways (not its aim) so the view stays\n // square-on to the wall and the piece lands at `centerX` on screen. Moving\n // the look target instead would yaw the camera and leave it skewed.\n const tangent = new Vector3(n.z, 0, -n.x);\n const ndcX = (centerX - 0.5) * 2;\n const lateral = -ndcX * distance * tanH;\n // Eye stands a fixed height above the ground at the framing spot, so it\n // follows the (possibly uneven) terrain instead of a fixed world Y. The eye\n // height is read live from the controls: it may have been recalibrated\n // after the floor's heightmap loaded.\n const px = center.x + n.x * distance + tangent.x * lateral;\n const pz = center.z + n.z * distance + tangent.z * lateral;\n const toPos = new Vector3(px, this.groundHeight(px, pz) + roam.getEyeHeight(), pz);\n\n // Aim straight ahead horizontally (perpendicular to the wall), tilting only\n // to center the piece vertically since the eye height is locked.\n const target = new Vector3(center.x + tangent.x * lateral, center.y, center.z + tangent.z * lateral);\n const f = target.sub(toPos).normalize();\n const toTheta = clamp(Math.asin(clamp(f.y, -1, 1)), -MAX_PITCH, MAX_PITCH);\n const toPhi = Math.atan2(-f.x, -f.z);\n\n const from = roam.getPose();\n roam.setEnabled(false);\n this.watch = null;\n this.tween = {\n id,\n fromPos: from.position,\n toPos,\n fromPhi: from.phi,\n dPhi: shortestAngle(toPhi - from.phi),\n fromTheta: from.theta,\n toTheta,\n elapsed: 0,\n duration,\n watch,\n };\n }\n\n /** Advance any running tween, and watch for user takeover once landed. */\n update(deltaMs: number): void {\n const roam = this.getRoam();\n const tween = this.tween;\n if (tween) {\n tween.elapsed += deltaMs;\n const t = clamp(tween.elapsed / tween.duration, 0, 1);\n const k = easeInOut(t);\n const phi = tween.fromPhi + tween.dPhi * k;\n const theta = tween.fromTheta + (tween.toTheta - tween.fromTheta) * k;\n // Y interpolates between two terrain-relative heights (start → target);\n // don't pin it to a fixed eye height or uphill framing flies in flat.\n const pos = tween.fromPos.clone().lerp(tween.toPos, k);\n roam?.setPose(pos, phi, theta);\n this.applyPose(pos, phi, theta);\n\n if (t >= 1) {\n roam?.setEnabled(true);\n this.watch = tween.watch ? { id: tween.id, pos: tween.toPos.clone(), phi } : null;\n this.tween = null;\n }\n return;\n }\n\n if (this.watch && roam) {\n const pose = roam.getPose();\n const moved = Math.hypot(pose.position.x - this.watch.pos.x, pose.position.z - this.watch.pos.z);\n const turned = Math.abs(shortestAngle(pose.phi - this.watch.phi));\n if (moved > TAKEOVER_DISTANCE || turned > TAKEOVER_YAW) {\n const id = this.watch.id;\n this.watch = null;\n this.emit(\"artwork:released\", { id });\n }\n }\n }\n\n /** Write a pose straight to the camera (used while we own it during a tween). */\n private applyPose(position: Vector3, phi: number, theta: number): void {\n this.camera.position.copy(position);\n const forward = new Vector3(-Math.cos(theta) * Math.sin(phi), Math.sin(theta), -Math.cos(theta) * Math.cos(phi));\n this.camera.lookAt(position.clone().add(forward));\n }\n}\n","import {\n ROAM_CONTROL,\n TERRAIN,\n type CameraApi,\n type RoamControl,\n type SystemPlugin,\n type TerrainCapability,\n} from \"@scenography/engine\";\n\nimport { CameraController } from \"./CameraController\";\n\n/**\n * Camera framing — a *system* plugin. On selecting an artwork it animates the\n * camera to frame the piece (offset beside an info panel), and centers it when\n * the panel closes, handing control back to free-roam afterwards. Its api\n * surfaces as `ctx.camera`. Cooperates with the roaming camera through the\n * `roamControl` capability (suspends it during a tween); frames on the terrain\n * via `terrain`.\n */\nexport const focus = (): SystemPlugin<CameraApi> => ({\n name: \"focus\",\n setup: (ctx) => {\n const { camera, capabilities, emit, resolve } = ctx;\n const groundHeight = (x: number, z: number): number =>\n capabilities.get<TerrainCapability>(TERRAIN)?.heightAt(x, z) ?? 0;\n\n // The start pose backs `reset()`; keep it live so a scene hot-swap moves home.\n let start = ctx.scene.camera.start;\n const controller = new CameraController(\n camera,\n () => capabilities.get<RoamControl>(ROAM_CONTROL),\n resolve,\n emit,\n groundHeight,\n () => ({ position: start.position, yaw: start.yaw ?? 0 }),\n );\n\n return {\n update: (deltaMs) => controller.update(deltaMs),\n setScene: (s) => {\n start = s.camera.start;\n },\n api: controller,\n };\n },\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenography/plugin-focus",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Focus plugin for scenography: animates the camera to frame an artwork on selection and center it when its info panel closes. A system plugin exposing ctx.camera.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -23,16 +23,16 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "@scenography/scene": "0.2.0"
26
+ "@scenography/scene": "0.3.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/three": "^0.160.0",
30
30
  "three": "^0.160.0",
31
- "@scenography/engine": "0.2.0"
31
+ "@scenography/engine": "0.3.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "three": ">=0.160.0",
35
- "@scenography/engine": "0.2.0"
35
+ "@scenography/engine": "0.3.1"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsdown",