Turn a place you love into something you can make. TopoStack is a browser-based terrain studio for creating layered, laser-cut reliefs and flat topographic engravings from real elevation and map data.
Visit the website · Open the studio · Report a bug or share an idea

| Workflow | Controls | Output |
|---|---|---|
| Layered relief | Physical size, material thickness, vertical exaggeration, map details, and fabrication settings | Master SVG, cut panels, matching engraving panels, and an assembly guide |
| Flat engraving | Physical size, contour density, index contours, linework, map details, and border | One SVG at physical size, containing engraving paths only |
Both workflows support rectangular and circular crops; roads, trails, transportation labels, water outlines and fill patterns; state/province boundaries; latitude/longitude grids; elevation labels; a compass; and a scale bar. Add custom coordinate-based markers, trails, and boundaries to make a map your own.
Layered projects also support modeled lake depth, alignment guides, and material reuse. Sheet count is calculated from terrain relief, map scale, vertical exaggeration, and material thickness. Preview a project on the map, as 2D cut layers, as an engraving, or as a stacked/exploded 3D model, depending on the output type.
The initial preview uses a bundled snapshot of real terrain and map data. Generate fresh terrain before fabrication export. SVGs use physical millimeter coordinates; layered artwork separates red cuts, blue scores, and black engravings. Exports include project metadata and source attribution. Review the artwork and machine settings in your laser software before making a piece.
Project settings are saved in your browser's IndexedDB. Export a project-settings JSON backup to keep a copy or move to another device; import it using the studio's import control. Restored or imported projects need fresh terrain generation before fabrication export. Settings backups are available even when fabrication export is blocked.
The homepage lives at /, the editor at /studio, and the former /about URL redirects to /. TopoStack also supports the Atomm export lifecycle and Open in Studio integration.
TopoStack is a solo developer's spare-time project under Echo Foxtrot Works, unashamedly built with help from AI. That collaboration helps turn ideas into working software and make the most of the time available.
Explore the code, ask questions, suggest improvements, or contribute through GitHub. For bugs, include the output type, selected location, reproduction steps, and browser details; a project-settings JSON file can help reproduce geometry problems. For code changes, explain the behavior you changed and how you verified it. Pull requests normally target dev; releases are promoted to main.
Donations help support development and are always optional. Every export is available without donating.
Use the Node version in .nvmrc, currently 22.22.2, to match CI. The supported runtime ranges are declared in package.json.
git clone https://github.com/Echo-Foxtrot-Works/topostack.git
cd topostack
nvm install
nvm use
npm ci
If you use another Node version manager, select the version from .nvmrc before installing dependencies. Run the following commands from the repository root.
npm run dev:web
Open the URL printed by Vite, normally localhost:5273. This is enough to work on the homepage, studio UI, and bundled preview without Cloudflare credentials. Live place search and generation require a reachable map API. If terrain loading fails, the studio can display synthetic fallback terrain, but fabrication export remains blocked.
npm run dev
This starts the API, waits for /health, then starts Vite. The default ports are 8787 for the API and 5273 for the frontend. The launcher tries the next available ports when a default is busy and connects the frontend to the chosen API port.
The local Worker simulates terrain-cache storage but reads the provisioned vector and lake archives from the remote development R2 bucket. Full data access therefore requires Wrangler authentication and access to those Cloudflare resources. For your own deployment, follow the map API setup guide and data setup below.
Place search additionally needs a Geoapify key:
cp workers/map-api/.dev.vars.example workers/map-api/.dev.vars
Set GEOCODER_API_KEY in that local file. The geocoder key is not required to fetch elevation for known coordinates. .dev.vars is ignored by Git.
| Variable | Purpose |
|---|---|
TOPOSTACK_WEB_PORT | Frontend port; defaults to 5273 |
VITE_MAP_API_PORT | Local API port; defaults to 8787 |
VITE_MAP_API_URL | Explicit API origin; overrides the local API URL. Use a reachable deployment that permits your frontend origin. |
VITE_DONATION_URL | Optional donation destination; defaults to the TopoStack PayPal page |
GEOCODER_API_KEY | Worker-only Geoapify credential; keep it in .dev.vars locally or a deployment secret |
Pass port overrides in the shell:
VITE_MAP_API_PORT=8799 TOPOSTACK_WEB_PORT=5299 npm run dev
Explicitly selected busy ports cause an error. The individual dev:web and dev:api commands honor their port variables but do not search for a free port. Set frontend API/donation overrides in apps/generator/.env or the shell; VITE_ values are included in the browser build and must not contain secrets.
| Path | Responsibility |
|---|---|
apps/generator | Svelte 5/SvelteKit homepage and studio, previews, browser storage, downloads, and Atomm integration |
packages/core | Portable TypeScript geometry engine, fabrication planning, and SVG generation |
workers/map-api | Cloudflare Worker for terrain, map archives, geocoding, caching, and readiness checks |
e2e | Deterministic Playwright tests for navigation, previews, generation, and exports |
e2e-live | Browser canary that generates and exports against a deployed API |
scripts | Development launcher, data provisioning, build budgets, and release verification |
atomm | Platform listing and cover artwork |
docs | Architecture, fabrication details, and operational runbooks |
Terrain geometry is calculated in the browser, with expensive work delegated to a Web Worker. The API streams and caches source data. MapLibre supplies the interactive reference map, and Three.js renders the 3D preview. See architecture for the geometry pipeline and coordinate conventions.
npm run lint
npm run typecheck
npm test
npm run build
npm run budget:web
npm run build builds all workspaces, including a dry run of the Worker deployment; it does not publish the app. The generated frontend is in apps/generator/dist. npm run budget:web checks that built output against separate homepage/editor JavaScript budgets, startup costs, total assets, and HTML limits.
Install browsers before running the end-to-end suite:
npx playwright install chromium firefox webkit
npm run test:e2e
The browser suite builds its own deterministic test version and covers Chromium, Firefox, and WebKit. npm run test:coverage runs the unit/component/Worker suites with the thresholds used in CI. Run the live browser canary against a deployed environment with:
PUBLIC_APP_URL=https://dev-topostack.echofoxtrot.works npm run test:e2e:live
If local lint reports files under .wrangler/tmp, exclude those generated files with npx eslint . --max-warnings=0 --ignore-pattern '**/.wrangler/**'. CI uses a clean checkout.
GitHub Actions validates pull requests targeting dev or main. Successful pushes to those branches, or manual runs on them, deploy the matching environment after quality, build, and browser checks pass.
| Branch | GitHub environment | Worker | Website |
|---|---|---|---|
dev | development | topostack-dev | Development |
main | production | topostack | Production |
Each Worker serves the static frontend and /v1/* API, with /health for liveness and /ready for data/geocoder readiness. Configure these secrets separately in both GitHub environments:
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKENGEOCODER_API_KEYRestrict development deployments to dev and production to main. Configure any production reviewer gate to work with your maintainer team. Pull-request validation does not reference deployment environments; only the deployment job receives their secrets. See the Worker guide for resource configuration and token permissions.
The hourly production monitor checks the frontend, API, and data-serving paths. The daily browser monitor generates real terrain and inspects a downloaded fabrication package. Use the release acceptance and rollback runbook for platform, physical fabrication, and recovery checks.
To preview the local studio in Atomm, use the running frontend URL as its local parameter. With the default port:
https://www.atomm.com/creativetools/community/generator/topographic-map-generator?local=http://localhost:5273/studio
Build a release against the deployed production API:
VITE_MAP_API_URL=https://topostack.echofoxtrot.works npm run release:atomm
This produces apps/generator/topostack-atomm.zip, its .zip.sha256 checksum, and topostack-atomm.release.json with the source revision, API origin, dataset/archive identities, and dirty-tree flag. Packaging requires a real HTTPS API origin and rejects local, placeholder, and *.workers.dev URLs. Use npm run package:atomm with the same API variable for the ZIP and validation without the checksum/receipt step.
After a successful production deployment and smoke test, CI uploads the ZIP, checksum, receipt, cover image, and listing as a topostack-atomm-<commit> artifact retained for 30 days. Publish from a clean commit and keep the release evidence with the data-provisioning receipts.
osm/current.pmtiles.lakes/current.pmtiles; lake basins are modeled, with per-lake depth overrides in the studio.Archive provisioning is separate from application deployment. Use provision-vector-data.mjs for the map archive, and build-lake-data.mjs plus provision-lake-data.mjs for lake data. The lake build requires Tippecanoe; archive verification requires the PMTiles CLI. The provisioning scripts require an archive path, --provision, Cloudflare credentials, and a pinned SHA-256 digest. A development-only --skip-digest-check option supports establishing a new archive pin.
Provisioning writes development by default. --prod writes both development and production, requires a pinned digest, and overwrites the configured archive keys. Retain the original archives and emitted provisioning receipts for rollback. The Worker guide documents the map archive and resource setup.
/ready requires both archives and the geocoder configuration. Fabrication export requires current real terrain and any requested map/lake data; missing or truncated requested data blocks export. The bundled preview and synthetic fallback are for previewing and development.
Terrain and map data are decorative source material, not survey, navigation, or engineering data. Exported projects include attribution; see data and fabrication notes and the maintained source-credit list.
A project LICENSE file has not been added to this repository yet. Dependency and map-data licenses are documented separately in their packages and the source credits.
80 commits
9 commits
TypeScript
63.4%
Svelte
22.9%
JavaScript
8.1%
CSS
5.6%
Turn a place you love into something you can make. TopoStack is a browser-based terrain studio for creating layered, laser-cut reliefs and flat topographic engravings from real elevation and map data.
Visit the website · Open the studio · Report a bug or share an idea

| Workflow | Controls | Output |
|---|---|---|
| Layered relief | Physical size, material thickness, vertical exaggeration, map details, and fabrication settings | Master SVG, cut panels, matching engraving panels, and an assembly guide |
| Flat engraving | Physical size, contour density, index contours, linework, map details, and border | One SVG at physical size, containing engraving paths only |
Both workflows support rectangular and circular crops; roads, trails, transportation labels, water outlines and fill patterns; state/province boundaries; latitude/longitude grids; elevation labels; a compass; and a scale bar. Add custom coordinate-based markers, trails, and boundaries to make a map your own.
Layered projects also support modeled lake depth, alignment guides, and material reuse. Sheet count is calculated from terrain relief, map scale, vertical exaggeration, and material thickness. Preview a project on the map, as 2D cut layers, as an engraving, or as a stacked/exploded 3D model, depending on the output type.
The initial preview uses a bundled snapshot of real terrain and map data. Generate fresh terrain before fabrication export. SVGs use physical millimeter coordinates; layered artwork separates red cuts, blue scores, and black engravings. Exports include project metadata and source attribution. Review the artwork and machine settings in your laser software before making a piece.
Project settings are saved in your browser's IndexedDB. Export a project-settings JSON backup to keep a copy or move to another device; import it using the studio's import control. Restored or imported projects need fresh terrain generation before fabrication export. Settings backups are available even when fabrication export is blocked.
The homepage lives at /, the editor at /studio, and the former /about URL redirects to /. TopoStack also supports the Atomm export lifecycle and Open in Studio integration.
TopoStack is a solo developer's spare-time project under Echo Foxtrot Works, unashamedly built with help from AI. That collaboration helps turn ideas into working software and make the most of the time available.
Explore the code, ask questions, suggest improvements, or contribute through GitHub. For bugs, include the output type, selected location, reproduction steps, and browser details; a project-settings JSON file can help reproduce geometry problems. For code changes, explain the behavior you changed and how you verified it. Pull requests normally target dev; releases are promoted to main.
Donations help support development and are always optional. Every export is available without donating.
Use the Node version in .nvmrc, currently 22.22.2, to match CI. The supported runtime ranges are declared in package.json.
git clone https://github.com/Echo-Foxtrot-Works/topostack.git
cd topostack
nvm install
nvm use
npm ci
If you use another Node version manager, select the version from .nvmrc before installing dependencies. Run the following commands from the repository root.
npm run dev:web
Open the URL printed by Vite, normally localhost:5273. This is enough to work on the homepage, studio UI, and bundled preview without Cloudflare credentials. Live place search and generation require a reachable map API. If terrain loading fails, the studio can display synthetic fallback terrain, but fabrication export remains blocked.
npm run dev
This starts the API, waits for /health, then starts Vite. The default ports are 8787 for the API and 5273 for the frontend. The launcher tries the next available ports when a default is busy and connects the frontend to the chosen API port.
The local Worker simulates terrain-cache storage but reads the provisioned vector and lake archives from the remote development R2 bucket. Full data access therefore requires Wrangler authentication and access to those Cloudflare resources. For your own deployment, follow the map API setup guide and data setup below.
Place search additionally needs a Geoapify key:
cp workers/map-api/.dev.vars.example workers/map-api/.dev.vars
Set GEOCODER_API_KEY in that local file. The geocoder key is not required to fetch elevation for known coordinates. .dev.vars is ignored by Git.
| Variable | Purpose |
|---|---|
TOPOSTACK_WEB_PORT | Frontend port; defaults to 5273 |
VITE_MAP_API_PORT | Local API port; defaults to 8787 |
VITE_MAP_API_URL | Explicit API origin; overrides the local API URL. Use a reachable deployment that permits your frontend origin. |
VITE_DONATION_URL | Optional donation destination; defaults to the TopoStack PayPal page |
GEOCODER_API_KEY | Worker-only Geoapify credential; keep it in .dev.vars locally or a deployment secret |
Pass port overrides in the shell:
VITE_MAP_API_PORT=8799 TOPOSTACK_WEB_PORT=5299 npm run dev
Explicitly selected busy ports cause an error. The individual dev:web and dev:api commands honor their port variables but do not search for a free port. Set frontend API/donation overrides in apps/generator/.env or the shell; VITE_ values are included in the browser build and must not contain secrets.
| Path | Responsibility |
|---|---|
apps/generator | Svelte 5/SvelteKit homepage and studio, previews, browser storage, downloads, and Atomm integration |
packages/core | Portable TypeScript geometry engine, fabrication planning, and SVG generation |
workers/map-api | Cloudflare Worker for terrain, map archives, geocoding, caching, and readiness checks |
e2e | Deterministic Playwright tests for navigation, previews, generation, and exports |
e2e-live | Browser canary that generates and exports against a deployed API |
scripts | Development launcher, data provisioning, build budgets, and release verification |
atomm | Platform listing and cover artwork |
docs | Architecture, fabrication details, and operational runbooks |
Terrain geometry is calculated in the browser, with expensive work delegated to a Web Worker. The API streams and caches source data. MapLibre supplies the interactive reference map, and Three.js renders the 3D preview. See architecture for the geometry pipeline and coordinate conventions.
npm run lint
npm run typecheck
npm test
npm run build
npm run budget:web
npm run build builds all workspaces, including a dry run of the Worker deployment; it does not publish the app. The generated frontend is in apps/generator/dist. npm run budget:web checks that built output against separate homepage/editor JavaScript budgets, startup costs, total assets, and HTML limits.
Install browsers before running the end-to-end suite:
npx playwright install chromium firefox webkit
npm run test:e2e
The browser suite builds its own deterministic test version and covers Chromium, Firefox, and WebKit. npm run test:coverage runs the unit/component/Worker suites with the thresholds used in CI. Run the live browser canary against a deployed environment with:
PUBLIC_APP_URL=https://dev-topostack.echofoxtrot.works npm run test:e2e:live
If local lint reports files under .wrangler/tmp, exclude those generated files with npx eslint . --max-warnings=0 --ignore-pattern '**/.wrangler/**'. CI uses a clean checkout.
GitHub Actions validates pull requests targeting dev or main. Successful pushes to those branches, or manual runs on them, deploy the matching environment after quality, build, and browser checks pass.
| Branch | GitHub environment | Worker | Website |
|---|---|---|---|
dev | development | topostack-dev | Development |
main | production | topostack | Production |
Each Worker serves the static frontend and /v1/* API, with /health for liveness and /ready for data/geocoder readiness. Configure these secrets separately in both GitHub environments:
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKENGEOCODER_API_KEYRestrict development deployments to dev and production to main. Configure any production reviewer gate to work with your maintainer team. Pull-request validation does not reference deployment environments; only the deployment job receives their secrets. See the Worker guide for resource configuration and token permissions.
The hourly production monitor checks the frontend, API, and data-serving paths. The daily browser monitor generates real terrain and inspects a downloaded fabrication package. Use the release acceptance and rollback runbook for platform, physical fabrication, and recovery checks.
To preview the local studio in Atomm, use the running frontend URL as its local parameter. With the default port:
https://www.atomm.com/creativetools/community/generator/topographic-map-generator?local=http://localhost:5273/studio
Build a release against the deployed production API:
VITE_MAP_API_URL=https://topostack.echofoxtrot.works npm run release:atomm
This produces apps/generator/topostack-atomm.zip, its .zip.sha256 checksum, and topostack-atomm.release.json with the source revision, API origin, dataset/archive identities, and dirty-tree flag. Packaging requires a real HTTPS API origin and rejects local, placeholder, and *.workers.dev URLs. Use npm run package:atomm with the same API variable for the ZIP and validation without the checksum/receipt step.
After a successful production deployment and smoke test, CI uploads the ZIP, checksum, receipt, cover image, and listing as a topostack-atomm-<commit> artifact retained for 30 days. Publish from a clean commit and keep the release evidence with the data-provisioning receipts.
osm/current.pmtiles.lakes/current.pmtiles; lake basins are modeled, with per-lake depth overrides in the studio.Archive provisioning is separate from application deployment. Use provision-vector-data.mjs for the map archive, and build-lake-data.mjs plus provision-lake-data.mjs for lake data. The lake build requires Tippecanoe; archive verification requires the PMTiles CLI. The provisioning scripts require an archive path, --provision, Cloudflare credentials, and a pinned SHA-256 digest. A development-only --skip-digest-check option supports establishing a new archive pin.
Provisioning writes development by default. --prod writes both development and production, requires a pinned digest, and overwrites the configured archive keys. Retain the original archives and emitted provisioning receipts for rollback. The Worker guide documents the map archive and resource setup.
/ready requires both archives and the geocoder configuration. Fabrication export requires current real terrain and any requested map/lake data; missing or truncated requested data blocks export. The bundled preview and synthetic fallback are for previewing and development.
Terrain and map data are decorative source material, not survey, navigation, or engineering data. Exported projects include attribution; see data and fabrication notes and the maintained source-credit list.
A project LICENSE file has not been added to this repository yet. Dependency and map-data licenses are documented separately in their packages and the source credits.
80 commits
9 commits
TypeScript
63.4%
Svelte
22.9%
JavaScript
8.1%
CSS
5.6%