Refactor simulation and viewer architecture

This commit is contained in:
2026-03-14 15:08:49 -04:00
parent ddca4a16d5
commit 651556c916
71 changed files with 11472 additions and 9031 deletions

View File

@@ -0,0 +1,131 @@
import * as THREE from "three";
import { getSelectionGroup } from "./viewerSelection";
import type { Selectable, SelectionGroup, WorldState } from "./viewerTypes";
export function pickSelectableAtClientPosition(
renderer: THREE.WebGLRenderer,
raycaster: THREE.Raycaster,
mouse: THREE.Vector2,
camera: THREE.Camera,
selectableTargets: Map<THREE.Object3D, Selectable>,
clientX: number,
clientY: number,
) {
const bounds = renderer.domElement.getBoundingClientRect();
mouse.x = ((clientX - bounds.left) / bounds.width) * 2 - 1;
mouse.y = -(((clientY - bounds.top) / bounds.height) * 2 - 1);
raycaster.setFromCamera(mouse, camera);
const hit = raycaster.intersectObjects([...selectableTargets.keys()], false)[0];
return hit ? selectableTargets.get(hit.object) : undefined;
}
export function updateHoverLabel(params: {
dragMode?: string;
hoverLabelEl: HTMLDivElement;
selection: Selectable | undefined;
activeSystemId?: string;
world?: WorldState;
point: THREE.Vector2;
}) {
const {
dragMode,
hoverLabelEl,
selection,
activeSystemId,
world,
point,
} = params;
if (dragMode) {
hoverLabelEl.hidden = true;
return;
}
if (!selection || selection.kind !== "system" || selection.id === activeSystemId) {
hoverLabelEl.hidden = true;
return;
}
const system = world?.systems.get(selection.id);
if (!system) {
hoverLabelEl.hidden = true;
return;
}
hoverLabelEl.hidden = false;
hoverLabelEl.textContent = system.label;
hoverLabelEl.style.left = `${point.x + 14}px`;
hoverLabelEl.style.top = `${point.y + 14}px`;
}
export function updateMarqueeBox(
marqueeEl: HTMLDivElement,
dragStart: THREE.Vector2,
dragLast: THREE.Vector2,
) {
const minX = Math.min(dragStart.x, dragLast.x);
const minY = Math.min(dragStart.y, dragLast.y);
const maxX = Math.max(dragStart.x, dragLast.x);
const maxY = Math.max(dragStart.y, dragLast.y);
marqueeEl.style.left = `${minX}px`;
marqueeEl.style.top = `${minY}px`;
marqueeEl.style.width = `${maxX - minX}px`;
marqueeEl.style.height = `${maxY - minY}px`;
}
export function hideMarqueeBox(marqueeEl: HTMLDivElement) {
marqueeEl.style.display = "none";
marqueeEl.style.width = "0";
marqueeEl.style.height = "0";
}
export function completeMarqueeSelection(params: {
renderer: THREE.WebGLRenderer;
camera: THREE.Camera;
dragStart: THREE.Vector2;
dragLast: THREE.Vector2;
selectableTargets: Map<THREE.Object3D, Selectable>;
}) {
const {
renderer,
camera,
dragStart,
dragLast,
selectableTargets,
} = params;
const bounds = renderer.domElement.getBoundingClientRect();
const minX = Math.min(dragStart.x, dragLast.x);
const minY = Math.min(dragStart.y, dragLast.y);
const maxX = Math.max(dragStart.x, dragLast.x);
const maxY = Math.max(dragStart.y, dragLast.y);
const grouped = new Map<SelectionGroup, Selectable[]>();
for (const [object, selectable] of selectableTargets.entries()) {
if (object instanceof THREE.Sprite && !object.visible) {
continue;
}
if (!object.visible) {
continue;
}
const worldPosition = new THREE.Vector3();
object.getWorldPosition(worldPosition);
worldPosition.project(camera);
const screenX = ((worldPosition.x + 1) * 0.5) * bounds.width;
const screenY = ((1 - worldPosition.y) * 0.5) * bounds.height;
if (screenX < minX || screenX > maxX || screenY < minY || screenY > maxY) {
continue;
}
const group = getSelectionGroup(selectable);
const list = grouped.get(group) ?? [];
if (!list.some((entry) => JSON.stringify(entry) === JSON.stringify(selectable))) {
list.push(selectable);
}
grouped.set(group, list);
}
return [...grouped.entries()]
.sort((left, right) => right[1].length - left[1].length)[0]?.[1] ?? [];
}