feat(site): link the live demo + serve the gerber demo from the versioned CDN
Refer to demo.pcbjam.com from the landing: an accent "Live demo" link in the header nav, an "Open the live editor" hero link, a note in the gerber section, and a "Try the live demo now" line in the final CTA. Rework the embedded gerber viewer (public/gerber-demo/boot.js) to source the WASM from the deploy pipeline's versioned CDN instead of the hand-synced assets.pcbjam.com bucket: resolve gerbview's content-addressed folder at runtime from the release manifest (manifest-latest -> tag -> manifest-<tag> -> gerbview), load all assets (glue + wasm + images.tar.gz) from cdn.pcbjam.com, and load the cross-origin pthread worker via a same-origin blob importScripts shim (mirrors web/standalone/src/wasm/boot.ts). Bump the config-seed KICAD_VERSION_DIR 9.99 -> 10.0 to match the deployed build. Drop the now-obsolete committed glue mirror and the old assets.pcbjam.com sync/r2-deploy scripts. Collapse the header nav to the hamburger below 1025px — the added demo link no longer fits the 1024px-capped bar on a single row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13551f4f22
commit
9b41d7321f
15 changed files with 407 additions and 17539 deletions
7
site/.gitignore
vendored
7
site/.gitignore
vendored
|
|
@ -2,13 +2,6 @@
|
|||
dist/
|
||||
.vercel/
|
||||
|
||||
# Gerber-viewer demo WASM (synced from /output by scripts/sync-demo-wasm.sh).
|
||||
# The big binaries are served from Cloudflare R2 in production — never commit /
|
||||
# ship them to Vercel. The small glue JS (wx.js, wx-dom.js, gerbview.js) MUST be
|
||||
# committed: it's served same-origin (the pthread worker can't be cross-origin).
|
||||
public/gerber-demo/wasm/gerbview.wasm
|
||||
public/gerber-demo/wasm/kicad-resources.bin
|
||||
|
||||
# generated content collection types & cache
|
||||
.astro/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +1,65 @@
|
|||
# Gerber viewer blog demo
|
||||
# Gerber viewer demo
|
||||
|
||||
KiCad's `gerbview` compiled to WebAssembly, embedded (lazily) in the blog post.
|
||||
Click → it streams the WASM and renders the bundled tiny_tapeout board in-browser.
|
||||
KiCad's `gerbview` compiled to WebAssembly, embedded (lazily) in the landing page
|
||||
and the blog post. Click → it streams the WASM and renders the bundled
|
||||
tiny_tapeout board in-browser.
|
||||
|
||||
The WASM is **not** kept here — `boot.js` loads it from the versioned CDN
|
||||
(`cdn.pcbjam.com`), the same artifacts the demo/app deploy publishes. It resolves
|
||||
gerbview's immutable, content-addressed folder at runtime from the release
|
||||
manifest, so this page always shows the **latest deployed** gerbview with no
|
||||
manual sync:
|
||||
|
||||
```
|
||||
manifest-latest.json -> { tag }
|
||||
manifest-<tag>.json -> tools.gerbview -> <ver>
|
||||
base = https://cdn.pcbjam.com/wasm/gerbview/<ver>
|
||||
```
|
||||
|
||||
See `docs/features/demo-deploy/0001-wasm-cdn-versioning.md` (in `pcbjam-private`)
|
||||
for the CDN layout, manifest shapes, and header matrix.
|
||||
|
||||
## Files here (`site/public/gerber-demo/`)
|
||||
|
||||
| Path | What it does |
|
||||
|------|--------------|
|
||||
| `index.html` | The iframe target — minimal page with the `#main-window` / `#window-container` the WASM needs. |
|
||||
| `boot.js` | Boot harness: configures Emscripten `Module`, seeds KiCad config, preloads the board into MEMFS, auto-opens it via `Module.arguments`, and injects `wx.js → wx-dom.js → gerbview.js`. Holds `R2_BASE` + the dev/prod asset-base switch. |
|
||||
| `index.html` | The iframe/standalone target — minimal page with the `#main-window` / `#window-container` the WASM needs. |
|
||||
| `boot.js` | Boot harness: resolves gerbview's CDN folder from the manifest, configures Emscripten `Module`, seeds KiCad config, preloads the board into MEMFS, auto-opens it via `Module.arguments`, and injects `wx.js → wx-dom.js → gerbview.js` from the CDN. |
|
||||
| `board/` | The tiny_tapeout Gerber layers (committed) the demo opens. |
|
||||
| `poster.png` | Static fallback shown to browsers that can't run the live viewer. |
|
||||
| `wasm/` | `wx.js`/`wx-dom.js`/`gerbview.js` (committed, served same-origin) + `gerbview.wasm`/`kicad-resources.bin` (git-ignored; served from R2 in prod, local in dev). Synced from `/output`. |
|
||||
|
||||
The folder is cross-origin to this page (which is COEP `require-corp`); the CDN
|
||||
sends `Cross-Origin-Resource-Policy: cross-origin` + `Access-Control-Allow-Origin: *`,
|
||||
and the cross-origin pthread worker is loaded via a same-origin `blob:`
|
||||
`importScripts` shim (`new Worker(<cross-origin URL>)` is a SecurityError). This
|
||||
mirrors the standalone editor's `web/standalone/src/wasm/boot.ts`.
|
||||
|
||||
## Related pieces (elsewhere in `site/`)
|
||||
|
||||
| Path | What it does |
|
||||
|------|--------------|
|
||||
| `src/components/GerberDemo.astro` | The embed: lazy click-to-load iframe, cross-origin-isolation reload guard, feature-detect + poster fallback. |
|
||||
| `src/content/blog/porting-kicad-graphics-to-webgl-with-claude.mdx` | The post that renders `<GerberDemo />`. |
|
||||
| `src/sections/GerberDemoSection.astro` | The landing-page showcase: a poster + launch button that opens `/gerber-demo/` in a new tab (the landing itself is not cross-origin isolated). |
|
||||
| `src/components/GerberDemo.astro` | The blog embed: lazy click-to-load iframe, cross-origin-isolation reload guard, feature-detect + poster fallback. |
|
||||
| `astro.config.mjs` + `src/middleware.ts` | Dev cross-origin-isolation headers (COOP/COEP `require-corp`). |
|
||||
| `vercel.json` | Prod COOP/COEP, scoped to the post + `/gerber-demo/` routes. |
|
||||
| `scripts/sync-demo-wasm.sh` | Copy fresh WASM from `/output` into `wasm/` (run after a gerbview rebuild). |
|
||||
| `scripts/r2-deploy.sh` + `scripts/r2-cors.json` | Upload the heavy binaries to Cloudflare R2 (`pcbjam-assets` → `assets.pcbjam.com`) and set CORS. |
|
||||
| `vercel.json` | Prod COOP/COEP, scoped to the blog post + `/gerber-demo/` routes. |
|
||||
|
||||
## Run / update
|
||||
## Dev overrides
|
||||
|
||||
```bash
|
||||
# local dev (serves wasm/ from the local mirror)
|
||||
scripts/sync-demo-wasm.sh && npm run dev # /blog/porting-kicad-graphics-to-webgl-with-claude
|
||||
`boot.js` reads query params so you can point it elsewhere without a rebuild:
|
||||
|
||||
# after a new gerbview build: refresh local mirror, then push binaries to R2
|
||||
scripts/sync-demo-wasm.sh && scripts/r2-deploy.sh
|
||||
```
|
||||
| Param | Effect |
|
||||
|-------|--------|
|
||||
| `?tag=<tag>` | Pin a specific release instead of following `manifest-latest.json`. |
|
||||
| `?cdn=<root>` | Swap the CDN root (e.g. a local mirror serving `manifest-*.json` + `gerbview/<ver>/`). |
|
||||
| `?base=<folder>` | Use a tool folder verbatim (e.g. a fresh local build) — skips manifest resolution. |
|
||||
|
||||
The live viewer needs SharedArrayBuffer + WebGL2 (Chrome/Edge/Firefox, Safari 15.2+);
|
||||
other browsers get `poster.png`.
|
||||
other browsers get `poster.png`.
|
||||
|
||||
## Updating `KICAD_VERSION_DIR`
|
||||
|
||||
`boot.js` seeds KiCad config under a version dir (currently `"10.0"`) to suppress
|
||||
the first-run wizard. It **must** match the deployed build's
|
||||
`GetMajorMinorVersion()`. If a future deploy bumps KiCad's major.minor, update the
|
||||
`KICAD_VERSION_DIR` constant in `boot.js` (same coupling as
|
||||
`web/standalone/src/wasm/constants.ts`).
|
||||
|
|
|
|||
|
|
@ -1,44 +1,51 @@
|
|||
/*
|
||||
* Self-contained boot harness for the KiCad Gerber viewer (gerbview) WASM,
|
||||
* embedded in the blog post via an <iframe>. Adapted from the proven test
|
||||
* harness (tests/apps/kicad/gerbview.html) and the React port
|
||||
* (web/standalone/src/wasm/boot.ts) — same global `Module`, same preRun steps,
|
||||
* same wx.js -> wx-dom.js -> gerbview.js injection order.
|
||||
* embedded in the landing page / blog post via an <iframe>. A faithful port of
|
||||
* the standalone React boot (web/standalone/src/wasm/boot.ts) — same global
|
||||
* `Module`, same preRun steps, same wx.js -> wx-dom.js -> gerbview.js order.
|
||||
*
|
||||
* Classic script (NOT a module): the non-modularized WASM glue reads a GLOBAL
|
||||
* `var Module` and a GLOBAL `mainWindow`, and publishes `FS` onto the global
|
||||
* scope. Top-level `var`/`const` here share that global scope.
|
||||
* scope. `window.Module = ...` is what makes the glue see our config.
|
||||
*
|
||||
* Asset layout, split because the pthread worker script MUST be same-origin
|
||||
* (a Worker can't be created from a cross-origin URL). So the small glue stays
|
||||
* on the site origin and only the big binaries move to Cloudflare R2:
|
||||
* GLUE_BASE : wx.js, wx-dom.js, gerbview.js, mainScriptUrlOrBlob (same-origin)
|
||||
* BINARY_BASE : gerbview.wasm, kicad-resources.bin (R2 in prod, local in dev)
|
||||
* ── Assets come from the versioned CDN (cdn.pcbjam.com) — the SAME artifacts the
|
||||
* demo/app deploy publishes, not a hand-synced copy. We resolve gerbview's
|
||||
* immutable, content-addressed folder at runtime from the release manifest:
|
||||
*
|
||||
* BINARY_BASE auto-switches by hostname: localhost → the local mirror (./wasm),
|
||||
* any other host → R2_BASE below. Override either base with ?glue=/?bin=.
|
||||
* To point at R2, set R2_BASE to your bucket's public URL — either a custom
|
||||
* domain (https://assets.pcbjam.com) or the managed https://<id>.r2.dev URL.
|
||||
* manifest-latest.json -> { tag }
|
||||
* manifest-<tag>.json -> tools.gerbview -> <ver>
|
||||
* base = <CDN_ROOT>/gerbview/<ver>
|
||||
*
|
||||
* so the landing always shows the LATEST deployed gerbview with no manual sync.
|
||||
* A tool folder is self-contained + ABI-matched: gerbview.wasm, gerbview.js,
|
||||
* wx.js, wx-dom.js, images.tar.gz. See docs/features/demo-deploy/0001-*.
|
||||
*
|
||||
* The folder is CROSS-ORIGIN to this page (which is COEP `require-corp`); the CDN
|
||||
* sends `Cross-Origin-Resource-Policy: cross-origin` + `Access-Control-Allow-Origin: *`
|
||||
* so the <script>/fetch loads are permitted. The one thing a cross-origin base
|
||||
* breaks is the pthread worker — `new Worker(<cross-origin URL>)` is a
|
||||
* SecurityError — so `mainScriptUrlOrBlob` is a SAME-ORIGIN `blob:` worker that
|
||||
* `importScripts()` the cross-origin glue (a blob URL inherits the page origin;
|
||||
* a classic worker's importScripts is allowed cross-origin under CORP).
|
||||
*
|
||||
* Dev overrides (query string): ?base=<folder> uses that folder verbatim (e.g. a
|
||||
* local build); ?tag=<tag> pins a specific release; ?cdn=<root> swaps the CDN root.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// R2 bucket public URL (no trailing slash). Custom domain for the
|
||||
// pcbjam-assets bucket (the managed URL https://pub-cecc0239e6f74d99ba7d06630bd87c64.r2.dev
|
||||
// also still works).
|
||||
var R2_BASE = "https://assets.pcbjam.com";
|
||||
// Versioned WASM CDN root (no trailing slash). Overridable with ?cdn=.
|
||||
var CDN_ROOT = "https://cdn.pcbjam.com/wasm";
|
||||
|
||||
var params = new URLSearchParams(location.search);
|
||||
var isLocal = /^(localhost|127\.0\.0\.1|\[?::1\]?)$/.test(location.hostname);
|
||||
var GLUE_BASE = (params.get("glue") || "./wasm").replace(/\/+$/, "");
|
||||
var BINARY_BASE = (params.get("bin") || (isLocal ? "./wasm" : R2_BASE)).replace(
|
||||
/\/+$/,
|
||||
""
|
||||
);
|
||||
|
||||
// KiCad paths baked into the WASM build (see web/standalone/src/wasm/constants.ts).
|
||||
var KICAD_VERSION_DIR = "9.99";
|
||||
// KICAD_VERSION_DIR MUST match the deployed build's GetMajorMinorVersion() — the
|
||||
// config we seed to suppress the first-run wizard is only read from THIS dir.
|
||||
// The KiCad 10.0.x rebase bumped it from "9.99" to "10.0"; bump it here if a
|
||||
// future deploy changes KiCad's major.minor.
|
||||
var KICAD_VERSION_DIR = "10.0";
|
||||
var KICAD_CONFIG_DIR =
|
||||
"/home/kicad/.config/kicad/kicad/" + KICAD_VERSION_DIR;
|
||||
var RESOURCE_PATH =
|
||||
|
|
@ -99,49 +106,35 @@
|
|||
var mainWindow = document.getElementById("main-window");
|
||||
window.mainWindow = mainWindow;
|
||||
|
||||
// ── Prefetch the heavy, non-wasm assets in parallel with the wasm download ──
|
||||
// images.tar.gz (compiled-in KiCad resources) and the board layers. Both are
|
||||
// ~10x smaller than gerbview.wasm, so they land before preRun runs; a run
|
||||
// dependency guards the rare case where the wasm wins the race.
|
||||
var resourceData = null;
|
||||
// Served without a .gz extension on purpose (see sync-demo-wasm.sh) so no
|
||||
// server adds Content-Encoding: gzip. Written into MEMFS as images.tar.gz.
|
||||
fetch(BINARY_BASE + "/kicad-resources.bin")
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.arrayBuffer();
|
||||
})
|
||||
.then(function (buf) {
|
||||
resourceData = new Uint8Array(buf);
|
||||
console.log("[KICAD] prefetched images.tar.gz (" + resourceData.length + " bytes)");
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.warn("[KICAD] images.tar.gz prefetch failed:", err.message);
|
||||
// ── Resolve gerbview's versioned CDN folder from the release manifest ────────
|
||||
function fetchJson(url) {
|
||||
// manifest-*.json are served `no-store` so a rollback takes effect next load.
|
||||
return fetch(url, { cache: "no-store" }).then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status + " for " + url);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
var boardData = null; // { name: Uint8Array }
|
||||
var boardPromise = Promise.all(
|
||||
BOARD_FILES.map(function (name) {
|
||||
return fetch("./board/" + name)
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status + " for " + name);
|
||||
return r.arrayBuffer();
|
||||
})
|
||||
.then(function (buf) {
|
||||
return [name, new Uint8Array(buf)];
|
||||
function resolveBase() {
|
||||
var override = params.get("base");
|
||||
if (override) return Promise.resolve(override.replace(/\/+$/, ""));
|
||||
var root = (params.get("cdn") || CDN_ROOT).replace(/\/+$/, "");
|
||||
var tagParam = params.get("tag");
|
||||
var tagP = tagParam
|
||||
? Promise.resolve(tagParam)
|
||||
: fetchJson(root + "/manifest-latest.json").then(function (m) {
|
||||
if (!m || !m.tag) throw new Error("manifest-latest.json has no tag");
|
||||
return m.tag;
|
||||
});
|
||||
})
|
||||
)
|
||||
.then(function (pairs) {
|
||||
boardData = {};
|
||||
pairs.forEach(function (p) {
|
||||
boardData[p[0]] = p[1];
|
||||
return tagP.then(function (tag) {
|
||||
return fetchJson(root + "/manifest-" + tag + ".json").then(function (m) {
|
||||
var ver = m && m.tools && m.tools.gerbview;
|
||||
if (!ver) throw new Error("no gerbview version in manifest-" + tag);
|
||||
console.log("[KICAD] gerbview " + ver + " (release " + tag + ")");
|
||||
return root + "/gerbview/" + ver;
|
||||
});
|
||||
console.log("[KICAD] prefetched " + pairs.length + " board files");
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("[KICAD] board prefetch failed:", err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// ── preRun steps ────────────────────────────────────────────────────────────
|
||||
function createCanvas() {
|
||||
|
|
@ -166,16 +159,6 @@
|
|||
console.log("[KICAD] canvas " + window.innerWidth + "x" + window.innerHeight);
|
||||
}
|
||||
|
||||
function writeResources() {
|
||||
FS.mkdirTree(RESOURCE_PATH);
|
||||
if (resourceData) {
|
||||
FS.writeFile(RESOURCE_PATH + "/images.tar.gz", resourceData);
|
||||
console.log("[KICAD] wrote images.tar.gz");
|
||||
} else {
|
||||
console.warn("[KICAD] images.tar.gz not ready at preRun");
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress the first-run setup wizard (its modal loop crashes Asyncify on our
|
||||
// ephemeral MEMFS): make every settings provider report NeedsUserInput()==false.
|
||||
function seedKicadConfig() {
|
||||
|
|
@ -206,86 +189,174 @@
|
|||
console.log("[KICAD] seeded config (wizard suppressed)");
|
||||
}
|
||||
|
||||
// Write the board into MEMFS and point argv at it so gerbview auto-opens it.
|
||||
// argv must be set before main(); a run dependency keeps main() waiting if the
|
||||
// board fetch hasn't landed yet.
|
||||
function preloadBoard() {
|
||||
Module.arguments = OPEN_ARGS;
|
||||
var writeBoard = function () {
|
||||
if (!boardData) return;
|
||||
FS.mkdirTree(BOARD_DIR);
|
||||
Object.keys(boardData).forEach(function (name) {
|
||||
FS.writeFile(BOARD_DIR + "/" + name, boardData[name]);
|
||||
// Boot the tool once its CDN folder is resolved. `base` is the (cross-origin)
|
||||
// versioned folder; every asset — glue, wasm, images.tar.gz — lives under it.
|
||||
function boot(base) {
|
||||
// ── Prefetch the heavy, non-wasm assets in parallel with the wasm download ──
|
||||
// images.tar.gz (compiled-in KiCad resources) comes from the tool folder; the
|
||||
// board layers are same-origin fixtures. Both are far smaller than
|
||||
// gerbview.wasm, so they land before preRun runs; a run dependency guards the
|
||||
// rare case where the wasm wins the race.
|
||||
var resourceData = null;
|
||||
// The CDN stores images.tar.gz as raw gzip with NO Content-Encoding, so the
|
||||
// browser hands us the compressed bytes and KiCad's own gunzip succeeds.
|
||||
fetch(base + "/images.tar.gz")
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.arrayBuffer();
|
||||
})
|
||||
.then(function (buf) {
|
||||
resourceData = new Uint8Array(buf);
|
||||
console.log("[KICAD] prefetched images.tar.gz (" + resourceData.length + " bytes)");
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.warn("[KICAD] images.tar.gz prefetch failed:", err.message);
|
||||
});
|
||||
console.log("[KICAD] wrote board into " + BOARD_DIR + "; argv=" + OPEN_ARGS.length + " files");
|
||||
};
|
||||
if (boardData) {
|
||||
writeBoard();
|
||||
return;
|
||||
|
||||
var boardData = null; // { name: Uint8Array }
|
||||
var boardPromise = Promise.all(
|
||||
BOARD_FILES.map(function (name) {
|
||||
return fetch("./board/" + name)
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status + " for " + name);
|
||||
return r.arrayBuffer();
|
||||
})
|
||||
.then(function (buf) {
|
||||
return [name, new Uint8Array(buf)];
|
||||
});
|
||||
})
|
||||
)
|
||||
.then(function (pairs) {
|
||||
boardData = {};
|
||||
pairs.forEach(function (p) {
|
||||
boardData[p[0]] = p[1];
|
||||
});
|
||||
console.log("[KICAD] prefetched " + pairs.length + " board files");
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("[KICAD] board prefetch failed:", err.message);
|
||||
});
|
||||
|
||||
function writeResources() {
|
||||
FS.mkdirTree(RESOURCE_PATH);
|
||||
if (resourceData) {
|
||||
FS.writeFile(RESOURCE_PATH + "/images.tar.gz", resourceData);
|
||||
console.log("[KICAD] wrote images.tar.gz");
|
||||
} else {
|
||||
console.warn("[KICAD] images.tar.gz not ready at preRun");
|
||||
}
|
||||
}
|
||||
var add = window.addRunDependency;
|
||||
var rm = window.removeRunDependency;
|
||||
if (typeof add === "function" && typeof rm === "function") {
|
||||
add("board-files");
|
||||
boardPromise.then(function () {
|
||||
|
||||
// Write the board into MEMFS and point argv at it so gerbview auto-opens it.
|
||||
// argv must be set before main(); a run dependency keeps main() waiting if the
|
||||
// board fetch hasn't landed yet.
|
||||
function preloadBoard() {
|
||||
Module.arguments = OPEN_ARGS;
|
||||
var writeBoard = function () {
|
||||
if (!boardData) return;
|
||||
FS.mkdirTree(BOARD_DIR);
|
||||
Object.keys(boardData).forEach(function (name) {
|
||||
FS.writeFile(BOARD_DIR + "/" + name, boardData[name]);
|
||||
});
|
||||
console.log("[KICAD] wrote board into " + BOARD_DIR + "; argv=" + OPEN_ARGS.length + " files");
|
||||
};
|
||||
if (boardData) {
|
||||
writeBoard();
|
||||
rm("board-files");
|
||||
});
|
||||
} else {
|
||||
// Best-effort fallback (board << wasm, so this path is unlikely to lose).
|
||||
boardPromise.then(writeBoard);
|
||||
return;
|
||||
}
|
||||
var add = window.addRunDependency;
|
||||
var rm = window.removeRunDependency;
|
||||
if (typeof add === "function" && typeof rm === "function") {
|
||||
add("board-files");
|
||||
boardPromise.then(function () {
|
||||
writeBoard();
|
||||
rm("board-files");
|
||||
});
|
||||
} else {
|
||||
// Best-effort fallback (board << wasm, so this path is unlikely to lose).
|
||||
boardPromise.then(writeBoard);
|
||||
}
|
||||
}
|
||||
|
||||
// The pthread worker "script" for Module.mainScriptUrlOrBlob. KiCad spawns
|
||||
// CLASSIC workers via `new Worker(...)`; a cross-origin URL is a SecurityError,
|
||||
// so for the CDN base we hand emscripten a SAME-ORIGIN blob worker that
|
||||
// importScripts the cross-origin glue (allowed because the CDN sends CORP).
|
||||
function pthreadWorkerScript() {
|
||||
var abs = new URL(base + "/gerbview.js", location.href);
|
||||
if (abs.origin === location.origin) return base + "/gerbview.js";
|
||||
return new Blob(["importScripts(" + JSON.stringify(abs.href) + ");"], {
|
||||
type: "text/javascript",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Module config (global) ─────────────────────────────────────────────────
|
||||
var Module = {
|
||||
thisProgram: "/usr/bin/gerbview", // argv[0] for KiCad's DEBUG check
|
||||
arguments: OPEN_ARGS,
|
||||
preRun: [createCanvas, writeResources, seedKicadConfig, preloadBoard],
|
||||
postRun: [],
|
||||
print: function () {
|
||||
console.log("[KICAD_OUT] " + Array.prototype.join.call(arguments, " "));
|
||||
},
|
||||
printErr: function () {
|
||||
console.error("[KICAD_ERR] " + Array.prototype.join.call(arguments, " "));
|
||||
},
|
||||
setStatus: function (text) {
|
||||
if (text) console.log("[KICAD_STATUS] " + text);
|
||||
setStatusUI(text);
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function (left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(
|
||||
left
|
||||
? "Preparing… (" + (this.totalDependencies - left) + "/" + this.totalDependencies + ")"
|
||||
: "All downloads complete."
|
||||
);
|
||||
},
|
||||
onRuntimeInitialized: function () {
|
||||
console.log("[KICAD] runtime initialized");
|
||||
if (Module.canvas) Module.canvas.style.display = "block";
|
||||
hideStatus();
|
||||
},
|
||||
onAbort: function (what) {
|
||||
showError("aborted: " + (what === undefined ? "" : String(what)));
|
||||
},
|
||||
// Everything (the .wasm and the pthread worker's relative fetches) resolves
|
||||
// against the versioned CDN folder.
|
||||
locateFile: function (path) {
|
||||
return base + "/" + path;
|
||||
},
|
||||
// Pin the pthread worker: same-origin base → direct URL; cross-origin CDN →
|
||||
// a same-origin blob shim that importScripts the glue (see helper above).
|
||||
mainScriptUrlOrBlob: pthreadWorkerScript(),
|
||||
};
|
||||
window.Module = Module;
|
||||
|
||||
Module.setStatus("Downloading…");
|
||||
window.onerror = function (msg, url, line) {
|
||||
showError(msg + " @ " + url + ":" + line);
|
||||
return false;
|
||||
};
|
||||
|
||||
// ── Inject glue scripts in the required order ──────────────────────────────
|
||||
// wx.js → wx-dom.js → gerbview.js, all from the (cross-origin) CDN folder.
|
||||
loadScript(base + "/wx.js")
|
||||
.then(function () {
|
||||
return loadScript(base + "/wx-dom.js");
|
||||
})
|
||||
.then(function () {
|
||||
return loadScript(base + "/gerbview.js");
|
||||
})
|
||||
.then(function () {
|
||||
console.log("[KICAD] injected wx.js + wx-dom.js + gerbview.js (base=" + base + ")");
|
||||
})
|
||||
.catch(function (err) {
|
||||
showError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Module config (global) ───────────────────────────────────────────────────
|
||||
var Module = {
|
||||
thisProgram: "/usr/bin/gerbview", // argv[0] for KiCad's DEBUG check
|
||||
arguments: OPEN_ARGS,
|
||||
preRun: [createCanvas, writeResources, seedKicadConfig, preloadBoard],
|
||||
postRun: [],
|
||||
print: function () {
|
||||
console.log("[KICAD_OUT] " + Array.prototype.join.call(arguments, " "));
|
||||
},
|
||||
printErr: function () {
|
||||
console.error("[KICAD_ERR] " + Array.prototype.join.call(arguments, " "));
|
||||
},
|
||||
setStatus: function (text) {
|
||||
if (text) console.log("[KICAD_STATUS] " + text);
|
||||
setStatusUI(text);
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function (left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(
|
||||
left
|
||||
? "Preparing… (" + (this.totalDependencies - left) + "/" + this.totalDependencies + ")"
|
||||
: "All downloads complete."
|
||||
);
|
||||
},
|
||||
onRuntimeInitialized: function () {
|
||||
console.log("[KICAD] runtime initialized");
|
||||
if (Module.canvas) Module.canvas.style.display = "block";
|
||||
hideStatus();
|
||||
},
|
||||
onAbort: function (what) {
|
||||
showError("aborted: " + (what === undefined ? "" : String(what)));
|
||||
},
|
||||
// Route the .wasm to BINARY_BASE; everything else (the pthread worker) to GLUE_BASE.
|
||||
locateFile: function (path) {
|
||||
return (/\.wasm$/.test(path) ? BINARY_BASE : GLUE_BASE) + "/" + path;
|
||||
},
|
||||
// Pin the pthread worker to the same-origin glue (workers cannot be cross-origin).
|
||||
mainScriptUrlOrBlob: GLUE_BASE + "/gerbview.js",
|
||||
};
|
||||
window.Module = Module;
|
||||
|
||||
Module.setStatus("Downloading…");
|
||||
window.onerror = function (msg, url, line) {
|
||||
showError(msg + " @ " + url + ":" + line);
|
||||
return false;
|
||||
};
|
||||
|
||||
// ── Inject glue scripts in the required order ────────────────────────────────
|
||||
function loadScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var s = document.createElement("script");
|
||||
|
|
@ -298,17 +369,10 @@
|
|||
});
|
||||
}
|
||||
|
||||
loadScript(GLUE_BASE + "/wx.js")
|
||||
.then(function () {
|
||||
return loadScript(GLUE_BASE + "/wx-dom.js");
|
||||
})
|
||||
.then(function () {
|
||||
return loadScript(GLUE_BASE + "/gerbview.js");
|
||||
})
|
||||
.then(function () {
|
||||
console.log("[KICAD] injected wx.js + wx-dom.js + gerbview.js (glue=" + GLUE_BASE + ", bin=" + BINARY_BASE + ")");
|
||||
})
|
||||
setStatusUI("Resolving latest build…");
|
||||
resolveBase()
|
||||
.then(boot)
|
||||
.catch(function (err) {
|
||||
showError(err.message);
|
||||
showError("could not resolve gerbview build: " + err.message);
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"rules": [
|
||||
{
|
||||
"allowed": {
|
||||
"origins": [
|
||||
"https://pcbjam.com",
|
||||
"https://www.pcbjam.com",
|
||||
"https://*.vercel.app",
|
||||
"http://localhost:4321"
|
||||
],
|
||||
"methods": ["GET", "HEAD"],
|
||||
"headers": ["*"]
|
||||
},
|
||||
"exposeHeaders": ["Content-Length", "Content-Encoding", "Content-Type"],
|
||||
"maxAgeSeconds": 86400
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Deploy the heavy Gerber-viewer binaries to Cloudflare R2.
|
||||
#
|
||||
# Prereqs (one-time, done by you — see the chat guide):
|
||||
# 1. R2 enabled on your Cloudflare account (dashboard; needs a payment method).
|
||||
# 2. `wrangler login` (browser auth).
|
||||
#
|
||||
# This script then (idempotent — safe to re-run after a rebuild):
|
||||
# - creates the bucket (skips if it already exists)
|
||||
# - sets CORS (site/scripts/r2-cors.json)
|
||||
# - enables the public r2.dev URL and prints it
|
||||
# - gzips gerbview.wasm and uploads it (Content-Encoding: gzip) + kicad-resources.bin
|
||||
#
|
||||
# Usage: site/scripts/r2-deploy.sh [bucket-name] (default: pcbjam-assets)
|
||||
set -euo pipefail
|
||||
|
||||
BUCKET="${1:-pcbjam-assets}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # = site/
|
||||
SRC="$ROOT/public/gerber-demo/wasm"
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
command -v wrangler >/dev/null || { echo "wrangler not found (npm i -g wrangler)"; exit 1; }
|
||||
wrangler whoami >/dev/null 2>&1 || { echo "Not logged in. Run: wrangler login"; exit 1; }
|
||||
[[ -f "$SRC/gerbview.wasm" ]] || { echo "Missing $SRC/gerbview.wasm — run scripts/sync-demo-wasm.sh"; exit 1; }
|
||||
[[ -f "$SRC/kicad-resources.bin" ]] || { echo "Missing $SRC/kicad-resources.bin — run scripts/sync-demo-wasm.sh"; exit 1; }
|
||||
|
||||
echo "==> Bucket: $BUCKET"
|
||||
wrangler r2 bucket create "$BUCKET" 2>/dev/null \
|
||||
&& echo " created" \
|
||||
|| echo " already exists (continuing)"
|
||||
|
||||
echo "==> CORS"
|
||||
wrangler r2 bucket cors set "$BUCKET" --file "$ROOT/scripts/r2-cors.json"
|
||||
|
||||
echo "==> Public r2.dev URL (makes the bucket's objects publicly readable)"
|
||||
wrangler r2 bucket dev-url enable "$BUCKET" || true
|
||||
wrangler r2 bucket dev-url get "$BUCKET" || true
|
||||
|
||||
echo "==> gzip gerbview.wasm"
|
||||
gzip -9 -c "$SRC/gerbview.wasm" > "$TMP/gerbview.wasm.gz"
|
||||
echo " $(du -h "$SRC/gerbview.wasm" | cut -f1) -> $(du -h "$TMP/gerbview.wasm.gz" | cut -f1) gzipped"
|
||||
|
||||
echo "==> Upload gerbview.wasm (gzip, application/wasm)"
|
||||
wrangler r2 object put "$BUCKET/gerbview.wasm" --remote \
|
||||
--file "$TMP/gerbview.wasm.gz" \
|
||||
--content-type "application/wasm" \
|
||||
--content-encoding "gzip" \
|
||||
--cache-control "public, max-age=3600"
|
||||
|
||||
echo "==> Upload kicad-resources.bin (octet-stream, no encoding)"
|
||||
wrangler r2 object put "$BUCKET/kicad-resources.bin" --remote \
|
||||
--file "$SRC/kicad-resources.bin" \
|
||||
--content-type "application/octet-stream" \
|
||||
--cache-control "public, max-age=3600"
|
||||
|
||||
echo ""
|
||||
echo "Done. Copy the r2.dev URL printed above (https://<hash>.r2.dev) into"
|
||||
echo "site/public/gerber-demo/boot.js -> R2_BASE, then redeploy the site."
|
||||
echo "(Or bind a custom domain: wrangler r2 bucket domain add $BUCKET --domain assets.pcbjam.com)"
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Mirror the gerbview WASM build outputs into the blog demo's local asset dir.
|
||||
#
|
||||
# Phase A (local verification) serves the viewer from these files. They are
|
||||
# git-ignored (see site/.gitignore): the 52 MB gerbview.wasm must never be
|
||||
# committed or shipped to Vercel — in production the heavy binaries live on
|
||||
# Cloudflare R2 (see the blog-demo plan, Phase B). Re-run this after rebuilding
|
||||
# gerbview to refresh the local copy.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SRC="$ROOT/output"
|
||||
DST="$ROOT/site/public/gerber-demo/wasm"
|
||||
|
||||
mkdir -p "$DST"
|
||||
for f in wx.js wx-dom.js gerbview.js gerbview.wasm images.tar.gz; do
|
||||
if [[ ! -f "$SRC/$f" ]]; then
|
||||
echo "ERROR: missing $SRC/$f — build gerbview first (docker/build.sh)." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
cp "$SRC/wx.js" "$DST/wx.js"
|
||||
cp "$SRC/wx-dom.js" "$DST/wx-dom.js"
|
||||
cp "$SRC/gerbview.js" "$DST/gerbview.js"
|
||||
cp "$SRC/gerbview.wasm" "$DST/gerbview.wasm"
|
||||
# Store the resources tarball WITHOUT a .gz extension: dev servers (Vite/sirv)
|
||||
# and some CDNs auto-add `Content-Encoding: gzip` for `*.gz`, which makes the
|
||||
# browser pre-decompress it — then KiCad's own gunzip fails. boot.js fetches
|
||||
# this name and writes it into MEMFS as images.tar.gz.
|
||||
cp "$SRC/images.tar.gz" "$DST/kicad-resources.bin"
|
||||
|
||||
for f in wx.js wx-dom.js gerbview.js gerbview.wasm kicad-resources.bin; do
|
||||
echo "synced $f ($(du -h "$DST/$f" | cut -f1))"
|
||||
done
|
||||
echo "Local demo WASM ready at $DST"
|
||||
|
|
@ -11,6 +11,9 @@ const links = [
|
|||
label: 'Built by Emergence Engineering',
|
||||
external: true,
|
||||
},
|
||||
// Highlighted (accent) link to the public editor demo. A compact text link, not
|
||||
// a button — the header container caps at 1024px and a second button overflows it.
|
||||
{ href: 'https://demo.pcbjam.com', label: 'Live demo', external: true, highlight: true },
|
||||
];
|
||||
|
||||
const path = Astro.url.pathname;
|
||||
|
|
@ -36,6 +39,7 @@ const isActive = (href: string, external?: boolean) =>
|
|||
links.map((l) => (
|
||||
<a
|
||||
href={l.href}
|
||||
class:list={[{ 'nav-cta': l.highlight }]}
|
||||
aria-current={isActive(l.href, l.external) ? 'page' : undefined}
|
||||
rel={l.external ? 'noopener' : undefined}
|
||||
target={l.external ? '_blank' : undefined}
|
||||
|
|
@ -87,11 +91,12 @@ const isActive = (href: string, external?: boolean) =>
|
|||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.nav > a {
|
||||
color: var(--fg-muted);
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav > a:hover {
|
||||
color: var(--fg);
|
||||
|
|
@ -100,6 +105,15 @@ const isActive = (href: string, external?: boolean) =>
|
|||
.nav > a[aria-current='page'] {
|
||||
color: var(--fg);
|
||||
}
|
||||
/* Highlighted demo link — accent-colored so it stands out among the neutral
|
||||
nav links without needing a second button (which would overflow the bar). */
|
||||
.nav > a.nav-cta {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav > a.nav-cta:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.cta-join {
|
||||
padding-block: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
|
|
@ -120,7 +134,10 @@ const isActive = (href: string, external?: boolean) =>
|
|||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
/* Collapse to the hamburger menu once the full one-line nav no longer fits the
|
||||
1024px-capped bar (adding the demo link pushed the crowded desktop nav past
|
||||
the point where it stays on a single row). */
|
||||
@media (max-width: 1024px) {
|
||||
.brand-chip {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ import WaitlistForm from '../components/WaitlistForm.astro';
|
|||
microcopy="Get early-access invites and product updates. No spam, unsubscribe anytime."
|
||||
/>
|
||||
|
||||
<p class="final-demo">
|
||||
Don’t want to wait? <a
|
||||
href="https://demo.pcbjam.com"
|
||||
target="_blank"
|
||||
rel="noopener">Try the live demo now →</a>
|
||||
</p>
|
||||
|
||||
<ul class="final-trust">
|
||||
<li>Open source</li>
|
||||
<li>Built on KiCad</li>
|
||||
|
|
@ -37,6 +44,15 @@ import WaitlistForm from '../components/WaitlistForm.astro';
|
|||
margin-top: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
.final-demo {
|
||||
margin: 1rem 0 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.final-demo a {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.final-trust {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import SectionBand from '../components/SectionBand.astro';
|
|||
// in a new tab so the landing page itself never needs isolation.
|
||||
const DEMO_HREF = '/gerber-demo/index.html';
|
||||
const poster = '/gerber-demo/poster.png';
|
||||
// The full in-browser editor (not just the viewer) — the public demo.
|
||||
const EDITOR_URL = 'https://demo.pcbjam.com';
|
||||
---
|
||||
|
||||
<SectionBand id="demo" labelledby="gerber-demo-h2">
|
||||
|
|
@ -40,6 +42,12 @@ const poster = '/gerber-demo/poster.png';
|
|||
<span class="gerber-launch__cta">Launch the live Gerber viewer</span>
|
||||
<span class="gerber-launch__sub">Opens in a new tab · streams ~22 MB on demand</span>
|
||||
</a>
|
||||
|
||||
<p class="demo-note">
|
||||
Want to do more than look? Open a board in the full editor at
|
||||
<a href={EDITOR_URL} target="_blank" rel="noopener">demo.pcbjam.com</a> — edit
|
||||
the PCB or schematic in your browser, no install, no account.
|
||||
</p>
|
||||
</SectionBand>
|
||||
|
||||
<style>
|
||||
|
|
@ -99,6 +107,16 @@ const poster = '/gerber-demo/poster.png';
|
|||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.demo-note {
|
||||
margin: 1rem 0 0;
|
||||
text-align: center;
|
||||
color: var(--fg-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.demo-note a {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.gerber-launch {
|
||||
transition: none;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ import Icon from '../components/Icon.astro';
|
|||
<WaitlistForm source="hero" id="waitlist" class="hero-form" />
|
||||
|
||||
<p class="hero-secondary">
|
||||
<a
|
||||
href="https://demo.pcbjam.com"
|
||||
class="try"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Open the live editor →
|
||||
</a>
|
||||
<span class="dot" aria-hidden="true">·</span>
|
||||
<a href="#multiplayer" class="watch">
|
||||
<Icon name="icons/play.svg" size="1.1rem" alt="" /> Watch the 90-second demo →
|
||||
</a>
|
||||
|
|
@ -61,6 +70,19 @@ import Icon from '../components/Icon.astro';
|
|||
}
|
||||
.hero-secondary {
|
||||
margin: 1rem 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem 0.75rem;
|
||||
}
|
||||
.try {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
.dot {
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.watch {
|
||||
display: inline-flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue