From ea2927fb8c52751d23b6dc540a65ba1de42cb8e2 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 31 Mar 2026 04:23:06 +0200 Subject: [PATCH] Add viewport focus logic --- src/viewport-three/viewport-focus.ts | 170 +++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 src/viewport-three/viewport-focus.ts diff --git a/src/viewport-three/viewport-focus.ts b/src/viewport-three/viewport-focus.ts new file mode 100644 index 00000000..9167883e --- /dev/null +++ b/src/viewport-three/viewport-focus.ts @@ -0,0 +1,170 @@ +import { getSingleSelectedBrushId, getSingleSelectedEntityId, type EditorSelection } from "../core/selection"; +import type { Vec3 } from "../core/vector"; +import type { BoxBrush } from "../document/brushes"; +import type { SceneDocument } from "../document/scene-document"; + +const PLAYER_START_FOCUS_HALF_EXTENTS: Vec3 = { + x: 0.35, + y: 0.3, + z: 0.55 +}; + +interface FocusBoundsAccumulator { + min: Vec3; + max: Vec3; +} + +export interface ViewportFocusTarget { + center: Vec3; + radius: number; +} + +function createEmptyBoundsAccumulator(): FocusBoundsAccumulator { + return { + min: { + x: Number.POSITIVE_INFINITY, + y: Number.POSITIVE_INFINITY, + z: Number.POSITIVE_INFINITY + }, + max: { + x: Number.NEGATIVE_INFINITY, + y: Number.NEGATIVE_INFINITY, + z: Number.NEGATIVE_INFINITY + } + }; +} + +function includeBounds(bounds: FocusBoundsAccumulator, min: Vec3, max: Vec3) { + bounds.min.x = Math.min(bounds.min.x, min.x); + bounds.min.y = Math.min(bounds.min.y, min.y); + bounds.min.z = Math.min(bounds.min.z, min.z); + bounds.max.x = Math.max(bounds.max.x, max.x); + bounds.max.y = Math.max(bounds.max.y, max.y); + bounds.max.z = Math.max(bounds.max.z, max.z); +} + +function finishBounds(bounds: FocusBoundsAccumulator): ViewportFocusTarget | null { + if (!Number.isFinite(bounds.min.x) || !Number.isFinite(bounds.max.x)) { + return null; + } + + const center = { + x: (bounds.min.x + bounds.max.x) * 0.5, + y: (bounds.min.y + bounds.max.y) * 0.5, + z: (bounds.min.z + bounds.max.z) * 0.5 + }; + const radius = Math.max( + 0.5, + Math.hypot(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y, bounds.max.z - bounds.min.z) * 0.5 + ); + + return { + center, + radius + }; +} + +function createBrushFocusTarget(brush: BoxBrush): ViewportFocusTarget { + return { + center: { + ...brush.center + }, + radius: Math.max(0.5, Math.hypot(brush.size.x, brush.size.y, brush.size.z) * 0.5) + }; +} + +function createPlayerStartFocusTarget(position: Vec3): ViewportFocusTarget { + return { + center: { + x: position.x, + y: position.y + PLAYER_START_FOCUS_HALF_EXTENTS.y, + z: position.z + }, + radius: Math.max( + 0.45, + Math.hypot( + PLAYER_START_FOCUS_HALF_EXTENTS.x, + PLAYER_START_FOCUS_HALF_EXTENTS.y, + PLAYER_START_FOCUS_HALF_EXTENTS.z + ) + ) + }; +} + +function includeBrush(bounds: FocusBoundsAccumulator, brush: BoxBrush) { + const halfSize = { + x: brush.size.x * 0.5, + y: brush.size.y * 0.5, + z: brush.size.z * 0.5 + }; + + includeBounds( + bounds, + { + x: brush.center.x - halfSize.x, + y: brush.center.y - halfSize.y, + z: brush.center.z - halfSize.z + }, + { + x: brush.center.x + halfSize.x, + y: brush.center.y + halfSize.y, + z: brush.center.z + halfSize.z + } + ); +} + +function includePlayerStart(bounds: FocusBoundsAccumulator, position: Vec3) { + includeBounds( + bounds, + { + x: position.x - PLAYER_START_FOCUS_HALF_EXTENTS.x, + y: position.y, + z: position.z - PLAYER_START_FOCUS_HALF_EXTENTS.z + }, + { + x: position.x + PLAYER_START_FOCUS_HALF_EXTENTS.x, + y: position.y + PLAYER_START_FOCUS_HALF_EXTENTS.y * 2, + z: position.z + PLAYER_START_FOCUS_HALF_EXTENTS.z + } + ); +} + +function getSceneFocusTarget(document: SceneDocument): ViewportFocusTarget | null { + const bounds = createEmptyBoundsAccumulator(); + + for (const brush of Object.values(document.brushes)) { + includeBrush(bounds, brush); + } + + for (const entity of Object.values(document.entities)) { + if (entity.kind === "playerStart") { + includePlayerStart(bounds, entity.position); + } + } + + return finishBounds(bounds); +} + +export function resolveViewportFocusTarget(document: SceneDocument, selection: EditorSelection): ViewportFocusTarget | null { + const selectedBrushId = getSingleSelectedBrushId(selection); + + if (selectedBrushId !== null) { + const brush = document.brushes[selectedBrushId]; + + if (brush !== undefined && brush.kind === "box") { + return createBrushFocusTarget(brush); + } + } + + const selectedEntityId = getSingleSelectedEntityId(selection); + + if (selectedEntityId !== null) { + const entity = document.entities[selectedEntityId]; + + if (entity?.kind === "playerStart") { + return createPlayerStartFocusTarget(entity.position); + } + } + + return getSceneFocusTarget(document); +}