Skip to main content
There are two ways to put Pascal in your product. Pick one before you start:
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 Pascal viewer embedded in the Pascal Capture iOS app

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

Replace 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

Add embed=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 of 1, true, or yes enables a flag.
Even with both bars hidden, visitors can still orbit, pan, and zoom the camera with the mouse or touch.
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.
The editor expects React 18 or 19, Next.js 15 or newer, three.js 0.186, @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
Then mount the editor once 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/viewer and three.
  • Point Tailwind at the packages’ files, which it doesn’t scan in node_modules by default, and make dark: follow the .dark class:
Adjust the paths to where your stylesheet sits. The editor also reads the theme tokens (colours, radii, fonts) that the open-source app defines in 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:
  • selectZoneOrRoom selects an enclosed room as a room (its panel and pill), and anything else as a zone.
  • frameViewerNode moves 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. source is a ViewerSource: { nodes, objectFor }, the scene’s nodes and a lookup from a node id to its rendered three.js object.
  • enterUnitFocus focuses a unit, which isn’t selectable on its own.
Nodes your app lists in 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’s PluginChatExtension 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.