@plasius/gpu-demo-viewer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html ADDED
@@ -0,0 +1,160 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Plasius GPU Demo Viewer</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <main class="app">
11
+ <header class="hero">
12
+ <div class="hero__copy">
13
+ <p class="eyebrow">Plasius GPU</p>
14
+ <h1>Demo Viewer</h1>
15
+ <p class="lead">
16
+ Launch, preview, and validate every <code>gpu-*</code> demo from one root URL.
17
+ Browser demos can be embedded directly here, including the shared 3D harbor scene
18
+ used by cloth, fluid, lighting, debug, and performance validation surfaces.
19
+ </p>
20
+ </div>
21
+ <section class="runtime-card" aria-live="polite">
22
+ <p id="runtimeBadge" class="runtime-badge">Checking runtime…</p>
23
+ <p id="runtimeDetails" class="runtime-details">
24
+ Browser-backed WebGPU demos require <code>localhost</code> or <code>HTTPS</code>.
25
+ </p>
26
+ </section>
27
+ </header>
28
+
29
+ <section class="toolbar">
30
+ <label class="search-field">
31
+ <span>Search demos</span>
32
+ <input
33
+ id="searchInput"
34
+ type="search"
35
+ placeholder="gpu-renderer, particles, world…"
36
+ autocomplete="off"
37
+ />
38
+ </label>
39
+ <div class="filters" role="toolbar" aria-label="Demo filters">
40
+ <button type="button" class="filter is-active" data-filter="all">All</button>
41
+ <button type="button" class="filter" data-filter="browser">Browser</button>
42
+ <button type="button" class="filter" data-filter="3d">3D Canvas</button>
43
+ <button type="button" class="filter" data-filter="integration">Integration</button>
44
+ <button type="button" class="filter" data-filter="example">Code Examples</button>
45
+ </div>
46
+ </section>
47
+
48
+ <section class="layout">
49
+ <aside class="catalog panel">
50
+ <div class="catalog__header">
51
+ <div>
52
+ <p class="section-eyebrow">Packages</p>
53
+ <h2>GPU Demo Catalog</h2>
54
+ </div>
55
+ <p id="catalogSummary" class="catalog__summary">Loading demo manifest…</p>
56
+ </div>
57
+ <div id="demoList" class="demo-list"></div>
58
+ </aside>
59
+
60
+ <section class="viewer panel">
61
+ <div class="viewer__header">
62
+ <div>
63
+ <p id="selectedMeta" class="section-eyebrow">@plasius/gpu-demo-viewer</p>
64
+ <h2 id="selectedTitle">integration-showcase</h2>
65
+ <p id="selectedDescription" class="viewer__description">
66
+ Unified 3D validation scene with GLTF ships, colliding rigid bodies, cloth, fluid
67
+ continuity, lighting bands, adaptive performance, and telemetry.
68
+ </p>
69
+ </div>
70
+ <div class="viewer__actions">
71
+ <a id="openDemo" class="action" href="./showcase/" target="_blank" rel="noreferrer">
72
+ Open showcase
73
+ </a>
74
+ <a
75
+ id="openSource"
76
+ class="action action--ghost"
77
+ href="./showcase/main.js"
78
+ target="_blank"
79
+ rel="noreferrer"
80
+ >
81
+ Source
82
+ </a>
83
+ <a
84
+ id="openDocs"
85
+ class="action action--ghost"
86
+ href="./README.md"
87
+ target="_blank"
88
+ rel="noreferrer"
89
+ >
90
+ Docs
91
+ </a>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="details-grid">
96
+ <section class="detail-card">
97
+ <h3>Validation Summary</h3>
98
+ <dl class="detail-list">
99
+ <div>
100
+ <dt>Mode</dt>
101
+ <dd id="detailMode">Unified 3D integration showcase</dd>
102
+ </div>
103
+ <div>
104
+ <dt>Runtime</dt>
105
+ <dd id="detailRuntime">3D canvas + GLTF rigid bodies</dd>
106
+ </div>
107
+ <div>
108
+ <dt>Launch</dt>
109
+ <dd id="detailLaunch">showcase/</dd>
110
+ </div>
111
+ </dl>
112
+ </section>
113
+
114
+ <section class="detail-card">
115
+ <h3>Run Command</h3>
116
+ <pre id="detailCommand" class="command-block">cd gpu-demo-viewer && npm run demo</pre>
117
+ </section>
118
+
119
+ <section class="detail-card detail-card--wide">
120
+ <h3>Notes</h3>
121
+ <ul id="detailNotes" class="notes-list">
122
+ <li>This is the top-level cross-package validation surface.</li>
123
+ <li>The ships are loaded from GLTF and expose physics metadata used by the collision loop.</li>
124
+ </ul>
125
+ </section>
126
+ </div>
127
+
128
+ <section id="browserPane" class="preview-pane">
129
+ <div class="preview-pane__header">
130
+ <p class="preview-label">Embedded Preview</p>
131
+ <p id="previewHint" class="preview-hint">
132
+ Mounted canvas preview. Interact with the embedded demo directly.
133
+ </p>
134
+ </div>
135
+ <iframe
136
+ id="demoFrame"
137
+ class="demo-frame"
138
+ title="GPU demo preview"
139
+ loading="lazy"
140
+ src="./showcase/"
141
+ ></iframe>
142
+ </section>
143
+
144
+ <section id="examplePane" class="example-pane" hidden>
145
+ <div class="preview-pane__header">
146
+ <p class="preview-label">Code Example</p>
147
+ <p class="preview-hint">
148
+ This package does not mount a browser demo. Use the command and source entry
149
+ points below for validation.
150
+ </p>
151
+ </div>
152
+ <ul id="exampleFiles" class="example-files"></ul>
153
+ </section>
154
+ </section>
155
+ </section>
156
+ </main>
157
+
158
+ <script type="module" src="./main.js"></script>
159
+ </body>
160
+ </html>
package/index.js ADDED
@@ -0,0 +1 @@
1
+ import "./main.js";
package/main.js ADDED
@@ -0,0 +1,262 @@
1
+ import { demos } from "./viewer-manifest.js";
2
+
3
+ const state = {
4
+ filter: "all",
5
+ search: "",
6
+ selectedId: "integration-showcase"
7
+ };
8
+
9
+ const demoListEl = document.getElementById("demoList");
10
+ const catalogSummaryEl = document.getElementById("catalogSummary");
11
+ const searchInputEl = document.getElementById("searchInput");
12
+ const filterButtons = Array.from(document.querySelectorAll(".filter"));
13
+ const runtimeBadgeEl = document.getElementById("runtimeBadge");
14
+ const runtimeDetailsEl = document.getElementById("runtimeDetails");
15
+
16
+ const selectedMetaEl = document.getElementById("selectedMeta");
17
+ const selectedTitleEl = document.getElementById("selectedTitle");
18
+ const selectedDescriptionEl = document.getElementById("selectedDescription");
19
+ const detailModeEl = document.getElementById("detailMode");
20
+ const detailRuntimeEl = document.getElementById("detailRuntime");
21
+ const detailLaunchEl = document.getElementById("detailLaunch");
22
+ const detailCommandEl = document.getElementById("detailCommand");
23
+ const detailNotesEl = document.getElementById("detailNotes");
24
+ const openDemoEl = document.getElementById("openDemo");
25
+ const openSourceEl = document.getElementById("openSource");
26
+ const openDocsEl = document.getElementById("openDocs");
27
+ const browserPaneEl = document.getElementById("browserPane");
28
+ const examplePaneEl = document.getElementById("examplePane");
29
+ const demoFrameEl = document.getElementById("demoFrame");
30
+ const previewHintEl = document.getElementById("previewHint");
31
+ const exampleFilesEl = document.getElementById("exampleFiles");
32
+
33
+ function setRuntimeStatus() {
34
+ const onLocalhost = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
35
+ const secure = window.isSecureContext || onLocalhost;
36
+ if (secure) {
37
+ runtimeBadgeEl.textContent = "Secure context ready";
38
+ runtimeBadgeEl.dataset.tone = "success";
39
+ runtimeDetailsEl.innerHTML =
40
+ 'Browser-backed WebGPU demos can initialize here. Open this viewer from <code>localhost</code> or <code>HTTPS</code> for consistent results.';
41
+ return;
42
+ }
43
+
44
+ runtimeBadgeEl.textContent = "Secure context missing";
45
+ runtimeBadgeEl.dataset.tone = "error";
46
+ runtimeDetailsEl.innerHTML =
47
+ 'This host is not running in a secure context. Browser-backed WebGPU demos may report <code>navigator.gpu</code> as unavailable until you serve the workspace from <code>localhost</code> or <code>HTTPS</code>.';
48
+ }
49
+
50
+ function matchesFilter(demo) {
51
+ if (state.filter === "all") {
52
+ return true;
53
+ }
54
+ if (state.filter === "browser") {
55
+ return demo.type === "browser";
56
+ }
57
+ if (state.filter === "example") {
58
+ return demo.type === "example";
59
+ }
60
+ if (state.filter === "3d") {
61
+ return demo.tags.includes("3d");
62
+ }
63
+ if (state.filter === "integration") {
64
+ return demo.tags.includes("integration");
65
+ }
66
+ return true;
67
+ }
68
+
69
+ function matchesSearch(demo) {
70
+ const term = state.search.trim().toLowerCase();
71
+ if (!term) {
72
+ return true;
73
+ }
74
+ return [
75
+ demo.id,
76
+ demo.packageName,
77
+ demo.mode,
78
+ demo.runtime,
79
+ demo.summary,
80
+ ...demo.notes
81
+ ]
82
+ .join(" ")
83
+ .toLowerCase()
84
+ .includes(term);
85
+ }
86
+
87
+ function getVisibleDemos() {
88
+ return demos.filter((demo) => matchesFilter(demo) && matchesSearch(demo));
89
+ }
90
+
91
+ function getSelectedDemo(visibleDemos) {
92
+ if (state.selectedId) {
93
+ const selected = visibleDemos.find((demo) => demo.id === state.selectedId);
94
+ if (selected) {
95
+ return selected;
96
+ }
97
+ }
98
+ const hashId = window.location.hash.replace(/^#/, "");
99
+ if (hashId) {
100
+ const selected = visibleDemos.find((demo) => demo.id === hashId);
101
+ if (selected) {
102
+ return selected;
103
+ }
104
+ }
105
+ return visibleDemos[0] ?? null;
106
+ }
107
+
108
+ function renderCatalog() {
109
+ const visibleDemos = getVisibleDemos();
110
+ const selectedDemo = getSelectedDemo(visibleDemos);
111
+ state.selectedId = selectedDemo?.id ?? null;
112
+
113
+ catalogSummaryEl.textContent =
114
+ visibleDemos.length === demos.length
115
+ ? `${visibleDemos.length} demos available`
116
+ : `${visibleDemos.length} of ${demos.length} demos shown`;
117
+
118
+ if (visibleDemos.length === 0) {
119
+ demoListEl.innerHTML =
120
+ '<div class="empty-state">No demos match the current search and filter combination.</div>';
121
+ renderSelection(null);
122
+ return;
123
+ }
124
+
125
+ demoListEl.innerHTML = "";
126
+
127
+ for (const demo of visibleDemos) {
128
+ const card = document.createElement("button");
129
+ card.type = "button";
130
+ card.className = `demo-card${demo.id === state.selectedId ? " is-selected" : ""}`;
131
+ card.dataset.id = demo.id;
132
+ card.innerHTML = `
133
+ <div class="demo-card__top">
134
+ <div>
135
+ <p class="section-eyebrow">${demo.packageName}</p>
136
+ <h3 class="demo-card__name">${demo.id}</h3>
137
+ </div>
138
+ <span class="tag tag--${demo.type}">${demo.type}</span>
139
+ </div>
140
+ <p class="demo-card__summary">${demo.summary}</p>
141
+ <div class="demo-card__tags">
142
+ ${demo.tags.map((tag) => `<span class="tag">${tag}</span>`).join("")}
143
+ </div>
144
+ `;
145
+ card.addEventListener("click", () => {
146
+ state.selectedId = demo.id;
147
+ window.location.hash = demo.id;
148
+ renderCatalog();
149
+ });
150
+ demoListEl.appendChild(card);
151
+ }
152
+
153
+ renderSelection(selectedDemo);
154
+ }
155
+
156
+ function renderNotes(notes) {
157
+ detailNotesEl.innerHTML = "";
158
+ for (const note of notes) {
159
+ const item = document.createElement("li");
160
+ item.textContent = note;
161
+ detailNotesEl.appendChild(item);
162
+ }
163
+ }
164
+
165
+ function renderExampleFiles(demo) {
166
+ exampleFilesEl.innerHTML = "";
167
+ const files = [
168
+ { label: "Primary entry", href: demo.sourcePath },
169
+ demo.docsPath ? { label: "Package docs", href: demo.docsPath } : null
170
+ ].filter(Boolean);
171
+
172
+ for (const file of files) {
173
+ const item = document.createElement("li");
174
+ const link = document.createElement("a");
175
+ link.href = file.href;
176
+ link.target = "_blank";
177
+ link.rel = "noreferrer";
178
+ link.textContent = `${file.label}: ${file.href.replace(/^\.\//, "")}`;
179
+ item.appendChild(link);
180
+ exampleFilesEl.appendChild(item);
181
+ }
182
+ }
183
+
184
+ function renderSelection(demo) {
185
+ if (!demo) {
186
+ selectedMetaEl.textContent = "No demo selected";
187
+ selectedTitleEl.textContent = "Choose a package";
188
+ selectedDescriptionEl.textContent =
189
+ "Adjust the filters or search term to find a demo entry.";
190
+ detailModeEl.textContent = "-";
191
+ detailRuntimeEl.textContent = "-";
192
+ detailLaunchEl.textContent = "-";
193
+ detailCommandEl.textContent = "Select a demo to see its command.";
194
+ detailNotesEl.innerHTML = "<li>No notes available.</li>";
195
+ browserPaneEl.hidden = true;
196
+ examplePaneEl.hidden = true;
197
+ openDemoEl.hidden = true;
198
+ openSourceEl.hidden = true;
199
+ openDocsEl.hidden = true;
200
+ demoFrameEl.removeAttribute("src");
201
+ return;
202
+ }
203
+
204
+ selectedMetaEl.textContent = demo.packageName;
205
+ selectedTitleEl.textContent = demo.id;
206
+ selectedDescriptionEl.textContent = demo.summary;
207
+ detailModeEl.textContent = demo.mode;
208
+ detailRuntimeEl.textContent = demo.runtime;
209
+ detailLaunchEl.textContent = demo.launchPath.replace(/^\.\//, "");
210
+ detailCommandEl.textContent = demo.command;
211
+ renderNotes(demo.notes);
212
+
213
+ openDemoEl.hidden = false;
214
+ openDemoEl.textContent = demo.launchLabel;
215
+ openDemoEl.href = demo.launchPath;
216
+ openSourceEl.hidden = false;
217
+ openSourceEl.href = demo.sourcePath;
218
+ openDocsEl.hidden = !demo.docsPath;
219
+ if (demo.docsPath) {
220
+ openDocsEl.href = demo.docsPath;
221
+ }
222
+
223
+ if (demo.type === "browser") {
224
+ browserPaneEl.hidden = false;
225
+ examplePaneEl.hidden = true;
226
+ previewHintEl.textContent = demo.tags.includes("integration")
227
+ ? "Integrated cross-package harbor preview. Use this to validate the full family together."
228
+ : "Mounted 3D preview. Interact with the embedded demo directly.";
229
+ demoFrameEl.src = demo.launchPath;
230
+ } else {
231
+ browserPaneEl.hidden = true;
232
+ examplePaneEl.hidden = false;
233
+ demoFrameEl.removeAttribute("src");
234
+ renderExampleFiles(demo);
235
+ }
236
+ }
237
+
238
+ function bindEvents() {
239
+ searchInputEl.addEventListener("input", (event) => {
240
+ state.search = event.target.value;
241
+ renderCatalog();
242
+ });
243
+
244
+ for (const button of filterButtons) {
245
+ button.addEventListener("click", () => {
246
+ state.filter = button.dataset.filter ?? "all";
247
+ for (const sibling of filterButtons) {
248
+ sibling.classList.toggle("is-active", sibling === button);
249
+ }
250
+ renderCatalog();
251
+ });
252
+ }
253
+
254
+ window.addEventListener("hashchange", () => {
255
+ state.selectedId = window.location.hash.replace(/^#/, "") || null;
256
+ renderCatalog();
257
+ });
258
+ }
259
+
260
+ setRuntimeStatus();
261
+ bindEvents();
262
+ renderCatalog();
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@plasius/gpu-demo-viewer",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Root validation viewer for Plasius gpu-* demos.",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Plasius-LTD/gpu-demo-viewer.git"
10
+ },
11
+ "homepage": "https://github.com/Plasius-LTD/gpu-demo-viewer#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Plasius-LTD/gpu-demo-viewer/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "exports": {
19
+ ".": "./viewer-manifest.js",
20
+ "./viewer-manifest": "./viewer-manifest.js",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "assets/",
25
+ "docs/",
26
+ "scripts/",
27
+ "showcase/",
28
+ "wavefront/",
29
+ "CHANGELOG.md",
30
+ "NFR.md",
31
+ "README.md",
32
+ "index.html",
33
+ "index.js",
34
+ "main.js",
35
+ "styles.css",
36
+ "viewer-manifest.js"
37
+ ],
38
+ "engines": {
39
+ "node": ">=24"
40
+ },
41
+ "scripts": {
42
+ "audit:npm": "npm audit --audit-level=high --omit=dev",
43
+ "build": "node scripts/verify-static-viewer-contract.cjs",
44
+ "demo": "python3 -m http.server --directory ..",
45
+ "lint": "npm run typecheck",
46
+ "pack:check": "node scripts/verify-public-package.cjs",
47
+ "prepublishOnly": "npm run build && npm run pack:check",
48
+ "typecheck": "node --check index.js && node --check main.js && node --check viewer-manifest.js && node --check showcase/index.js && node --check showcase/main.js && node --check wavefront/main.js",
49
+ "test": "node --test"
50
+ },
51
+ "dependencies": {
52
+ "@plasius/gpu-renderer": "^0.2.3",
53
+ "@plasius/gpu-shared": "^0.1.16"
54
+ }
55
+ }
@@ -0,0 +1,90 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const { spawnSync } = require("node:child_process");
5
+
6
+ const repoRoot = path.resolve(__dirname, "..");
7
+
8
+ function assert(condition, message) {
9
+ if (!condition) {
10
+ throw new Error(message);
11
+ }
12
+ }
13
+
14
+ function readJson(relativePath) {
15
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), "utf8"));
16
+ }
17
+
18
+ function packDryRunFiles() {
19
+ const result = spawnSync("npm", ["pack", "--dry-run", "--json"], {
20
+ cwd: repoRoot,
21
+ encoding: "utf8",
22
+ env: {
23
+ ...process.env,
24
+ npm_config_cache:
25
+ process.env.PLASIUS_PACK_CHECK_NPM_CACHE ||
26
+ path.join(os.tmpdir(), "plasius-gpu-demo-viewer-npm-cache")
27
+ },
28
+ stdio: ["ignore", "pipe", "pipe"]
29
+ });
30
+
31
+ assert(
32
+ result.status === 0,
33
+ `npm pack --dry-run failed:\n${result.stdout}\n${result.stderr}`
34
+ );
35
+
36
+ const [packInfo] = JSON.parse(result.stdout);
37
+ assert(packInfo && Array.isArray(packInfo.files), "npm pack output did not include file metadata");
38
+ return packInfo.files.map((file) => file.path).sort();
39
+ }
40
+
41
+ function main() {
42
+ const packageJson = readJson("package.json");
43
+
44
+ assert(packageJson.name === "@plasius/gpu-demo-viewer", "package name must remain scoped");
45
+ assert(packageJson.private !== true, "public package must not set private=true");
46
+ assert(
47
+ packageJson.publishConfig?.access === "public",
48
+ "public package must publish with public npm access"
49
+ );
50
+ assert(packageJson.license === "Apache-2.0", "package license is required");
51
+ assert(packageJson.repository?.url?.includes("Plasius-LTD/gpu-demo-viewer"), "repository metadata is required");
52
+ assert(packageJson.exports?.["."] === "./viewer-manifest.js", "viewer manifest must be the package root export");
53
+
54
+ const files = packDryRunFiles();
55
+ const fileSet = new Set(files);
56
+ const requiredFiles = [
57
+ "CHANGELOG.md",
58
+ "LICENSE",
59
+ "README.md",
60
+ "assets/brigantine.gltf",
61
+ "docs/adrs/index.md",
62
+ "index.html",
63
+ "main.js",
64
+ "package.json",
65
+ "showcase/index.html",
66
+ "viewer-manifest.js",
67
+ "wavefront/index.html",
68
+ "wavefront/main.js"
69
+ ];
70
+
71
+ for (const requiredFile of requiredFiles) {
72
+ assert(fileSet.has(requiredFile), `package tarball must include ${requiredFile}`);
73
+ }
74
+
75
+ for (const file of files) {
76
+ assert(!file.startsWith("node_modules/"), `package tarball must not include ${file}`);
77
+ assert(!file.startsWith("tests/"), `package tarball must not include ${file}`);
78
+ assert(!file.startsWith(".git/"), `package tarball must not include ${file}`);
79
+ assert(!file.startsWith(".tmp"), `package tarball must not include ${file}`);
80
+ }
81
+
82
+ process.stdout.write("Public package contract verified.\n");
83
+ }
84
+
85
+ try {
86
+ main();
87
+ } catch (error) {
88
+ process.stderr.write(`${error.message}\n`);
89
+ process.exitCode = 1;
90
+ }
@@ -0,0 +1,64 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { pathToFileURL } = require("node:url");
4
+
5
+ const repoRoot = path.resolve(__dirname, "..");
6
+
7
+ function assert(condition, message) {
8
+ if (!condition) {
9
+ throw new Error(message);
10
+ }
11
+ }
12
+
13
+ function requirePath(relativePath, label) {
14
+ const fullPath = path.join(repoRoot, relativePath);
15
+ assert(fs.existsSync(fullPath), `${label} is missing: ${relativePath}`);
16
+ return fullPath;
17
+ }
18
+
19
+ function staticPathFromUrl(relativePath) {
20
+ return relativePath.split(/[?#]/, 1)[0];
21
+ }
22
+
23
+ async function main() {
24
+ requirePath("README.md", "README");
25
+ requirePath("CHANGELOG.md", "CHANGELOG");
26
+ requirePath("NFR.md", "NFR");
27
+ requirePath("docs/adrs/index.md", "ADR index");
28
+ requirePath(
29
+ "docs/adrs/adr-0001-gpu-demo-viewer-governance-baseline.md",
30
+ "Baseline ADR"
31
+ );
32
+ requirePath("index.html", "Root viewer entry");
33
+ requirePath("main.js", "Root viewer runtime");
34
+ requirePath("showcase/index.html", "Showcase entry");
35
+ requirePath("showcase/main.js", "Showcase runtime");
36
+
37
+ const { demos } = await import(pathToFileURL(requirePath("viewer-manifest.js", "Viewer manifest")).href);
38
+ assert(Array.isArray(demos) && demos.length > 0, "viewer-manifest must export demos");
39
+
40
+ for (const demo of demos) {
41
+ assert(typeof demo.id === "string" && demo.id.length > 0, "demo.id is required");
42
+ assert(typeof demo.launchPath === "string", `${demo.id} launchPath is required`);
43
+ assert(typeof demo.sourcePath === "string", `${demo.id} sourcePath is required`);
44
+
45
+ if (demo.launchPath.startsWith("./")) {
46
+ requirePath(staticPathFromUrl(demo.launchPath.slice(2)), `${demo.id} launchPath`);
47
+ }
48
+
49
+ if (demo.sourcePath.startsWith("./")) {
50
+ requirePath(staticPathFromUrl(demo.sourcePath.slice(2)), `${demo.id} sourcePath`);
51
+ }
52
+
53
+ if (typeof demo.docsPath === "string" && demo.docsPath.startsWith("./")) {
54
+ requirePath(staticPathFromUrl(demo.docsPath.slice(2)), `${demo.id} docsPath`);
55
+ }
56
+ }
57
+
58
+ process.stdout.write("Static viewer contract verified.\n");
59
+ }
60
+
61
+ main().catch((error) => {
62
+ process.stderr.write(`${error.message}\n`);
63
+ process.exitCode = 1;
64
+ });
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Plasius GPU Integration Showcase</title>
7
+ <script type="importmap">
8
+ {
9
+ "imports": {
10
+ "@plasius/gpu-shared": "../node_modules/@plasius/gpu-shared/dist/index.js"
11
+ }
12
+ }
13
+ </script>
14
+ </head>
15
+ <body>
16
+ <div id="app"></div>
17
+ <script type="module" src="./main.js"></script>
18
+ </body>
19
+ </html>
@@ -0,0 +1 @@
1
+ import "./main.js";
@@ -0,0 +1,14 @@
1
+ import { mountGpuShowcase } from "@plasius/gpu-shared";
2
+
3
+ const focus = new URLSearchParams(window.location.search).get("focus") ?? "integrated";
4
+
5
+ await mountGpuShowcase({
6
+ root: document.getElementById("app"),
7
+ focus,
8
+ packageName: "@plasius/gpu-demo-viewer",
9
+ title: focus === "physics" ? "Flag by the Sea: Physics Focus" : "Flag by the Sea",
10
+ subtitle:
11
+ focus === "physics"
12
+ ? "Stable-world-snapshot validation scene with moonlit GLTF ships, mass-aware collisions, lantern reflections, downstream cloth and fluid continuity, and ray-traced lighting bands."
13
+ : "Unified moonlit 3D validation scene with GLTF ships, lantern-lit collisions, cloth, fluid continuity, lighting bands, adaptive performance, and telemetry.",
14
+ });