Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions app/src/components/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as THREE from "three";
import { useEffect, useRef, memo, type RefObject } from "react";
import { useEffect, useRef, memo, useMemo, type RefObject } from "react";
import { Canvas, useThree } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
Expand Down Expand Up @@ -39,6 +39,7 @@ import { PathTracingRenderer } from "./PathTracingRenderer";
import { GeometryErrorBoundary } from "./three/GeometryErrorBoundary";
import { useFrameLoadTime } from "../hooks/useFrameLoadTime";
import { ScreenshotProvider } from "./three/ScreenshotProvider";
import { resolvePosition } from "../utils/cameraUtils";

/**
* Component configuration for geometry types.
Expand Down Expand Up @@ -234,6 +235,15 @@ function MyScene() {
const sessionCamera = sessionCameraKey ? geometries[sessionCameraKey] : null;
const sessionCameraData = sessionCamera?.data;

// Memoize camera position resolution (must be before early returns)
const cameraPosition = useMemo(
() =>
sessionCameraData?.position
? resolvePosition(sessionCameraData.position, geometries)
: ([0, 0, 10] as [number, number, number]),
[sessionCameraData?.position, geometries],
);

// Show error state if initialization failed
if (initializationError) {
return <CanvasErrorState error={initializationError} />;
Expand All @@ -251,7 +261,6 @@ function MyScene() {
const pathtracingSettings = settingsResponse.data.pathtracing;
const pathtracingEnabled = pathtracingSettings.enabled === true;

const cameraPosition = sessionCameraData.position as [number, number, number];
const cameraFov = sessionCameraData.fov;
const cameraType = sessionCameraData.camera_type;
const showCrosshair = sessionCameraData.show_crosshair;
Expand Down
29 changes: 7 additions & 22 deletions app/src/components/PathTracingRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ interface PathTracingRendererProps {

/**
* Wraps scene with GPU path tracing renderer and environment lighting.
* When disabled, passes children through without modification.
*
* IMPORTANT: When path tracing is enabled:
* When path tracing is enabled:
* - All user interactions (click, hover, selection) are disabled
* - Instanced meshes are converted to merged meshes (required for path tracer)
* - Studio lighting is automatically disabled (environment provides lighting)
Expand All @@ -24,11 +23,6 @@ export function PathTracingRenderer({
settings,
children,
}: PathTracingRendererProps) {
// Handle undefined settings (loading state)
if (!settings) {
return <>{children}</>;
}

const {
enabled = false,
min_samples = 1,
Expand All @@ -39,29 +33,20 @@ export function PathTracingRenderer({
environment_intensity = 1.0,
environment_blur = 0.0,
environment_background = false,
} = settings;

// Pass through if not enabled
if (!enabled) {
return <>{children}</>;
}

} = settings ?? {};
return (
<Pathtracer
minSamples={min_samples}
samples={samples}
bounces={bounces}
tiles={tiles}
enabled={true}
enabled={enabled}
>
{/* Pathtracing updater - watches for scene changes and calls update() */}
<PathtracingUpdater settings={settings} />

{/* Registers pathtracer capture function to store (DRY - ScreenshotProvider handles logic) */}
<PathtracingCaptureProvider />
{enabled && <PathtracingUpdater settings={settings!} />}
{enabled && <PathtracingCaptureProvider />}

