HiStruct

Viewer — Integration API

How to open, embed, and automate the HiStruct 3D viewer

1. URL Schemes

Load a scene by constructing a URL. All schemes work without any server or authentication.

SchemeExampleNotes
?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.

Priority order

When multiple params are present, the viewer applies them in this order: ?url > ?data > #hash. Interactive UI is shown when none are present.

Encoding reference

// 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'

2. postMessage API

Push a scene into an already-open viewer window from any script on the same machine or from a parent frame.

Sending a scene

// 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 }, '*');

Receiving confirmation (optional)

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).

Replacing a scene

Call postMessage again at any time. The viewer replaces the current scene.

3. iframe Embed

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');

iframe with a blank scene picker

Omit the hash to show the load panel inside the frame:

<iframe src="https://viewer.histruct.com/" width="100%" height="600" frameborder="0"></iframe>

4. Golem / Agent Integration

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.

Pattern A — Pre-encoded URL (recommended for agents)

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})'

Pattern B — Hosted JSON + ?url= param

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

Pattern C — Selenium / Playwright automation

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)
        )

Pattern D — From gist test output (CI / knowledge-base)

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'

MCP tool stub (for Golem's MCP server)

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}`'

5. Internal Message Protocol

Advanced — only needed if you're wrapping the viewer in a WebView or want to react to viewer events.

Message format

{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

Sending a scene (host → viewer)

// Works from the same page or from a parent frame
var sceneId = Date.now();
window.postMessage(
  { msgRPC: 'info:newScene:' + sceneId + ':ok::' + jsonString },
  '*'
);

Detecting viewer ready (WebView hosts)

// 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
    }
  }
};

Required page setup

RequirementValue
Local viewer mode flagwindow.localViewer = true (set before bundles load)
Aurelia mount point<div id="model-viewer-app">
Kendo culturekendo.culture("en-US")
Hidden inputs<input id="SpaceBaseUrl">, <input id="PageUiLanguage">

6. CORS Notes

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).

Common servers

ServerHow to enable CORS
FastAPI / StarletteCORSMiddleware(allow_origins=['*'])
Express.jsapp.use(require('cors')())
nginxadd_header Access-Control-Allow-Origin *;
Azure Blob StorageSet 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

7. Try It

Generate a viewer URL from a JSON snippet: