Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion apps/ui/app/components/cad-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Loader } from '#components/ui/loader.js';
import { GraphicsProvider } from '#hooks/use-graphics.js';
import { useCadPreview } from '#hooks/use-cad-preview.js';
import { cn } from '#utils/ui.utils.js';
import type { StageOptions } from '#components/geometry/graphics/three/stage.js';
import type { StageOptions } from '@taucad/three/react';

/**
* Visual rendering settings for the CAD preview viewer.
Expand Down
74 changes: 74 additions & 0 deletions apps/ui/app/components/geometry/cad/actor-bridge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useEffect } from 'react';
import type { ReactNode } from 'react';
import { useThree } from '@react-three/fiber';
import { useActorRef } from '@xstate/react';
import type { OrbitControls } from 'three/addons';
import { updateCameraFov } from '@taucad/three';
import { useViewerStore } from '@taucad/three/react';
import { controlsListenerMachine } from '#machines/controls-listener.machine.js';
import { useGraphics, useGraphicsSelector, useScreenshotCapability } from '#hooks/use-graphics.js';

/**
* Bridges Three.js context with XState actors and syncs graphics machine
* state to zustand viewer stores. Sets up screenshot capability, controls
* listeners, and FOV updates.
*/
export function ActorBridge(): ReactNode {
const { gl, scene, camera, controls, invalidate } = useThree();
const screenshotCapabilityActor = useScreenshotCapability();
const graphicsActor = useGraphics();

const cameraFovAngle = useGraphicsSelector((state) => state.context.cameraFovAngle);
const enableGrid = useGraphicsSelector((state) => state.context.enableGrid);
const enableAxes = useGraphicsSelector((state) => state.context.enableAxes);
const enableMatcap = useGraphicsSelector((state) => state.context.enableMatcap);
const enableSurfaces = useGraphicsSelector((state) => state.context.enableSurfaces);
const enableLines = useGraphicsSelector((state) => state.context.enableLines);
const enableGizmo = useGraphicsSelector((state) => state.context.enableGizmo);
const enablePostProcessing = useGraphicsSelector((state) => state.context.enablePostProcessing);

const setFieldOfView = useViewerStore((s) => s.setFieldOfView);
const setEnableGrid = useViewerStore((s) => s.setEnableGrid);
const setEnableAxes = useViewerStore((s) => s.setEnableAxes);
const setEnableMatcap = useViewerStore((s) => s.setEnableMatcap);
const setEnableSurfaces = useViewerStore((s) => s.setEnableSurfaces);
const setEnableLines = useViewerStore((s) => s.setEnableLines);
const setEnableGizmo = useViewerStore((s) => s.setEnableGizmo);
const setEnablePostProcessing = useViewerStore((s) => s.setEnablePostProcessing);

useEffect(() => {
screenshotCapabilityActor.send({
type: 'registerCapture',
gl,
scene,
camera,
});

return () => {
screenshotCapabilityActor.send({ type: 'unregisterCapture', captureMode: 'threejs' });
};
}, [gl, scene, camera, screenshotCapabilityActor]);

useEffect(() => {
updateCameraFov({ camera, cameraFovAngle, invalidate });
}, [cameraFovAngle, camera, invalidate]);

// Sync xstate graphics machine state → zustand viewer store
useEffect(() => { setFieldOfView(cameraFovAngle); }, [cameraFovAngle, setFieldOfView]);
useEffect(() => { setEnableGrid(enableGrid); }, [enableGrid, setEnableGrid]);
useEffect(() => { setEnableAxes(enableAxes); }, [enableAxes, setEnableAxes]);
useEffect(() => { setEnableMatcap(enableMatcap); }, [enableMatcap, setEnableMatcap]);
useEffect(() => { setEnableSurfaces(enableSurfaces); }, [enableSurfaces, setEnableSurfaces]);
useEffect(() => { setEnableLines(enableLines); }, [enableLines, setEnableLines]);
useEffect(() => { setEnableGizmo(enableGizmo); }, [enableGizmo, setEnableGizmo]);
useEffect(() => { setEnablePostProcessing(enablePostProcessing); }, [enablePostProcessing, setEnablePostProcessing]);

useActorRef(controlsListenerMachine, {
input: {
graphicsActorRef: graphicsActor,
controls: controls as OrbitControls,
},
});

return null;
}
108 changes: 99 additions & 9 deletions apps/ui/app/components/geometry/cad/cad-viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { memo } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import type { Geometry } from '@taucad/types';
import { GltfMesh } from '#components/geometry/graphics/three/react/gltf-mesh.js';
import { ThreeProvider } from '#components/geometry/graphics/three/three-context.js';
import type { ThreeViewerProperties } from '#components/geometry/graphics/three/three-context.js';
import { GltfMesh, CadCanvas } from '@taucad/three/react';
import type { StageOptions } from '@taucad/three/react';
import { SvgViewer } from '#components/geometry/graphics/svg/svg-viewer.js';
import { WebglErrorBoundary } from '#components/geometry/cad/webgl-error-boundary.js';
import { WebglErrorFallback } from '#components/geometry/cad/webgl-fallback.js';
import { WebglErrorFallback, WebglLimitFallback } from '#components/geometry/cad/webgl-fallback.js';
import { WebglContextLostFallback } from '#components/geometry/cad/webgl-context-lost-fallback.js';
import { ActorBridge } from '#components/geometry/cad/actor-bridge.js';
import { cn } from '#utils/ui.utils.js';
import { useWebglContextRef } from '#hooks/use-webgl-context-tracker.js';

