feat: polish
This commit is contained in:
parent
2e6177fe74
commit
4bd927bb4e
137 changed files with 6357 additions and 137560 deletions
132
src/lib/features/workspaces/WorkspaceRenderer.svelte
Normal file
132
src/lib/features/workspaces/WorkspaceRenderer.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue