Integrating 3D Scans and Guide Images into Pascal Editor Scenes: Complete Workflow Guide

Pascal Editor treats 3D scans and guide images as first-class scene nodes, using a three-layer architecture of Zod schema validation, upload state management, and specialized React renderers to display GLTF models and image planes in the 3D canvas.

The pascalorg/editor repository provides a comprehensive framework for importing external reference assets into 3D environments. Understanding how to integrate 3D scans and guide images into Pascal Editor scenes requires examining the node schema definitions, upload lifecycle management, and renderer implementations that bridge data models with Three.js visualization.

Node Schema Architecture

Pascal Editor defines reference assets through strict Zod schemas that extend the BaseNode interface used throughout the core library. These schemas enforce type safety and default values for transform properties.

ScanNode Definition

The ScanNode schema in packages/core/src/schema/nodes/scan.ts validates GLTF/GLB model references with 3D transform capabilities:

// packages/core/src/schema/nodes/scan.ts
export const ScanNode = BaseNode.extend({
  id: objectId('scan'),
  type: nodeType('scan'),
  url: z.string(),
  position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
  rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
  scale: z.number().default(1),
  opacity: z.number().min(0).max(100).default(100),
})
export type ScanNode = z.infer<typeof ScanNode>

GuideNode Definition

The GuideNode schema in packages/core/src/schema/nodes/guide.ts handles image textures with identical transform fields:

// packages/core/src/schema/nodes/guide.ts
export const GuideNode = BaseNode.extend({
  id: objectId('guide'),
  type: nodeType('guide'),
  url: z.string(),
  position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
  rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
  scale: z.number().default(1),
  opacity: z.number().min(0).max(100).default(100),
})
export type GuideNode = z.infer<typeof GuideNode>

Both types export from packages/core/src/schema/index.ts, making them available to the viewer (@pascal-app/core) and editor packages.

Upload State Management

When users drag files into the Reference panel, the editor initiates uploads through useUploadStore defined in packages/editor/src/store/use-upload.ts. The store implements a state machine tracking the full upload lifecycle.

The UploadEntry interface manages progress and result storage:

// packages/editor/src/store/use-upload.ts
export interface UploadEntry {
  status: UploadStatus
  assetType: 'scan' | 'guide'
  fileName: string
  progress: number
  error: string | null
  resultUrl: string | null
}

After calling useUploadStore.startUpload(levelId, assetType, fileName), the store tracks progress percentage and stores the final public URL in resultUrl. Upon completion, the editor creates a new node via ScanNode.parse() or GuideNode.parse() and inserts it into the scene graph using useScene.getState().addNode().

Rendering Pipeline

The viewer package registers node IDs with the scene registry and renders assets through specialized React components that map data to Three.js objects.

Displaying GLTF Scans

The ScanRenderer in packages/viewer/src/components/renderers/scan/scan-renderer.tsx handles 3D model visualization:

// packages/viewer/src/components/renderers/scan/scan-renderer.tsx
export const ScanRenderer = ({ node }: { node: ScanNode }) => {
  const showScans = useViewer(s => s.showScans)
  const ref = useRef<Group>(null!)
  useRegistry(node.id, 'scan', ref)

  const resolvedUrl = useAssetUrl(node.url)

  return (
    <group
      position={node.position}
      rotation={node.rotation}
      scale={[node.scale, node.scale, node.scale]}
      visible={showScans}
      ref={ref}
    >
      {resolvedUrl && (
        <Suspense>
          <ScanModel opacity={node.opacity} url={resolvedUrl} />
        </Suspense>
      )}
    </group>
  )
}

The component uses a custom GLTF-KTX2 hook to load models, then walks the scene graph to set material properties including opacity and transparency. It disables ray-casting and bounding-box calculations for performance optimization.

Displaying Image Guides

The GuideRenderer in packages/viewer/src/components/renderers/guide/guide-renderer.tsx handles 2D image projection:

// packages/viewer/src/components/renderers/guide/guide-renderer.tsx
export const GuideRenderer = ({ node }: { node: GuideNode }) => {
  const showGuides = useViewer(s => s.showGuides)
  const ref = useRef<Group>(null!)
  useRegistry(node.id, 'guide', ref)

  const resolvedUrl = useAssetUrl(node.url)

  return (
    <group position={node.position} rotation={[0, node.rotation[1], 0]} visible={showGuides} ref={ref}>
      {resolvedUrl && (
        <Suspense>
          <GuidePlane opacity={node.opacity} scale={node.scale} url={resolvedUrl} />
        </Suspense>
      )}
    </group>
  )
}