type CadViewerProperties = ThreeViewerProperties & {
type CadViewerProperties = {
readonly geometries: Geometry[];
readonly enableSurfaces?: boolean;
readonly enableLines?: boolean;
readonly enableMatcap?: boolean;
readonly enableGizmo?: boolean;
readonly enableGrid?: boolean;
readonly enableAxes?: boolean;
readonly enableZoom?: boolean;
readonly enablePan?: boolean;
readonly enableDamping?: boolean;
readonly upDirection?: 'x' | 'y' | 'z';
readonly className?: string;
readonly enableCentering?: boolean;
readonly stageOptions?: StageOptions;
readonly zoomSpeed?: number;
readonly gizmoContainer?: HTMLElement | string;
};

export const CadViewer = memo(
Expand All @@ -20,11 +35,11 @@ export const CadViewer = memo(
enableSurfaces = true,
enableLines = true,
enableMatcap = false,
className,
...properties
}: CadViewerProperties): React.JSX.Element => {
const svgGeometries = geometries.filter((geometry) => geometry.format === 'svg');

// If there are any SVG geometries, we render them in a SVG viewer
if (svgGeometries.length > 0) {
return (
<SvgViewer enableGrid={properties.enableGrid} enableAxes={properties.enableAxes} geometries={svgGeometries} />
Expand All @@ -33,7 +48,7 @@ export const CadViewer = memo(

return (
<WebglErrorBoundary fallback={(errorProps) => <WebglErrorFallback {...errorProps} />}>
<ThreeProvider {...properties}>
<CadViewerCanvas className={className} {...properties}>
{geometries.map((geometry) => {
switch (geometry.format) {
case 'gltf': {
Expand Down Expand Up @@ -62,8 +77,83 @@ export const CadViewer = memo(
}
}
})}
</ThreeProvider>
</CadViewerCanvas>
</WebglErrorBoundary>
);
},
);

type CadViewerCanvasProperties = {
readonly children: React.ReactNode;
readonly enableGizmo?: boolean;
readonly enableGrid?: boolean;
readonly enableAxes?: boolean;
readonly enableZoom?: boolean;
readonly enablePan?: boolean;
readonly enableDamping?: boolean;
readonly upDirection?: 'x' | 'y' | 'z';
readonly className?: string;
readonly enableCentering?: boolean;
readonly stageOptions?: StageOptions;
readonly zoomSpeed?: number;
readonly gizmoContainer?: HTMLElement | string;
};

function CadViewerCanvas({
children,
className,
...properties
}: CadViewerCanvasProperties): React.JSX.Element {
const [isContextLost, setIsContextLost] = useState(false);

const webglRef = useWebglContextRef();

// eslint-disable-next-line react/hook-use-state -- one-time snapshot, setter intentionally unused
const [isOverLimit] = useState(() => {
if (!webglRef) {
return false;
}

const snap = webglRef.getSnapshot();
return snap.context.count >= snap.context.limit;
});

useEffect(() => {
if (!webglRef || isOverLimit) {
return;
}

webglRef.send({ type: 'acquire' });
return () => {
webglRef.send({ type: 'release' });
};
}, [webglRef, isOverLimit]);

const [canvasKey, setCanvasKey] = useState(0);
const handleRetry = useCallback(() => {
setIsContextLost(false);
setCanvasKey((previous) => previous + 1);
}, []);

if (isOverLimit) {
return <WebglLimitFallback onRetry={handleRetry} />;
}

if (isContextLost) {
return <WebglContextLostFallback onRetry={handleRetry} />;
}

return (
<CadCanvas
key={canvasKey}
className={cn('bg-background', className)}
onContextLost={() => {
setIsContextLost(true);
}}
{...properties}
>
{children}
<ActorBridge />
</CadCanvas>
);
}
52 changes: 0 additions & 52 deletions apps/ui/app/components/geometry/graphics/three/actor-bridge.tsx

This file was deleted.

Loading
Loading