Files
workinf_Blender_Wasm/web/protocol/viewport-camera.ts
mes123456 371427a6bc feat(web): add dynamic area layouts and camera views
Area headers now expose a real ⋮ menu with split, join, swap,
duplicate, maximize/focus, close and reset; the first operation
measures the CSS grid into normalized rects and switches that
workspace to an OPFS-persisted custom layout while reusing the
existing React content per slot.

ViewportNavigation gains Numpad0 camera view with last-view restore,
framing-only navigation when unlocked, Camera to View lock that writes
the camera object (rotation-mode-aware native setObjectTransform),
and Ctrl+Alt+Numpad0 camera-to-view. Camera lock persists per area.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-13 09:50:12 +08:00

111 lines
4.3 KiB
TypeScript

export interface ViewportOrbitState {
yaw: number;
pitch: number;
distance: number;
target: [number, number, number];
}
export interface ViewportCameraState extends ViewportOrbitState {
position: [number, number, number];
}
export type ViewportView = 'USER' | 'CAMERA' | 'FRONT' | 'BACK' | 'RIGHT' | 'LEFT' | 'TOP' | 'BOTTOM';
export type ViewportNavigationCommand = ViewportView | 'PROJECTION' | 'FRAME_ALL' | 'FRAME_SELECTED' | 'ZOOM_IN' | 'ZOOM_OUT' | 'TOGGLE_CAMERA_VIEW' | 'CAMERA_TO_VIEW' | 'CAMERA_LOCK';
export interface ViewportNavigationCamera {
position: [number,number,number];
quaternion: [number,number,number,number];
target: [number,number,number];
projection:'PERSPECTIVE'|'ORTHOGRAPHIC';
fov:number;aspect:number;near:number;far:number;zoom:number;orthoHeight:number;
}
export interface NativeViewportState {
offset: [number,number,number];
rotation: [number,number,number,number];
distance: number; lens: number; near: number; far: number;
projection: 'PERSPECTIVE'|'ORTHOGRAPHIC'|'CAMERA';
locked?: boolean;
}
export interface BlenderViewportCamera {
position: [number,number,number]; target: [number,number,number];
orientation: [number,number,number,number];
projection: 'PERSPECTIVE'|'ORTHOGRAPHIC';
fov:number; near:number; far:number; zoom:number; orthoHeight:number;
shiftX?:number; shiftY?:number; objectId?:string; lensMm?:number; sensorWidthMm?:number; sensorHeightMm?:number; camzoom?:number;
}
export interface BlenderSceneCameraFrame extends BlenderViewportCamera {
objectId: string;
lensMm: number;
sensorWidthMm: number;
sensorHeightMm: number;
sensorFit: number;
shift: [number, number];
orthoScale: number;
camzoom: number;
}
export function cameraZoomFac(camzoom: number): number {
const clamped = Math.max(-30, Math.min(600, camzoom));
return ((Math.SQRT2 + clamped / 50) ** 2) / 4;
}
export function eulerFromQuaternion(q: readonly [number, number, number, number]): [number, number, number] {
const [x, y, z, w] = q;
const sinr = 2 * (w * x + y * z);
const cosr = 1 - 2 * (x * x + y * y);
const roll = Math.atan2(sinr, cosr);
const sinp = 2 * (w * y - z * x);
const pitch = Math.abs(sinp) >= 1 ? Math.sign(sinp) * Math.PI / 2 : Math.asin(sinp);
const siny = 2 * (w * z + x * y);
const cosy = 1 - 2 * (y * y + z * z);
return [roll, pitch, Math.atan2(siny, cosy)];
}
export const VIEWPORT_DEFAULT_ORBIT: Readonly<ViewportOrbitState> = Object.freeze({
yaw: -Math.PI / 4,
pitch: 0.55,
distance: 7,
target: [0, 0, 0] as [number, number, number],
});
export const VIEWPORT_ORBIT_ROTATE_SENSITIVITY = 0.008;
export const VIEWPORT_ORBIT_ZOOM_SENSITIVITY = 0.001;
export const VIEWPORT_ORBIT_MIN_DISTANCE = 0.2;
export const VIEWPORT_ORBIT_MAX_DISTANCE = 500;
export function orbitPosition(state: Pick<ViewportOrbitState, "yaw" | "pitch" | "distance" | "target">): [number, number, number] {
const horizontal = state.distance * Math.cos(state.pitch);
return [
state.target[0] + horizontal * Math.cos(state.yaw),
state.target[1] + horizontal * Math.sin(state.yaw),
state.target[2] + state.distance * Math.sin(state.pitch),
];
}
export function cameraState(state: ViewportOrbitState): ViewportCameraState {
return { ...state, target: [...state.target] as [number, number, number], position: orbitPosition(state) };
}
export function orbitStateFromPosition(position: readonly number[], target: readonly number[] = [0, 0, 0]): ViewportOrbitState {
const dx = position[0] - target[0];
const dy = position[1] - target[1];
const dz = position[2] - target[2];
const distance = Math.max(VIEWPORT_ORBIT_MIN_DISTANCE, Math.hypot(dx, dy, dz));
return {
yaw: Math.atan2(dy, dx),
pitch: Math.asin(Math.max(-1, Math.min(1, dz / distance))),
distance,
target: [target[0], target[1], target[2]],
};
}
export function applyOrbitDelta(state: ViewportOrbitState, deltaX: number, deltaY: number, zoom: number): ViewportOrbitState {
return {
...state,
yaw: state.yaw - deltaX * VIEWPORT_ORBIT_ROTATE_SENSITIVITY,
pitch: Math.max(-1.45, Math.min(1.45, state.pitch + deltaY * VIEWPORT_ORBIT_ROTATE_SENSITIVITY)),
distance: Math.max(VIEWPORT_ORBIT_MIN_DISTANCE, Math.min(VIEWPORT_ORBIT_MAX_DISTANCE, state.distance * Math.exp(zoom * VIEWPORT_ORBIT_ZOOM_SENSITIVITY))),
target: [...state.target] as [number, number, number],
};
}