feat(scene): shared zoom/pan for control and presentation
Keep effects pinned to map coordinates when the viewport changes; materials/NPC overlays stay screen-fixed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,8 @@ import type {
|
||||
SceneDarknessEvent,
|
||||
SceneDarknessState,
|
||||
SceneId,
|
||||
SceneViewEvent,
|
||||
SceneViewState,
|
||||
VideoPlaybackEvent,
|
||||
VideoPlaybackState,
|
||||
} from '../types';
|
||||
@@ -143,6 +145,11 @@ export const ipcChannels = {
|
||||
dispatch: 'sceneDarkness.dispatch',
|
||||
stateChanged: 'sceneDarkness.stateChanged',
|
||||
},
|
||||
sceneView: {
|
||||
getState: 'sceneView.getState',
|
||||
dispatch: 'sceneView.dispatch',
|
||||
stateChanged: 'sceneView.stateChanged',
|
||||
},
|
||||
video: {
|
||||
getState: 'video.getState',
|
||||
dispatch: 'video.dispatch',
|
||||
@@ -203,6 +210,7 @@ export type IpcEventMap = {
|
||||
[ipcChannels.materialsOverlay.stateChanged]: { state: MaterialsOverlayState };
|
||||
[ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState };
|
||||
[ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
|
||||
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
|
||||
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
|
||||
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
|
||||
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
|
||||
@@ -593,6 +601,14 @@ export type IpcInvokeMap = {
|
||||
req: { event: SceneDarknessEvent };
|
||||
res: { ok: true };
|
||||
};
|
||||
[ipcChannels.sceneView.getState]: {
|
||||
req: Record<string, never>;
|
||||
res: { state: SceneViewState };
|
||||
};
|
||||
[ipcChannels.sceneView.dispatch]: {
|
||||
req: { event: SceneViewEvent };
|
||||
res: { ok: true };
|
||||
};
|
||||
[ipcChannels.video.getState]: {
|
||||
req: Record<string, never>;
|
||||
res: { state: VideoPlaybackState };
|
||||
@@ -630,6 +646,7 @@ export type LegacyIpcEventMap = {
|
||||
[ipcChannels.materialsOverlay.stateChanged]: { state: MaterialsOverlayState };
|
||||
[ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState };
|
||||
[ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
|
||||
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
|
||||
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
|
||||
[ipcChannels.license.statusChanged]: Record<string, never>;
|
||||
};
|
||||
|
||||
@@ -4,4 +4,5 @@ export * from './ids';
|
||||
export * from './materials';
|
||||
export * from './npcs';
|
||||
export * from './sceneDarkness';
|
||||
export * from './sceneView';
|
||||
export * from './videoPlayback';
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/** Камера вида сцены (общая для Control и Presentation). */
|
||||
|
||||
export type SceneViewCamera = {
|
||||
/** Множитель поверх contain-fit; 1 = вся картинка влезает. */
|
||||
scale: number;
|
||||
/** Точка контента (0..1), которая держится в центре viewport. */
|
||||
ox: number;
|
||||
oy: number;
|
||||
};
|
||||
|
||||
export type SceneViewState = {
|
||||
revision: number;
|
||||
} & SceneViewCamera;
|
||||
|
||||
export type SceneViewEvent =
|
||||
| { kind: 'reset' }
|
||||
| { kind: 'set'; camera: SceneViewCamera };
|
||||
|
||||
export const DEFAULT_SCENE_VIEW_CAMERA: SceneViewCamera = {
|
||||
scale: 1,
|
||||
ox: 0.5,
|
||||
oy: 0.5,
|
||||
};
|
||||
|
||||
export const SCENE_VIEW_MIN_SCALE = 1;
|
||||
export const SCENE_VIEW_MAX_SCALE = 8;
|
||||
|
||||
export function clampSceneViewCamera(camera: SceneViewCamera): SceneViewCamera {
|
||||
const scale = Math.max(
|
||||
SCENE_VIEW_MIN_SCALE,
|
||||
Math.min(SCENE_VIEW_MAX_SCALE, Number.isFinite(camera.scale) ? camera.scale : 1),
|
||||
);
|
||||
if (scale <= 1.001) {
|
||||
return { ...DEFAULT_SCENE_VIEW_CAMERA };
|
||||
}
|
||||
const ox = Math.max(0, Math.min(1, Number.isFinite(camera.ox) ? camera.ox : 0.5));
|
||||
const oy = Math.max(0, Math.min(1, Number.isFinite(camera.oy) ? camera.oy : 0.5));
|
||||
return { scale, ox, oy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Зум относительно точки курсора в координатах хоста (px).
|
||||
* `containW/H` — размер contain-fit без зума.
|
||||
*/
|
||||
export function sceneViewZoomAt(
|
||||
camera: SceneViewCamera,
|
||||
args: {
|
||||
hostW: number;
|
||||
hostH: number;
|
||||
containW: number;
|
||||
containH: number;
|
||||
hostX: number;
|
||||
hostY: number;
|
||||
factor: number;
|
||||
},
|
||||
): SceneViewCamera {
|
||||
const hostW = Math.max(1, args.hostW);
|
||||
const hostH = Math.max(1, args.hostH);
|
||||
const containW = Math.max(1, args.containW);
|
||||
const containH = Math.max(1, args.containH);
|
||||
const cur = clampSceneViewCamera(camera);
|
||||
const displayW = containW * cur.scale;
|
||||
const displayH = containH * cur.scale;
|
||||
const left = hostW / 2 - cur.ox * displayW;
|
||||
const top = hostH / 2 - cur.oy * displayH;
|
||||
const ix = (args.hostX - left) / Math.max(1e-6, displayW);
|
||||
const iy = (args.hostY - top) / Math.max(1e-6, displayH);
|
||||
const nextScale = Math.max(
|
||||
SCENE_VIEW_MIN_SCALE,
|
||||
Math.min(SCENE_VIEW_MAX_SCALE, cur.scale * args.factor),
|
||||
);
|
||||
if (nextScale <= 1.001) return { ...DEFAULT_SCENE_VIEW_CAMERA };
|
||||
const nextDisplayW = containW * nextScale;
|
||||
const nextDisplayH = containH * nextScale;
|
||||
const ox = ix - (args.hostX - hostW / 2) / Math.max(1e-6, nextDisplayW);
|
||||
const oy = iy - (args.hostY - hostH / 2) / Math.max(1e-6, nextDisplayH);
|
||||
return clampSceneViewCamera({ scale: nextScale, ox, oy });
|
||||
}
|
||||
|
||||
/** Пан на `dx/dy` пикселей хоста (тянуть картинку в сторону движения указателя). */
|
||||
export function sceneViewPanBy(
|
||||
camera: SceneViewCamera,
|
||||
args: { containW: number; containH: number; dx: number; dy: number },
|
||||
): SceneViewCamera {
|
||||
const cur = clampSceneViewCamera(camera);
|
||||
if (cur.scale <= 1.001) return { ...DEFAULT_SCENE_VIEW_CAMERA };
|
||||
const displayW = Math.max(1e-6, args.containW * cur.scale);
|
||||
const displayH = Math.max(1e-6, args.containH * cur.scale);
|
||||
return clampSceneViewCamera({
|
||||
scale: cur.scale,
|
||||
ox: cur.ox - args.dx / displayW,
|
||||
oy: cur.oy - args.dy / displayH,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user