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

> Learn the complete workflow for integrating 3D scans and guide images into Pascal Editor scenes. Discover how a three-layer architecture handles GLTF models and image planes in the 3D canvas.

- Repository: [Pascal/editor](https://github.com/pascalorg/editor)
- Tags: how-to-guide
- Published: 2026-03-25

---

**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`](https://github.com/pascalorg/editor/blob/main/packages/core/src/schema/nodes/scan.ts) validates GLTF/GLB model references with 3D transform capabilities:

```typescript
// 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`](https://github.com/pascalorg/editor/blob/main/packages/core/src/schema/nodes/guide.ts) handles image textures with identical transform fields:

```typescript
// 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`](https://github.com/pascalorg/editor/blob/main/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`](https://github.com/pascalorg/editor/blob/main/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:

```typescript
// 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`](https://github.com/pascalorg/editor/blob/main/packages/viewer/src/components/renderers/scan/scan-renderer.tsx) handles 3D model visualization:

```tsx
// 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`](https://github.com/pascalorg/editor/blob/main/packages/viewer/src/components/renderers/guide/guide-renderer.tsx) handles 2D image projection:

```tsx
// 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`](https://github.com/pascalorg/editor/blob/main/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:

```tsx
// 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

```typescript
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

```tsx
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

```typescript
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`](https://github.com/pascalorg/editor/blob/main/packages/core/src/schema/nodes/scan.ts) and [`guide.ts`](https://github.com/pascalorg/editor/blob/main/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`](https://github.com/pascalorg/editor/blob/main/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`](https://github.com/pascalorg/editor/blob/main/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`](https://github.com/pascalorg/editor/blob/main/scan-renderer.tsx), while guide images support standard **JPEG/PNG textures** loaded as Three.js Textures in [`guide-renderer.tsx`](https://github.com/pascalorg/editor/blob/main/guide-renderer.tsx).

### How does the upload state machine track file upload progress?

The `useUploadStore` defined in [`packages/editor/src/store/use-upload.ts`](https://github.com/pascalorg/editor/blob/main/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.