This renderer loads the image as a Three.js Texture, applies it to a MeshBasicNodeMaterial respecting the opacity value, and renders a flat plane that ignores frustum culling to remain always visible.

Editor UI Integration

The Reference panel in packages/editor/src/components/ui/panels/reference-panel.tsx provides the interface for adding and managing reference assets. The panel handles file selection, invokes the upload store, and creates nodes upon successful uploads:

// packages/editor/src/components/ui/panels/reference-panel.tsx (excerpt)
type ReferenceNode = ScanNode | GuideNode

const handleAdd = (type: 'scan' | 'guide') => {
  // open file picker → startUpload → on success:
  const newNode: ReferenceNode = type === 'scan'
    ? ScanNode.parse({ id: newId, url: uploadedUrl, ...defaultTransform })
    : GuideNode.parse({ id: newId, url: uploadedUrl, ...defaultTransform })
  useScene.getState().addNode(newNode, parentLevelId)
}

The panel subscribes to useUploadStore to display real-time progress bars and exposes controls for renaming assets, toggling visibility, and editing transform properties (position, rotation, scale, opacity).

Practical Implementation Examples

Adding a Scan Node After Upload

import { ScanNode } from '@pascal-app/core'
import { useScene } from '@pascal-app/core'

// after upload finishes
const addScan = (levelId: string, url: string) => {
  const node = ScanNode.parse({
    id: crypto.randomUUID(),
    url,
    position: [0, 0, 0],
    rotation: [0, 0, 0],
    scale: 1,
    opacity: 100,
  })
  useScene.getState().addNode(node, levelId)
}

Toggling Guide Visibility Globally

import useViewer from '@pascal-app/viewer/store/use-viewer'

const ToggleGuides = () => {
  const { showGuides, setShowGuides } = useViewer()
  return (
    <button onClick={() => setShowGuides(!showGuides)}>
      {showGuides ? 'Hide' : 'Show'} Guides
    </button>
  )
}

Updating Scan Opacity Programmatically

import { useScene } from '@pascal-app/core'

const setScanOpacity = (nodeId: string, opacity: number) => {
  useScene.getState().updateNode(nodeId, (node) => ({
    ...node,
    opacity: Math.min(Math.max(opacity, 0), 100),
  }))
}

Summary

  • Schema validation occurs in packages/core/src/schema/nodes/scan.ts and guide.ts, where ScanNode and GuideNode extend BaseNode with URL, transform, and opacity fields.
  • Upload management happens through useUploadStore in packages/editor/src/store/use-upload.ts, which tracks progress and stores the resulting asset URL.
  • 3D rendering is handled by ScanRenderer and GuideRenderer in packages/viewer/src/components/renderers/, using useRegistry for fast node lookups and Three.js for visualization.
  • User interface controls reside in packages/editor/src/components/ui/panels/reference-panel.tsx, enabling drag-and-drop uploads and real-time property editing.
  • State propagation flows automatically from the core scene store to React renderers, ensuring UI changes immediately reflect in the 3D canvas.

Frequently Asked Questions

What file formats does Pascal Editor support for 3D scans and guide images?

According to the pascalorg/editor source code, 3D scans use GLTF/GLB models loaded via a custom GLTF-KTX2 hook in scan-renderer.tsx, while guide images support standard JPEG/PNG textures loaded as Three.js Textures in guide-renderer.tsx.

How does the upload state machine track file upload progress?

The useUploadStore defined in packages/editor/src/store/use-upload.ts maintains an UploadEntry interface with status, progress (percentage), and resultUrl fields. This enables the Reference panel to display progress bars and trigger node creation only after the upload completes successfully.

Can I programmatically update a scan's opacity after adding it to the scene?

Yes. Call useScene.getState().updateNode(nodeId, callback) with the new opacity value (0-100). The ScanRenderer automatically applies this value to the GLTF material properties, adjusting transparency in real-time without reloading the model.

What is the architectural difference between ScanRenderer and GuideRenderer?

ScanRenderer loads full 3D GLTF scenes with ray-casting disabled and respects the global showScans viewer state. GuideRenderer creates a flat plane with MeshBasicNodeMaterial that ignores frustum culling and only applies Y-axis rotation to keep images oriented correctly while supporting the global showGuides toggle.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →