A public project is visible to anyone who has its link, so the hosted viewer is not a
private way to show client work. See Share a project link.
Hosted viewer
Any public Pascal project can be embedded on another site with a plain<iframe>
pointing at its viewer page. Visitors get the full interactive viewer — orbiting the
camera, toggling levels and wall cutaways, and walking through the scene in first
person — with no accounts, SDKs, or scripts required on your side.
We rely on the same embed ourselves: the Pascal Capture iOS app
shows scenes through this exact viewer.

The embedded viewer inside the Pascal Capture iOS app — levels panel and controls bar included
Requirements
- The project must be public. Flip the visibility control in the editor’s top bar from Private to Public. Private projects render an access screen instead of the scene.
- Grab the URL with the Copy viewer link button next to the visibility control, or copy it from the address bar on the project’s viewer page. It has the form:
Basic embed
PROJECT_ID with your project’s id (the last path segment of your viewer
link). For a responsive embed, wrap the iframe in a container with a fixed aspect
ratio:
Embed mode for listings
Addembed=1 to show the scene the way the editor’s Preview does: the scene header
(project name, breadcrumb and levels), the controls bar and the 2D/3D switcher. The
community’s owner card, likes, Duplicate button, collections and the “Get Started” card are
hidden.
When the scene is loaded and the opening view has landed, the frame posts one message
to its parent, so your page can reveal the frame without showing the default view
first:
URL parameters
The viewer accepts two flags to reduce its chrome inside embeds. A value of1,
true, or yes enables a flag.
On another site, the viewer always uses the embed presentation: the owner card, likes,
Duplicate button, collections and the “Get Started” card are hidden even without
embed=1, so visitors can’t be tricked into acting on their Pascal account.What the embed serves
Embeds load the same thing your shared viewer link does: once a published project has finished baking, the viewer serves the optimized baked copy for fast loading, with doors and windows still interactive. Until a fresh bake is ready, it falls back to rendering the live scene. Republishing your project updates every embed automatically — the iframe URL never changes.Current limits
- An iframe is the only supported embed for hosted projects. There is no oEmbed
endpoint or JavaScript API; the frame posts one message,
pascal:viewer-ready, and accepts none. - Only the read-only viewer of a hosted project can be embedded, not the editor. To build the editor into your own React app, see Host the editor in a React app.
- A self-hosted editor has its own public embeds at
/embed/<id>. See Share a project publicly.
Host the editor in a React app
The open-source editor is also a React component,Editor from @pascal-app/editor.
Your app supplies the scene and saves it; the component brings the tools, panels, the
floor plan and the 3D view. Everything below is in version 1.0.5 of the packages.
@react-three/fiber 9 and @react-three/drei 10 in your app. Load the built-in
node definitions once, before you mount it:
registry.ts
registryReady has resolved. Without the node definitions,
the scene can’t render:
onLoad resolves to the scene graph to open (or null), and onSave receives the
scene graph to store. The editor’s own app,
apps/editor/components/scene-loader.tsx,
is a complete host with persistence. Your app owns authentication, access control and
storage: before you ship, make an edit, let it save, reload the page and check that
onLoad opens the saved scene.
Styles and build setup
@pascal-app/editor ships TypeScript source and styles itself with Tailwind CSS v4
utility classes. It ships no compiled stylesheet. In your app:
- Add the editor to Next.js
transpilePackages, with@pascal-app/core,@pascal-app/viewerandthree. - Point Tailwind at the packages’ files, which it doesn’t scan in
node_modulesby default, and makedark:follow the.darkclass:
apps/editor/app/globals.css;
copy them, or map them to your own design tokens.
Host props
onViewerBreadcrumb forwards the onBreadcrumb prop of Viewer from
@pascal-app/viewer. If you mount the read-only Viewer on its own, pass
onBreadcrumb there. Both use the ViewerBreadcrumb and ViewerBreadcrumbHandler
types that @pascal-app/viewer exports.
Light and dark chrome
The editor sets no theme of its own: its chrome follows the nearest.dark class.
Leave the class off for a light editor. Menus and tooltips render into <body>, so for
a dark editor put dark on <body> or <html>. The Preview stage, the walkthrough
HUD and the controls bar stay dark in either theme, and the 3D scene keeps its own
Scene theme.
To let people pick, pass InterfaceThemeRow through SettingsPanel’s preferences
slot. It’s the System, Light and Dark row; applying the choice is up to your app:
InterfaceTheme is 'system' | 'light' | 'dark'. Without the row, Preferences shows
no theme setting.
Select and show a node
These are the helpers the editor’s own lists use, so a node selected from your UI behaves the same way:selectZoneOrRoomselects an enclosed room as a room (its panel and pill), and anything else as a zone.frameViewerNodemoves the camera to the node: its saved view if it has one, else its bounding box. When only the 2D plan is on screen, the plan glides to fit the node instead. Under reduced motion, it jumps.sourceis aViewerSource:{ nodes, objectFor }, the scene’s nodes and a lookup from a node id to its rendered three.js object.enterUnitFocusfocuses a unit, which isn’t selectable on its own.
useViewer’s pointedIds (for example, the ones a chat
message mentions) are highlighted in the plan, as a room outline, and in 3D.
PLUGINS_VIEW_ID names the plugin directory view. Open it with
openEditorView(PLUGINS_VIEW_ID).
Plugin start screens in your chat
A plugin’sPluginChatExtension can add a start screen that your chat shows while a
thread is empty. start lazily loads a component that receives ChatStartProps:
IconRefGlyph ({ icon, size? }) and IconRefImage ({ src, size?, className? })
render registry icons the way the editor does. See
Create a plugin for the rest of the plugin contract.