{/* Environment lighting for path tracing */}
{environment_preset !== "none" && (
{/* Environment lighting - only when pathtracing enabled */}
{enabled && environment_preset !== "none" && (
<Environment
preset={environment_preset}
background={environment_background}
Expand Down
167 changes: 36 additions & 131 deletions app/src/components/three/Camera.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { useEffect, useMemo, useState } from "react";
import * as THREE from "three";
import { useThree } from "@react-three/fiber";
import { useAppStore } from "../../store";
import { isCurveAttachment, PositionType } from "../../utils/cameraUtils";
import {
isCurveAttachment,
resolvePosition,
PositionType,
} from "../../utils/cameraUtils";

interface CameraData {
// Position and target can be either direct coordinates or CurveAttachment
Expand Down Expand Up @@ -40,47 +44,21 @@ export default function Camera({
const attachedCameraKey = useAppStore((state) => state.attachedCameraKey);
const curveRefs = useAppStore((state) => state.curveRefs);

/**
* Resolve initial position from data.
* For arrays: use directly. For CurveAttachment: try to resolve from geometry data.
* Backend always provides valid data, so fallback cases should not occur.
*/
const resolveInitialPosition = (
positionData: PositionType,
): THREE.Vector3 => {
if (Array.isArray(positionData)) {
return new THREE.Vector3(
positionData[0],
positionData[1],
positionData[2],
);
}
// CurveAttachment - resolve from geometry data
if (isCurveAttachment(positionData)) {
const curveGeometry = geometries[positionData.geometry_key];
if (
curveGeometry?.type === "Curve" &&
curveGeometry.data?.position?.[0]
) {
const [x, y, z] = curveGeometry.data.position[0];
return new THREE.Vector3(x, y, z);
}
}
// Backend should always provide valid data - this indicates a bug
console.error("Camera: received invalid position data from backend");
throw new Error("Invalid camera position data");
};

const [computedPosition, setComputedPosition] = useState<THREE.Vector3>(() =>
resolveInitialPosition(data.position),
);
const [computedTarget, setComputedTarget] = useState<THREE.Vector3>(() =>
resolveInitialPosition(data.target),
// Initial position/target from shared utility (linear interpolation)
const [computedPosition, setComputedPosition] = useState<THREE.Vector3>(
() => {
const [x, y, z] = resolvePosition(data.position, geometries);
return new THREE.Vector3(x, y, z);
},
);
const [computedTarget, setComputedTarget] = useState<THREE.Vector3>(() => {
const [x, y, z] = resolvePosition(data.target, geometries);
return new THREE.Vector3(x, y, z);
});

const isAttached = attachedCameraKey === geometryKey;

// Extract curve info from position/target (either direct coords or CurveAttachment)
// Extract curve info from position/target
const positionCurveKey = isCurveAttachment(data.position)
? data.position.geometry_key
: null;
Expand All @@ -101,94 +79,38 @@ export default function Camera({
const targetCurve = targetCurveKey ? curveRefs[targetCurveKey] : undefined;

/**
* Helper: Resolve position from either direct coordinates or CurveAttachment.
* Returns null if CurveAttachment cannot be resolved (curve not loaded yet).
* Backend always provides valid data, so null indicates a timing issue, not invalid data.
* Resolve position to Vector3. Uses curveRef for smooth spline interpolation
* when available, falls back to resolvePosition (linear) otherwise.
*/
const resolvePositionToVector = (
const resolveToVector3 = (
positionData: PositionType,
curveKey: string | null,
curve: THREE.CatmullRomCurve3 | undefined,
progress: number,
): THREE.Vector3 | null => {
// Direct coordinates - just use them
if (Array.isArray(positionData)) {
return new THREE.Vector3(
positionData[0],
positionData[1],
positionData[2],
);
}

// CurveAttachment - resolve via curve
if (!curveKey) {
// Invalid CurveAttachment (no geometry_key) - backend bug
console.error(
`Camera ${geometryKey}: CurveAttachment missing geometry_key`,
);
return null;
}

if (curve) {
// Multi-point curve - use shared THREE.js curve object
): THREE.Vector3 => {
// Use curveRef for smooth spline interpolation when available
if (curve && isCurveAttachment(positionData)) {
return curve.getPointAt(progress);
} else {
// Single-point curve (or not yet built) - read position directly from geometry data
const curveGeometry = geometries[curveKey];
if (
curveGeometry?.type === "Curve" &&
curveGeometry.data?.position?.[0]
) {
const [x, y, z] = curveGeometry.data.position[0];
return new THREE.Vector3(x, y, z);
} else {
// Curve not loaded yet - will be resolved when geometry loads
return null;
}
}
// Fallback to linear interpolation via shared utility
const [x, y, z] = resolvePosition(positionData, geometries);
return new THREE.Vector3(x, y, z);
};

// Compute position (from direct coords or curve)
useEffect(() => {
const point = resolvePositionToVector(
const point = resolveToVector3(
data.position,
positionCurveKey,
positionCurve,
positionProgress,
);
// Only update if resolution succeeded (null = curve not loaded yet)
if (point !== null) {
setComputedPosition(point);
}
}, [
data.position,
positionCurve,
positionCurveKey,
positionProgress,
geometries,
geometryKey,
]);
setComputedPosition(point);
}, [data.position, positionCurve, positionProgress, geometries]);

// Compute target (from direct coords or curve)
useEffect(() => {
const point = resolvePositionToVector(
data.target,
targetCurveKey,
targetCurve,
targetProgress,
);
// Only update if resolution succeeded (null = curve not loaded yet)
if (point !== null) {
setComputedTarget(point);
}
}, [
data.target,
targetCurve,
targetCurveKey,
targetProgress,
geometries,
geometryKey,
]);
const point = resolveToVector3(data.target, targetCurve, targetProgress);
setComputedTarget(point);
}, [data.target, targetCurve, targetProgress, geometries]);

// Update scene camera if this camera is attached
useEffect(() => {
Expand All @@ -199,29 +121,13 @@ export default function Camera({
sceneCamera.lookAt(computedTarget);

// Update projection properties
let needsProjectionUpdate = false;

if ("fov" in sceneCamera) {
(sceneCamera as THREE.PerspectiveCamera).fov = data.fov;
needsProjectionUpdate = true;
}
if ("near" in sceneCamera) {
sceneCamera.near = data.near;
needsProjectionUpdate = true;
}
if ("far" in sceneCamera) {
sceneCamera.far = data.far;
needsProjectionUpdate = true;
}
if ("zoom" in sceneCamera) {
sceneCamera.zoom = data.zoom;
needsProjectionUpdate = true;
}

// Update projection matrix if any projection property changed
if (needsProjectionUpdate) {
sceneCamera.updateProjectionMatrix();
}
sceneCamera.near = data.near;
sceneCamera.far = data.far;
sceneCamera.zoom = data.zoom;
sceneCamera.updateProjectionMatrix();
}, [
isAttached,
computedPosition,
Expand All @@ -238,9 +144,8 @@ export default function Camera({
const helperCamera = useMemo(() => {
if (data.camera_type === "PerspectiveCamera") {
return new THREE.PerspectiveCamera(data.fov, 1.0, data.near, data.far);
} else {
return new THREE.OrthographicCamera(-1, 1, 1, -1, data.near, data.far);
}
return new THREE.OrthographicCamera(-1, 1, 1, -1, data.near, data.far);
}, [data.camera_type, data.fov, data.near, data.far]);

// Update helper camera position and target
Expand Down
5 changes: 3 additions & 2 deletions app/src/components/three/Curve.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -849,8 +849,9 @@ export default function Curve({
if (!marker || !virtual_marker) return null;
// Don't render if geometry is disabled OR if required keys are not available
if (fullData.active === false || !hasRequiredKeys) return null;
// Hide curve when pathtracing (Line components not supported by GPU pathtracer)
if (pathtracingEnabled) return null;
// Hide curve visuals when pathtracing (Line components not supported by GPU pathtracer)
// BUT keep the component mounted so curveRef stays registered for CurveAttachment resolution
if (pathtracingEnabled) return <group />;

// --- Render ---
return (
Expand Down
Loading
Loading