Spaces:
Sleeping
Sleeping
File size: 6,826 Bytes
39ff632 8d7dfcd 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 37e3d5a 39ff632 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src blob: data:; worker-src blob:">
<title>img2threejs viewer</title>
<style>
html, body { margin: 0; height: 100%; background: #16181d; overflow: hidden; }
#viewer { position: fixed; inset: 0; }
#status {
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
color: #9aa0ad; font: 14px/1.5 system-ui, sans-serif; text-align: center; padding: 1rem;
}
#status[hidden] { display: none !important; }
</style>
</head>
<body>
<div id="viewer" role="region" aria-label="3D model viewer"></div>
<div id="status">Loading model…</div>
<script type="module">
// Sandboxed viewer shell. This document runs in an opaque origin
// (sandbox="allow-scripts", no allow-same-origin): the LLM-influenced
// generated code cannot touch the parent application's origin, cookies or
// storage. Communication with the parent is postMessage-only.
//
// Protocol (parent -> iframe):
// {type:'init', bundleText, requestId?} — full ESM bundle
// {type:'set-option', option, value, requestId?} — wireframe/shadows
// {type:'reset-camera', requestId?} — restore initial view
// {type:'capture', requestId?} — request a PNG
// {type:'dispose', requestId?} — release WebGL resources
// Protocol (iframe -> parent):
// {type:'ready', stats, options, requestId?}
// {type:'option-applied', option, value, requestId?}
// {type:'option-error', option, message, requestId?}
// {type:'camera-reset'|'capture'|'disposed', ...}
// {type:'capture-error', code, message, requestId?}
// {type:'error', operation, message, requestId?}
const status = document.getElementById('status');
const viewerEl = document.getElementById('viewer');
let session = null;
let bootVersion = 0;
function post(message) {
// The parent's origin is unknown to us (opaque origin); '*' is acceptable
// here because the payloads contain no secrets and the parent validates
// event.source. Model code never sees these messages.
parent.postMessage(message, '*');
}
function response(message, request) {
const requestId = request && request.requestId;
if (typeof requestId === 'string' || typeof requestId === 'number') {
message.requestId = requestId;
}
return message;
}
function messageOf(error) {
return (error && error.message) || String(error);
}
function showStatus(message) {
status.textContent = message;
status.hidden = false;
}
function disposeSession() {
const active = session;
session = null;
if (!active) return null;
try {
active.dispose();
return null;
} catch (error) {
// A failed generated material disposer must not leave its canvas layered
// over the next session.
viewerEl.replaceChildren();
return error;
}
}
async function boot(bundleText, request) {
const version = ++bootVersion;
disposeSession();
showStatus('Loading model…');
const blob = new Blob([bundleText], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
try {
const mod = await import(url);
if (version !== bootVersion) return;
if (!document.createElement('canvas').getContext('webgl2') &&
!document.createElement('canvas').getContext('webgl')) {
throw new Error('WebGL is not available in this browser or GPU.');
}
const nextSession = mod.mountViewer(viewerEl, mod.makeModel,
typeof mod.makeLights === 'function' ? mod.makeLights : null, {});
if (version !== bootVersion) {
nextSession.dispose();
return;
}
session = nextSession;
status.hidden = true;
post(response({
type: 'ready',
stats: session.stats,
options: session.options,
}, request));
} catch (error) {
if (version !== bootVersion) return;
const message = messageOf(error);
showStatus(`The generated model failed to render: ${message}`);
post(response({ type: 'error', operation: 'init', message }, request));
} finally {
URL.revokeObjectURL(url);
}
}
function setOption(data) {
const option = data.option;
const value = data.value ?? data.enabled;
try {
if (!session) throw new Error('viewer not ready');
if (typeof value !== 'boolean') throw new Error('option value must be a boolean');
if (option === 'wireframe') {
session.setWireframe(value);
} else if (option === 'shadows') {
session.setShadows(value);
} else {
throw new Error(`unsupported viewer option: ${String(option)}`);
}
post(response({ type: 'option-applied', option, value }, data));
} catch (error) {
post(response({
type: 'option-error',
option,
message: messageOf(error),
}, data));
}
}
window.addEventListener('message', (event) => {
if (event.source !== parent) return;
const data = event.data;
if (!data || typeof data !== 'object') return;
if (data.type === 'init' && typeof data.bundleText === 'string') {
void boot(data.bundleText, data);
} else if (data.type === 'set-option') {
setOption(data);
} else if (data.type === 'reset-camera') {
try {
if (!session) throw new Error('viewer not ready');
session.resetCamera();
post(response({ type: 'camera-reset' }, data));
} catch (error) {
post(response({
type: 'error',
operation: 'reset-camera',
message: messageOf(error),
}, data));
}
} else if (data.type === 'capture') {
try {
if (!session) throw new Error('viewer not ready');
void session.capture().then((dataUrl) => {
post(response({ type: 'capture', dataUrl }, data));
}).catch((error) => {
post(response({
type: 'capture-error',
code: error && error.code ? error.code : 'capture-failed',
message: messageOf(error),
}, data));
});
} catch (error) {
post(response({
type: 'capture-error',
code: 'viewer-not-ready',
message: messageOf(error),
}, data));
}
} else if (data.type === 'dispose') {
++bootVersion;
const error = disposeSession();
showStatus(error ? 'Viewer disposal encountered an error.' : 'Viewer disposed.');
if (error) {
post(response({
type: 'error',
operation: 'dispose',
message: messageOf(error),
}, data));
} else {
post(response({ type: 'disposed' }, data));
}
}
});
// Signal to the parent that the shell itself loaded (watchdog baseline).
post({ type: 'shell-ready' });
</script>
</body>
</html>
|