feat: polish

This commit is contained in:
Anatoly Antonov 2026-04-22 01:27:38 +09:00
parent 2e6177fe74
commit 4bd927bb4e
137 changed files with 6357 additions and 137560 deletions

View file

@ -0,0 +1,132 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { getMap, plotPrediction, plotAnimatedMarker } from '$map';
import { timelineStore } from '$features/timeline/store';
import { workspacesStore } from './store';
import type { Workspace } from './types';
import type { LatLngTuple } from '$domain';
/**
* Renders every workspace onto the shared map. Each workspace gets its own
* named scene (`ws/<id>`) so its layers can be cleared independently, plus
* a `cursor/<id>` scene for the animated playback marker.
*
* The component is placed as a child of <Map /> so `getMap()` returns a
* non-null instance via context.
*/
const map = getMap();
if (!map) throw new Error('WorkspaceRenderer must be a descendant of <Map />');
// Track the scenes we currently own so we can dispose the ones that
// belong to workspaces which were removed from the store since last tick.
const ownedPlotScenes = new Set<string>();
const ownedCursorScenes = new Set<string>();
const sceneName = (w: Workspace) => `ws/${w.id}`;
const cursorName = (w: Workspace) => `cursor/${w.id}`;
function updateGlobalRange(items: Workspace[]) {
let min = Infinity;
let max = -Infinity;
for (const w of items) {
if (!w.visible || !w.result) continue;
const l = w.result.launch.datetime.getTime();
const r = w.result.landing.datetime.getTime();
if (l < min) min = l;
if (r > max) max = r;
}
if (!isFinite(min) || !isFinite(max)) {
timelineStore.setRange(0, 0);
} else {
timelineStore.setRange(min, max);
}
}
function renderAll(items: Workspace[]) {
if (!map) return;
const live = new Set<string>();
for (const w of items) {
const name = sceneName(w);
live.add(name);
if (!w.visible || !w.result) {
if (ownedPlotScenes.has(name)) {
map.disposeScene(name);
ownedPlotScenes.delete(name);
}
continue;
}
const scene = map.scene(name);
plotPrediction(scene, w.result, { color: w.color, opacity: w.opacity });
ownedPlotScenes.add(name);
}
for (const name of Array.from(ownedPlotScenes)) {
if (!live.has(name)) {
map.disposeScene(name);
ownedPlotScenes.delete(name);
}
}
}
function positionAt(path: LatLngTuple[], time: number, launchMs: number, landingMs: number) {
if (path.length === 0) return null;
if (launchMs === landingMs) return path[0];
const t = Math.max(0, Math.min(1, (time - launchMs) / (landingMs - launchMs)));
const idx = Math.min(path.length - 1, Math.floor(t * (path.length - 1)));
return path[idx];
}
function renderCursors(items: Workspace[], time: number) {
if (!map) return;
const live = new Set<string>();
for (const w of items) {
const name = cursorName(w);
live.add(name);
if (!w.visible || !w.result) {
if (ownedCursorScenes.has(name)) {
map.disposeScene(name);
ownedCursorScenes.delete(name);
}
continue;
}
const p = positionAt(
w.result.flight_path,
time,
w.result.launch.datetime.getTime(),
w.result.landing.datetime.getTime(),
);
if (!p) continue;
const scene = map.scene(name);
plotAnimatedMarker(scene, p[1], p[0]);
ownedCursorScenes.add(name);
}
for (const name of Array.from(ownedCursorScenes)) {
if (!live.has(name)) {
map.disposeScene(name);
ownedCursorScenes.delete(name);
}
}
}
$effect(() => {
const items = $workspacesStore.items;
renderAll(items);
updateGlobalRange(items);
});
$effect(() => {
renderCursors($workspacesStore.items, $timelineStore.time);
});
onDestroy(() => {
if (!map) return;
for (const name of ownedPlotScenes) map.disposeScene(name);
for (const name of ownedCursorScenes) map.disposeScene(name);
ownedPlotScenes.clear();
ownedCursorScenes.clear();
});
</script>