130 lines
4.4 KiB
HTML
130 lines
4.4 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>EG WebGL Scene</title>
|
|
<link rel="stylesheet" href="./style.css" />
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<canvas id="scene-canvas"></canvas>
|
|
<div id="status" class="status">Loading scene...</div>
|
|
<details class="docs-panel">
|
|
<summary>Frontend Integration</summary>
|
|
<div class="docs-content">
|
|
<p><strong>Selectors:</strong> prefer <code>publicId</code>, then <code>id</code>, then exact <code>name</code>. Duplicate names return a structured error.</p>
|
|
<p><strong>Coordinates:</strong> API defaults to <code>{ coordSystem: "panda", space: "local" }</code>. Use <code>coordSystem: "three"</code> to work in Three.js axes. Local means transform relative to parent.</p>
|
|
<pre><code>// Same-page JS API
|
|
const viewer = window.EGWebGLViewer;
|
|
await viewer.ready;
|
|
|
|
const nodes = viewer.nodes.list();
|
|
const hero = nodes.find((node) => node.kind === "model");
|
|
|
|
viewer.animation.play({ publicId: hero.publicId }, {
|
|
clip: "Walk",
|
|
loop: true,
|
|
restart: true,
|
|
});
|
|
|
|
viewer.nodes.translate({ publicId: hero.publicId }, { x: 1, y: 0, z: 0 });
|
|
viewer.nodes.setTransform(
|
|
{ publicId: hero.publicId },
|
|
{ rotation: { h: 45, p: 0, r: 0 } },
|
|
{ coordSystem: "panda", space: "local" },
|
|
);
|
|
|
|
const supportedScripts = viewer.scripts.listSupported();
|
|
console.log("web supported scripts", supportedScripts);
|
|
|
|
viewer.scripts.attach({ publicId: hero.publicId }, {
|
|
name: "MoverScript",
|
|
params: {
|
|
move_axis: "x",
|
|
move_distance: 3,
|
|
move_speed: 1.5,
|
|
},
|
|
});
|
|
|
|
viewer.events.on("animationFinished", (payload) => {
|
|
console.log("animation finished", payload);
|
|
});</code></pre>
|
|
<pre><code>// iframe + postMessage
|
|
const iframe = document.querySelector("iframe");
|
|
|
|
iframe.contentWindow.postMessage({
|
|
source: "eg-frontend",
|
|
type: "handshake",
|
|
id: "hello-1",
|
|
}, iframe.src ? new URL(iframe.src).origin : "*");
|
|
|
|
window.addEventListener("message", (event) => {
|
|
if (!event.data || event.data.source !== "eg-webgl") return;
|
|
console.log("viewer message", event.data);
|
|
});
|
|
|
|
iframe.contentWindow.postMessage({
|
|
source: "eg-frontend",
|
|
type: "command",
|
|
id: "cmd-1",
|
|
command: "scripts.attach",
|
|
payload: {
|
|
selector: { publicId: "model:asset-guid:root" },
|
|
script: {
|
|
name: "RotatorScript",
|
|
params: { rotation_speed_y: 45 },
|
|
},
|
|
},
|
|
}, iframe.src ? new URL(iframe.src).origin : "*");</code></pre>
|
|
<p><strong>Message protocol:</strong> commands use <code>{ source: "eg-frontend", id, type: "command", command, payload }</code>. Responses/events use <code>{ source: "eg-webgl", type, replyTo?, event?, ok?, result?, error? }</code>.</p>
|
|
<p><strong>Events:</strong> <code>ready</code>, <code>error</code>, <code>animationFinished</code>, <code>stateChanged</code>.</p>
|
|
<p><strong>Script note:</strong> exported WebGL can read existing script metadata, but browser-side runtime only executes Web-supported script adapters, not arbitrary Python files.</p>
|
|
<p><strong>Demo page:</strong> after export, open <code>frontend_demo.html</code> to see a working iframe host example.</p>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<script type="module">
|
|
const candidates = ["./js/viewer.js", "./viewer.js"];
|
|
const statusEl = document.getElementById("status");
|
|
|
|
if (window.location.protocol === "file:") {
|
|
if (statusEl) {
|
|
statusEl.textContent = [
|
|
"This page cannot run from file://",
|
|
"",
|
|
"Please start a local HTTP server in the exported WebGL folder, for example:",
|
|
"python3 -m http.server 8000",
|
|
"",
|
|
"Then open:",
|
|
"http://127.0.0.1:8000/index.html",
|
|
"http://127.0.0.1:8000/frontend_demo.html",
|
|
].join("\n");
|
|
statusEl.className = "status error";
|
|
}
|
|
throw new Error("file_protocol_not_supported");
|
|
}
|
|
|
|
let loaded = false;
|
|
let lastError = null;
|
|
for (const specifier of candidates) {
|
|
try {
|
|
await import(specifier);
|
|
loaded = true;
|
|
break;
|
|
} catch (error) {
|
|
lastError = error;
|
|
}
|
|
}
|
|
|
|
if (!loaded) {
|
|
console.error(lastError);
|
|
if (statusEl) {
|
|
statusEl.textContent = `Failed to load viewer bootstrap:\n${String(lastError?.message || lastError)}`;
|
|
statusEl.className = "status error";
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|