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,180 @@
import * as THREE from "three";
import { createHistoryWindowState, refreshHistoryWindow } from "./viewerHistory";
import type { HistoryWindowState, Selectable, WorldState } from "./viewerTypes";
export function openHistoryWindow(
historyWindows: HistoryWindowState[],
historyLayerEl: HTMLDivElement,
target: Selectable,
nextCounter: number,
bringToFront: (windowState: HistoryWindowState) => void,
refreshWindows: () => void,
) {
const existing = historyWindows.find((windowState) => JSON.stringify(windowState.target) === JSON.stringify(target));
if (existing) {
bringToFront(existing);
refreshWindows();
return nextCounter;
}
const windowState = createHistoryWindowState(document, target, historyWindows.length, nextCounter);
historyWindows.push(windowState);
historyLayerEl.append(windowState.root);
bringToFront(windowState);
refreshWindows();
return nextCounter;
}
export function refreshHistoryWindows(
world: WorldState | undefined,
historyWindows: HistoryWindowState[],
renderRecentEvents: (entityKind: string, entityId: string) => string,
destroyWindow: (id: string) => void,
) {
if (!world) {
return;
}
for (const windowState of [...historyWindows]) {
if (!refreshHistoryWindow(world, windowState, renderRecentEvents)) {
destroyWindow(windowState.id);
}
}
}
export function destroyHistoryWindow(
historyWindows: HistoryWindowState[],
historyWindowDragId: string | undefined,
historyWindowDragPointerId: number | undefined,
id: string,
) {
const index = historyWindows.findIndex((windowState) => windowState.id === id);
if (index < 0) {
return {
historyWindowDragId,
historyWindowDragPointerId,
};
}
const [removed] = historyWindows.splice(index, 1);
removed.root.remove();
if (historyWindowDragId === id) {
return {
historyWindowDragId: undefined,
historyWindowDragPointerId: undefined,
};
}
return {
historyWindowDragId,
historyWindowDragPointerId,
};
}
export function bringHistoryWindowToFront(windowState: HistoryWindowState, nextZIndex: number) {
windowState.root.style.zIndex = `${nextZIndex}`;
}
export function beginHistoryWindowDrag(
historyWindows: HistoryWindowState[],
historyWindowDragOffset: THREE.Vector2,
pointerId: number,
windowId: string,
clientX: number,
clientY: number,
) {
const windowState = historyWindows.find((candidate) => candidate.id === windowId);
if (!windowState) {
return {
historyWindowDragId: undefined,
historyWindowDragPointerId: undefined,
};
}
const bounds = windowState.root.getBoundingClientRect();
historyWindowDragOffset.set(clientX - bounds.left, clientY - bounds.top);
windowState.root.setPointerCapture?.(pointerId);
return {
historyWindowDragId: windowId,
historyWindowDragPointerId: pointerId,
};
}
export function updateHistoryWindowDrag(
historyWindows: HistoryWindowState[],
historyWindowDragId: string | undefined,
historyWindowDragPointerId: number | undefined,
historyWindowDragOffset: THREE.Vector2,
pointerId: number,
clientX: number,
clientY: number,
) {
if (historyWindowDragPointerId !== pointerId || !historyWindowDragId) {
return;
}
const windowState = historyWindows.find((candidate) => candidate.id === historyWindowDragId);
if (!windowState) {
return;
}
const width = windowState.root.offsetWidth;
const height = windowState.root.offsetHeight;
const left = THREE.MathUtils.clamp(clientX - historyWindowDragOffset.x, 20, window.innerWidth - width - 20);
const top = THREE.MathUtils.clamp(clientY - historyWindowDragOffset.y, 20, window.innerHeight - height - 20);
windowState.root.style.left = `${left}px`;
windowState.root.style.top = `${top}px`;
}
export function endHistoryWindowDrag(
historyWindows: HistoryWindowState[],
historyWindowDragId: string | undefined,
historyWindowDragPointerId: number | undefined,
pointerId: number,
) {
if (historyWindowDragPointerId !== pointerId || !historyWindowDragId) {
return {
historyWindowDragId,
historyWindowDragPointerId,
};
}
const windowState = historyWindows.find((candidate) => candidate.id === historyWindowDragId);
windowState?.root.releasePointerCapture?.(pointerId);
return {
historyWindowDragId: undefined,
historyWindowDragPointerId: undefined,
};
}
export async function copyTextToClipboard(text: string) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return;
} catch {
}
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "true");
textarea.style.position = "fixed";
textarea.style.top = "0";
textarea.style.left = "0";
textarea.style.width = "1px";
textarea.style.height = "1px";
textarea.style.opacity = "0";
document.body.append(textarea);
textarea.focus();
textarea.select();
try {
const copied = document.execCommand("copy");
if (!copied) {
throw new Error("execCommand copy failed");
}
} finally {
textarea.remove();
}
}