How to open, embed, and automate the HiStruct 3D viewer
Load a scene by constructing a URL. All schemes work without any server or authentication.
| Scheme | Example | Notes |
|---|---|---|
?url= |
https://viewer.histruct.com/?url=https%3A%2F%2Fcdn.example.com%2Fscene.json |
Fetches JSON from the given URL. Server must send Access-Control-Allow-Origin: *. URL must be percent-encoded. |
?data= |
https://viewer.histruct.com/?data=eyJtZXRhZGF0YSI6… |
Base64url-encoded hiScene JSON in the query string. Good for short scenes. Visible in browser history. |
#<base64url> |
https://viewer.histruct.com/#eyJtZXRhZGF0YSI6… |
Recommended for sharing. Hash is never sent to the server. Use the Share button in the toolbar to generate one automatically. |
When multiple params are present, the viewer applies them in this order: ?url > ?data > #hash. Interactive UI is shown when none are present.
// JavaScript — encode a hiScene JSON string as a shareable hash URL
function toViewerUrl(jsonString, base) {
base = base || 'https://viewer.histruct.com/';
var b64 = btoa(unescape(encodeURIComponent(jsonString)))
.replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
return base + '#' + b64;
}
// Decode (viewer does this internally, but useful in agents/tests)
function fromBase64url(b64) {
b64 = b64.replace(/-/g,'+').replace(/_/g,'/');
while (b64.length % 4) b64 += '=';
return decodeURIComponent(escape(atob(b64)));
}
# Python — same encoding
import base64, json
def to_viewer_url(scene_dict, base='https://viewer.histruct.com/'):
json_bytes = json.dumps(scene_dict, separators=(',', ':')).encode('utf-8')
b64 = base64.urlsafe_b64encode(json_bytes).rstrip(b'=').decode()
return base + '#' + b64
def from_base64url(b64: str) -> dict:
padding = 4 - len(b64) % 4
b64 += '=' * (padding % 4)
return json.loads(base64.urlsafe_b64decode(b64).decode('utf-8'))
# PowerShell — encode a .json file as a viewer URL
function ConvertTo-ViewerUrl {
param([string]$JsonPath, [string]$Base = 'https://viewer.histruct.com/')
$bytes = [System.IO.File]::ReadAllBytes($JsonPath)
$b64 = [Convert]::ToBase64String($bytes) `
-replace '\+','-' -replace '/','_' -replace '=',''
return "${Base}#${b64}"
}
# Usage:
ConvertTo-ViewerUrl -JsonPath '.\expected-scene.json'
Push a scene into an already-open viewer window from any script on the same machine or from a parent frame.
// Works from: parent frame, browser extension, or Puppeteer/Playwright automation
viewerWindow.postMessage({ hiScene: jsonString }, 'https://viewer.histruct.com');
// When origin is unknown / relaxed:
viewerWindow.postMessage({ hiScene: jsonString }, '*');
The viewer does not currently emit a reply event for hiScene, but you can listen for the underlying info:newScene round-trip if needed (see Message Protocol).
Call postMessage again at any time. The viewer replaces the current scene.
Embed the viewer in any web page. Pass scenes via URL or via postMessage from the parent after load.
<!-- Basic embed — scene from URL hash -->
<iframe
id="hs-viewer"
src="https://viewer.histruct.com/#eyJtZXRhZGF0YSI6…"
width="100%" height="600"
frameborder="0"
allow="clipboard-write"></iframe>
// Push a new scene into the iframe after it loads
var frame = document.getElementById('hs-viewer');
frame.addEventListener('load', function() {
frame.contentWindow.postMessage({ hiScene: jsonString }, 'https://viewer.histruct.com');
});
// Or push at any time later:
frame.contentWindow.postMessage({ hiScene: anotherJsonString }, 'https://viewer.histruct.com');
Omit the hash to show the load panel inside the frame:
<iframe src="https://viewer.histruct.com/" width="100%" height="600" frameborder="0"></iframe>
This section is the handoff for AI agents (e.g. Golem / FemCAD Copilot) that generate hiScene JSON and want to display results in the browser.
After generating a scene, encode it and return a viewer URL. The user clicks the link; nothing else is needed.
import json, base64
def scene_to_viewer_url(scene: dict, base: str = 'https://viewer.histruct.com/') -> str:
"""
Encode a hiScene dict as a shareable viewer URL.
Safe for any scene size that fits in a browser URL (~8 KB practical limit for ?data=,
unlimited for #hash since the fragment is not sent to the server).
"""
raw = json.dumps(scene, separators=(',', ':')).encode('utf-8')
b64 = base64.urlsafe_b64encode(raw).rstrip(b'=').decode()
return f'{base}#{b64}'
# In an agent tool:
url = scene_to_viewer_url(generated_scene)
return f'[View in HiStruct Viewer]({url})'
If the agent serves a temporary HTTP endpoint (or uploads to a CDN/blob), pass the URL directly:
viewer_url = f'https://viewer.histruct.com/?url={requests.utils.quote(scene_endpoint_url)}'
The scene server must set Access-Control-Allow-Origin: *. A FastAPI one-liner:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=['*'], allow_methods=['GET'])
@app.get('/scene/{id}')
def get_scene(id: str):
return scenes[id] # dict → auto-serialised as JSON
Open the viewer, wait for it to be ready, then push scenes programmatically:
from playwright.async_api import async_playwright
import json, asyncio
async def display_scene(scene: dict):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
await page.goto('https://viewer.histruct.com/')
# Wait until viewer signals readiness (watches window.__hs.isReady)
await page.wait_for_function("window.__hs && window.__hs.isReady() === true", timeout=15000)
# Push scene
await page.evaluate(
"window.__hs.loadScene(scene)",
json.dumps(scene)
)
Each gist in the FemCAD knowledge base has an expected-scene.json. Use this to open it directly in the viewer:
# PowerShell helper — open a gist's expected scene in the browser
function Open-GistInViewer {
param(
[string]$GistDir,
[string]$ViewerBase = 'https://viewer.histruct.com/'
)
$json = Get-Content "$GistDir\expected-scene.json" -Raw
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$b64 = [Convert]::ToBase64String($bytes) `
-replace '\+','-' -replace '/','_' -replace '=',''
Start-Process "${ViewerBase}#${b64}"
}
# Usage:
Open-GistInViewer '.\TestData\UnitTestsKnowledgeBase\gists\gist-001-basic-block'
Add this tool to Golem's server.py to let the agent return a viewer link from any tool call:
import json, base64
from mcp.server import tool
VIEWER_BASE = 'https://viewer.histruct.com/'
@tool(name='open_in_viewer',
description='Encode a hiScene JSON dict as a HiStruct Viewer URL and return it to the user.')
def open_in_viewer(scene: dict) -> str:
"""Returns a shareable URL that opens the scene in the HiStruct Viewer."""
raw = json.dumps(scene, separators=(',', ':')).encode('utf-8')
b64 = base64.urlsafe_b64encode(raw).rstrip(b'=').decode()
url = f'{VIEWER_BASE}#{b64}'
return f'[View scene in HiStruct Viewer]({url})\n\n`{url}`'
Advanced — only needed if you're wrapping the viewer in a WebView or want to react to viewer events.
{type}:{url}:{id}:{status}::{data}
type "info" = Notification | "req" = Request | "res" = Response
url message type identifier, e.g. "newScene", "ready"
id correlation token (timestamp or counter)
status "ok" | "error"
data everything after the first "::" — usually JSON
// Works from the same page or from a parent frame
var sceneId = Date.now();
window.postMessage(
{ msgRPC: 'info:newScene:' + sceneId + ':ok::' + jsonString },
'*'
);
// Override window.external BEFORE the viewer bundles load.
// The viewer calls this when Aurelia has finished bootstrapping.
window.external = {
sendMessage: function(msg) {
if (typeof msg === 'string' && msg.indexOf(':ready:') !== -1) {
// viewer is ready — safe to postMessage newScene now
}
}
};
| Requirement | Value |
|---|---|
| Local viewer mode flag | window.localViewer = true (set before bundles load) |
| Aurelia mount point | <div id="model-viewer-app"> |
| Kendo culture | kendo.culture("en-US") |
| Hidden inputs | <input id="SpaceBaseUrl">, <input id="PageUiLanguage"> |
The ?url= scheme fetches scene JSON from a third-party server. That server must include the response header:
Access-Control-Allow-Origin: *
If you control the scene server, add this header. If you don't, use the #hash or ?data= scheme instead (no network request needed).
| Server | How to enable CORS |
|---|---|
| FastAPI / Starlette | CORSMiddleware(allow_origins=['*']) |
| Express.js | app.use(require('cors')()) |
| nginx | add_header Access-Control-Allow-Origin *; |
| Azure Blob Storage | Set CORS rule in Storage Account → Resource sharing (CORS) |
| GitHub Gists (raw) | Already allows * — works out of the box |
| CDN (Azure CDN / Cloudfront) | Add CORS rule in CDN origin settings |
Generate a viewer URL from a JSON snippet: