Agent guide - external renderers and Three.js
Decide whether this boundary fits
Section titled “Decide whether this boundary fits”Use defineRenderLayer for a framework-neutral external renderer, or defineThreeLayer from @pibbl/three for ordinary Three scenes. Read the Three guide first. The adapter is not a Three JSX reconciler or a parallel retained scene graph.
The result is one composited bitmap at one position in Canvas paint order. Pibbl primitives can paint before or after it; they cannot interleave among individual Three objects. HtmlBox remains above Canvas. External layers require OffscreenCanvas, transferToImageBitmap(), and the selected renderer’s capabilities. WebXR is outside the ordinary composited-layer contract.
Ownership and lifecycle
Section titled “Ownership and lifecycle”Define the layer once at module scope. Each mounted identity owns one persistent resource lifetime. The Three adapter creates a transparent renderer and its input bridge; the application creates the scene, camera, geometry, materials, textures, loaders, controls, mixers, and effects it needs.
Use create for initial resources, update for changed props, resize for camera/projection and renderer-dependent sizing, and dispose for application resources. The adapter disposes its renderer and bridge. Do not assume disposing a scene frees its geometry or materials. Track shared assets explicitly so they are neither leaked nor disposed twice.
An asynchronous loader may finish after the owner is gone. The application must prevent publishing that stale result and release late resources. Use the actual loader’s cancellation mechanism when it provides one; do not invent a Pibbl loader API. Keep a useful fallback while assets are unavailable.
Frames and interaction
Section titled “Frames and interaction”Advance controls, mixers, or effects from the supplied Pibbl frame. Request another frame only while work remains. Do not start another requestAnimationFrame loop or call Three’s setAnimationLoop for a normal Pibbl integration.
Default raycasting distinguishes target, block, and miss. A registered object or descendant becomes its logical Pibbl target. Other hit geometry blocks lower Canvas paint. Miss behavior chooses pass-through, block, or target-layer. A visually empty region is not necessarily a pass-through region unless configured that way.
Connect DOM-style controls through context.input as documented. Pibbl still owns coordinate arbitration, capture/bubble routing, pointer capture, focus, and the outer composition. Do not independently bind controls to the visible Canvas and thereby bypass Pibbl’s competing targets.
Complete example and assets
Section titled “Complete example and assets”The source below is the existing Three Layer lesson. Open it in the playground. Install matching @pibbl/core, @pibbl/three, and three packages, compile with Pibbl JSX, and call its default mount on an attached canvas. This example builds geometry procedurally, so it does not require a model download. Preserve its material/geometry cleanup when substituting a loaded model.
For product assets, specify format, origin/CORS needs, dimensions or bounding box, intended camera framing, loading state, failure state, and ownership. A concept image is not proof a loader or effect has been integrated.
Verification
Section titled “Verification”Assert one create per mounted identity and an update on prop changes. Resize at two aspect ratios and verify camera projection plus logical/backing dimensions. Test target, unregistered occluder, and empty-space miss separately, including a Canvas target behind the layer.
Test focus and controls without duplicate native listeners. Remove/remount during asset loading and confirm late resources are released. Confirm animation stops when idle and all application resources dispose once. Inspect full composition pixels after state/ownership checks; a nonblank WebGL canvas alone does not prove Pibbl composition or input routing.
Complete source
Section titled “Complete source”Host setup for this source: use a canvas with width 720 and height 420, compile with jsx: "react-jsx" and jsxImportSource: "@pibbl/core", import its default mount, and call const controller = mount(canvas) after attaching the canvas. Call controller.dispose() before removing it.
three-layer.example.tsx
Section titled “three-layer.example.tsx”import { dropShadow } from '@pibbl/core/filters';/** @jsxImportSource @pibbl/core */import { FocusManagement, Group, Rectangle, Text, pibbl, useSignal, type PibblController,} from '@pibbl/core';import { defineThreeLayer } from '@pibbl/three';import { Color, HemisphereLight, IcosahedronGeometry, Mesh, MeshPhongMaterial, PerspectiveCamera, Scene,} from 'three';
interface ThreeLayerProps { readonly turn: number; readonly onTarget: () => void;}
interface ThreeLayerResources { readonly scene: Scene; readonly camera: PerspectiveCamera; readonly blocker: Mesh<IcosahedronGeometry, MeshPhongMaterial>; readonly target: Mesh<IcosahedronGeometry, MeshPhongMaterial>;}
const ThreeScene = defineThreeLayer<ThreeLayerProps, ThreeLayerResources>({ renderer: { antialias: true }, create() { const scene = new Scene(); const camera = new PerspectiveCamera(45, 2, 0.1, 100); camera.position.set(0, 0, 7);
scene.add(new HemisphereLight('#dbeafe', '#172554', 2.8));
const blocker = new Mesh( new IcosahedronGeometry(1.05, 1), new MeshPhongMaterial({ color: new Color('#38bdf8'), shininess: 72 }), ); blocker.position.x = -1.6; scene.add(blocker);
const target = new Mesh( new IcosahedronGeometry(1.05, 1), new MeshPhongMaterial({ color: new Color('#fbbf24'), emissive: new Color('#5b2500'), shininess: 96, }), ); target.position.x = 1.6; scene.add(target);
return { scene, camera, blocker, target }; }, update(resources, props) { resources.blocker.rotation.set(props.turn * 0.7, props.turn, 0); resources.target.rotation.set(props.turn, props.turn * 0.6, 0.2); }, resize(resources, size) { resources.camera.aspect = size.width / size.height; resources.camera.updateProjectionMatrix(); }, targets(resources, props) { return [ { object: resources.target, handlers: { onClick: props.onTarget }, cursor: 'pointer', keyboardFocusable: true, }, ]; }, dispose(resources) { resources.blocker.geometry.dispose(); resources.blocker.material.dispose(); resources.target.geometry.dispose(); resources.target.material.dispose(); },});
function ThreeLayerLesson() { const state = useSignal({ backgroundClicks: 0, targetClicks: 0 }); const current = state.get(); const activateTarget = () => state.update((value) => ({ ...value, targetClicks: value.targetClicks + 1, }));
return ( <FocusManagement> <Group> <Rectangle style={{ width: 720, height: 420, fill: '#0f2742', cursor: 'pointer', }} onClick={() => state.update((value) => ({ ...value, backgroundClicks: value.backgroundClicks + 1, })) } /> <Group style={{ translateX: 70, translateY: 82 }}> <ThreeScene turn={current.targetClicks * 0.35} onTarget={activateTarget} missBehavior="pass-through" style={{ width: 580, height: 260, filter: [ dropShadow({
offsetX: 0, offsetY: 10, blurRadius: 18, color: '#0009', }), ], }} /> </Group> <Text pointerEvents="none" style={{ left: 28, top: 36, fill: '#f8fafc', font: '700 22px Arial' }} > Three.js inside Pibbl </Text> <Text pointerEvents="none" style={{ left: 28, top: 62, fill: '#bfdbfe', font: '14px Arial' }} > Blue blocks the Canvas · gold is a Pibbl target · empty pixels pass through </Text> <Text pointerEvents="none" style={{ left: 28, top: 392, fill: '#f8fafc', font: '600 14px Arial' }} > {`Canvas ${current.backgroundClicks} · Three target ${current.targetClicks}`} </Text> </Group> </FocusManagement> );}
export default function mount(canvas: HTMLCanvasElement): PibblController { return pibbl(canvas, <ThreeLayerLesson />);}Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision c321a83. ALPHA — NOT FOR PRODUCTION USE.