The 2D canvas system controls where a 3D viewport sits — position, size, rotation, opacity — while the Three.js renderer controls what is drawn inside it. This split is what makes a 3D shape behave like any other canvas element.
ThreeDShapeRenderer creates a div with position: relative and overflow: hidden, then passes it to ThreeDShapeElement as the mount container. Inside, ThreeDEngine creates a WebGLRenderer, sets its DOM element to display: block with 100% width and height, and appends it to that container.
The host div is placed inside the 2D artboard by Canvas.tsx using absolute positioning:
<div
style={{
position: 'absolute',
left: el.x,
top: el.y,
width: el.width,
height: el.height,
opacity: el.opacity ?? 1,
overflow: 'hidden',
pointerEvents: activeThreeDElementId === el.id ? 'auto' : 'none',
zIndex: elements.indexOf(el) + 1,
borderRadius: el.borderRadius ?? 0,
}}
>
<ThreeDShapeRenderer ... />
</div>Zoom and pan need no special handling. The CSS transform on the artboard container applies to the 3D div automatically, because the div is a child of the artboard and inherits the transform.
A useEffect in ThreeDShapeRenderer calls resize() whenever the element dimensions change, which updates both the renderer size and the camera aspect ratio:
useEffect(() => {
if (instanceRef.current) {
instanceRef.current.resize(element.width, element.height);
}
}, [element.width, element.height]);Both the outer positioning div in Canvas.tsx and the inner container in ThreeDShapeRenderer set overflow: hidden. If a 3D viewport ever bleeds outside the canvas boundary, one of those two is missing.