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.tsandguide.ts, whereScanNodeandGuideNodeextend BaseNode with URL, transform, and opacity fields. - Upload management happens through
useUploadStoreinpackages/editor/src/store/use-upload.ts, which tracks progress and stores the resulting asset URL. - 3D rendering is handled by
ScanRendererandGuideRendererinpackages/viewer/src/components/renderers/, usinguseRegistryfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →