Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CASCADE-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ samples/private/
# ── OS ───────────────────────────────────────────────────────────────────────
.DS_Store
Thumbs.db

# ── Generated from node_modules by scripts/copy-maplibre-worker.mjs ──────────
public/maplibre/
9 changes: 9 additions & 0 deletions CASCADE-app/components/geo/geo-map-background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ import { useCanvasStore } from "@/store/canvas-store";
import { useMapViewportSync } from "@/hooks/useMapViewportSync";
import type { GeoAnchor } from "@/lib/schemas/network";

// maplibre-gl 6 loads its tile-decoding work into a Web Worker, and under
// Next.js the worker file has to be served as a plain same-origin asset:
// `scripts/copy-maplibre-worker.mjs` puts it (and the shared chunk it imports)
// under public/maplibre/ at predev/prebuild time. Without this call the map
// mounts and reports "load", but never requests a single tile — a blank
// background with no error. Module scope, so it runs exactly once before any
// Map is constructed.
maplibregl.setWorkerUrl("/maplibre/maplibre-gl-worker.mjs");

// ---------------------------------------------------------------------------
// Ghost graph overlay — shows node/edge positions in setup mode
// ---------------------------------------------------------------------------
Expand Down
13 changes: 12 additions & 1 deletion CASCADE-app/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ import coreWebVitals from "eslint-config-next/core-web-vitals";
import typescript from "eslint-config-next/typescript";

const config = [
{ ignores: ["node_modules/**", ".next/**", "out/**", "shared/schemas/**", "next-env.d.ts"] },
// public/maplibre/** is MapLibre's worker bundle copied verbatim out of
// node_modules by scripts/copy-maplibre-worker.mjs — vendor code, not ours.
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"shared/schemas/**",
"next-env.d.ts",
"public/maplibre/**",
],
},
...coreWebVitals,
...typescript,
{
Expand Down
5 changes: 3 additions & 2 deletions CASCADE-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
"license": "AGPL-3.0-or-later",
"private": true,
"scripts": {
"predev": "npm run sync:samples",
"predev": "npm run sync:samples && npm run sync:maplibre-worker",
"dev": "next dev --turbopack",
"prebuild": "npm run sync:samples",
"prebuild": "npm run sync:samples && npm run sync:maplibre-worker",
"build": "next build",
"sync:samples": "node scripts/sync-samples.mjs",
"sync:maplibre-worker": "node scripts/copy-maplibre-worker.mjs",
"start": "next start",
"lint": "eslint .",
"type-check": "tsc --noEmit",
Expand Down
34 changes: 34 additions & 0 deletions CASCADE-app/scripts/copy-maplibre-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copy MapLibre's web worker (and the shared chunk it imports) into
* public/maplibre/ so `setWorkerUrl` can point at a real, same-origin URL.
*
* Why this exists: maplibre-gl 6 ships as ES modules only, and its worker
* imports a sibling file (`maplibre-gl-shared.mjs`) by relative path. Next.js
* — in both its Turbopack and its --webpack mode — turns the usual
* `new URL('maplibre-gl/dist/maplibre-gl-worker.mjs', import.meta.url)` trick
* into a single hashed asset WITHOUT emitting that sibling next to it. The
* worker then throws on its first import and the map mounts but never requests
* a tile: a blank background, no error in the page. Serving both files verbatim
* from public/ is the upstream-documented fix for this bundler.
*
* See: https://maplibre.org/maplibre-gl-js/docs/ → Installation → Turbopack tab.
*
* The copy runs at build time from node_modules, so it always matches the
* installed version — nothing to keep in sync by hand.
*/
import { copyFileSync, mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";

const dist = path.join(
path.dirname(createRequire(import.meta.url).resolve("maplibre-gl/package.json")),
"dist",
);
const dest = path.join(process.cwd(), "public", "maplibre");

mkdirSync(dest, { recursive: true });
// Both files, not just the worker: the worker imports the shared chunk by
// relative path, so they have to land in the same directory.
for (const file of ["maplibre-gl-worker.mjs", "maplibre-gl-shared.mjs"]) {
copyFileSync(path.join(dist, file), path.join(dest, file));
}
3 changes: 2 additions & 1 deletion docs/project/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ A georeferenced Canvas renders a MapLibre map as a **non-interactive background

- **`components/geo/geo-map-background.tsx`** — owns the MapLibre lifecycle (init, tile-style swap, interaction toggle, resize) and the setup/synced UI (style picker, "Set anchor", crosshair, debug overlay). In *setup* mode the map is fully interactive so the user can navigate and drop a **GeoAnchor**; in *synced* mode interaction is disabled and the map follows the viewport.
- **`hooks/useMapViewportSync.ts`** — the viewport-sync seam. Mirrors React Flow's transform onto the map container every frame (GPU compositor, zero lag) and reloads sharp tiles via `map.jumpTo()` only when a gesture ends. Returns `invalidate()` for style swaps.
- **`scripts/copy-maplibre-worker.mjs`** — copies MapLibre's tile-decoding worker (and the shared chunk it imports by relative path) from `node_modules` into `public/maplibre/` on `predev`/`prebuild`, so `setWorkerUrl` can point at a same-origin asset. MapLibre 6 is ESM-only, and Next.js emits the worker as a lone hashed asset without its sibling — the map then mounts and fires `load` but never requests a tile, showing a blank background with no error. This is the bundler setup upstream documents for Turbopack/Next.js.
- **`lib/geo-utils.ts`** — the **GeoAnchor projection**: exact Web Mercator (`anchorFlowToGeo`, `anchorGeoToFlow`, `computeMapTarget`). One seam converts flow ↔ geo, so `node.geo`-on-drag and the map camera can never use disagreeing projections. See CONTEXT.md → *GeoAnchor*.

A node carries both `position` (flow) and `geo` (lng/lat); see CONTEXT.md → *Node Position vs Geo Coordinates*. The GeoAnchor is the single per-Canvas correspondence tying the two.
Expand Down Expand Up @@ -566,7 +567,7 @@ CASCADE-v2/
| State | Zustand + Immer | 5 / 11 | Lightweight, immutable stores |
| Styling | Tailwind CSS | 4 | Utility-first design system |
| Validation | Zod | 4 | Runtime schema validation, type inference |
| Maps | MapLibre GL JS | 5 | Open-source map background for georeferenced Canvases |
| Maps | MapLibre GL JS | 6 | Open-source map background for georeferenced Canvases |
| API Server | FastAPI | — | High-performance async Python API |
| Backend validation | Pydantic v2 | — | Request/response schema enforcement |
| Auth | OAuth2/OIDC (provider-agnostic) | — | Identity, JWT validation |
Expand Down
Loading