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,175 @@
import * as THREE from "three";
import {
beginHistoryWindowDrag,
bringHistoryWindowToFront,
copyTextToClipboard,
destroyHistoryWindow,
endHistoryWindowDrag,
openHistoryWindow,
refreshHistoryWindows,
updateHistoryWindowDrag,
} from "./viewerHistoryManager";
import type { HistoryWindowState, Selectable, WorldState } from "./viewerTypes";
export interface ViewerHistoryWindowContext {
historyLayerEl: HTMLDivElement;
historyWindows: HistoryWindowState[];
getWorld: () => WorldState | undefined;
getHistoryWindowCounter: () => number;
setHistoryWindowCounter: (value: number) => void;
getHistoryWindowZCounter: () => number;
setHistoryWindowZCounter: (value: number) => void;
getHistoryWindowDragId: () => string | undefined;
setHistoryWindowDragId: (value: string | undefined) => void;
getHistoryWindowDragPointerId: () => number | undefined;
setHistoryWindowDragPointerId: (value: number | undefined) => void;
historyWindowDragOffset: THREE.Vector2;
renderRecentEvents: (entityKind: string, entityId: string) => string;
}
export class ViewerHistoryWindowController {
constructor(private readonly context: ViewerHistoryWindowContext) {}
openHistoryWindow(target: Selectable) {
const nextCounter = openHistoryWindow(
this.context.historyWindows,
this.context.historyLayerEl,
target,
this.context.getHistoryWindowCounter() + 1,
(windowState) => this.bringHistoryWindowToFront(windowState),
() => this.refreshHistoryWindows(),
);
this.context.setHistoryWindowCounter(nextCounter);
}
refreshHistoryWindows() {
refreshHistoryWindows(
this.context.getWorld(),
this.context.historyWindows,
this.context.renderRecentEvents,
(id) => this.destroyHistoryWindow(id),
);
}
readonly onHistoryLayerClick = (event: MouseEvent) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const windowEl = target.closest<HTMLElement>("[data-history-window-id]");
const windowId = windowEl?.dataset.historyWindowId;
if (!windowId) {
return;
}
if (target.closest(".history-window-copy")) {
void this.copyHistoryWindowContent(windowId);
return;
}
if (target.closest(".history-window-close")) {
this.destroyHistoryWindow(windowId);
return;
}
const windowState = this.context.historyWindows.find((candidate) => candidate.id === windowId);
if (windowState) {
this.bringHistoryWindowToFront(windowState);
}
};
readonly onHistoryLayerPointerDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const windowEl = target.closest<HTMLElement>("[data-history-window-id]");
const windowId = windowEl?.dataset.historyWindowId;
if (!windowEl || !windowId) {
return;
}
const windowState = this.context.historyWindows.find((candidate) => candidate.id === windowId);
if (!windowState) {
return;
}
this.bringHistoryWindowToFront(windowState);
if (!target.closest(".history-window-header") || target.closest("button")) {
return;
}
const nextState = beginHistoryWindowDrag(
this.context.historyWindows,
this.context.historyWindowDragOffset,
event.pointerId,
windowId,
event.clientX,
event.clientY,
);
this.context.setHistoryWindowDragId(nextState.historyWindowDragId);
this.context.setHistoryWindowDragPointerId(nextState.historyWindowDragPointerId);
};
readonly onHistoryWindowPointerMove = (event: PointerEvent) => {
updateHistoryWindowDrag(
this.context.historyWindows,
this.context.getHistoryWindowDragId(),
this.context.getHistoryWindowDragPointerId(),
this.context.historyWindowDragOffset,
event.pointerId,
event.clientX,
event.clientY,
);
};
readonly onHistoryWindowPointerUp = (event: PointerEvent) => {
const nextState = endHistoryWindowDrag(
this.context.historyWindows,
this.context.getHistoryWindowDragId(),
this.context.getHistoryWindowDragPointerId(),
event.pointerId,
);
this.context.setHistoryWindowDragId(nextState.historyWindowDragId);
this.context.setHistoryWindowDragPointerId(nextState.historyWindowDragPointerId);
};
private destroyHistoryWindow(id: string) {
const nextState = destroyHistoryWindow(
this.context.historyWindows,
this.context.getHistoryWindowDragId(),
this.context.getHistoryWindowDragPointerId(),
id,
);
this.context.setHistoryWindowDragId(nextState.historyWindowDragId);
this.context.setHistoryWindowDragPointerId(nextState.historyWindowDragPointerId);
}
private async copyHistoryWindowContent(windowId: string) {
const windowState = this.context.historyWindows.find((candidate) => candidate.id === windowId);
if (!windowState?.text) {
return;
}
try {
await copyTextToClipboard(windowState.text);
windowState.copyButtonEl.textContent = "Copied";
window.setTimeout(() => {
windowState.copyButtonEl.textContent = "Copy";
}, 1200);
} catch {
windowState.copyButtonEl.textContent = "Failed";
window.setTimeout(() => {
windowState.copyButtonEl.textContent = "Copy";
}, 1200);
}
}
private bringHistoryWindowToFront(windowState: HistoryWindowState) {
const nextZIndex = this.context.getHistoryWindowZCounter() + 1;
this.context.setHistoryWindowZCounter(nextZIndex);
bringHistoryWindowToFront(windowState, nextZIndex);
}
}