import { useEffect, useMemo } from "react";
import CadViewer from "../CadViewer";
import DxfViewer from "../DxfViewer";
import ImplicitCadViewer from "../ImplicitCadViewer";
import { CircleAlert, X } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "../ui/dropdown-menu";
import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
import AssemblyContextMenuItems from "./AssemblyContextMenuItems";
import { cn } from "@/ui/utils";
import { RENDER_FORMAT } from "@/workbench/constants";
import {
isMeshRenderFormat,
isRobotRenderFormat
} from "cadjs/lib/fileFormats";
import {
CAMERA_PROJECTION,
normalizeCameraProjection
} from "cadjs/lib/displaySettings";
import { VIEWER_SCENE_SCALE } from "cadjs/lib/viewer/sceneScale";
import { VIEWER_PICK_MODE } from "cadjs/lib/viewer/constants";
import { useStepAnimationSnapshot } from "@/workbench/stepAnimationStore";
import { viewerPickModeForRenderPane } from "@/workbench/viewerPickMode";
const EMPTY_LIST = Object.freeze([]);
const VIEWPORT_ISSUE_META = Object.freeze({
error: {
label: "Error",
borderClassName: "border-destructive/45",
iconClassName: "border-destructive/45 bg-destructive/10 text-destructive dark:text-red-300",
labelClassName: "text-destructive dark:text-red-300"
},
warning: {
label: "Warning",
borderClassName: "border-amber-500/45",
iconClassName: "border-amber-500/55 bg-amber-500/10 text-amber-500 dark:text-amber-300",
labelClassName: "text-amber-500 dark:text-amber-300"
}
});
function viewportInsetPx(value) {
const numericValue = Number(value);
return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : 0;
}
function viewportIssueMetaForAlert(alert) {
return alert?.severity === "warning"
? VIEWPORT_ISSUE_META.warning
: VIEWPORT_ISSUE_META.error;
}
function viewerContextMenuAnchorStyle(menu, viewportFrameInsets) {
if (!menu) {
return null;
}
const margin = 8;
const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 0;
const viewportHeight = typeof window !== "undefined" ? window.innerHeight : 0;
const minX = viewportInsetPx(viewportFrameInsets?.left) + margin;
const minY = viewportInsetPx(viewportFrameInsets?.top) + margin;
const maxX = viewportWidth > 0
? Math.max(minX, viewportWidth - viewportInsetPx(viewportFrameInsets?.right) - margin)
: Number(menu.x) || minX;
const maxY = viewportHeight > 0
? Math.max(minY, viewportHeight - viewportInsetPx(viewportFrameInsets?.bottom) - margin)
: Number(menu.y) || minY;
const x = Math.min(Math.max(Number(menu.x) || minX, minX), maxX);
const y = Math.min(Math.max(Number(menu.y) || minY, minY), maxY);
return {
position: "fixed",
left: `${x}px`,
top: `${y}px`,
width: "1px",
height: "1px"
};
}
function DxfViewModeControl({
value,
threeDimensionalAvailable = false,
onChange
}) {
const normalizedValue = value === "3d" && threeDimensionalAvailable ? "3d" : "2d";
return (
{
if (!nextValue) {
return;
}
if (nextValue === "3d" && !threeDimensionalAvailable) {
return;
}
onChange?.(nextValue);
}}
className="grid h-7 w-[5.5rem] grid-cols-2"
aria-label="DXF view mode"
>
2D
3D
);
}
function ViewerContextMenu({
menu,
positionStyle,
onClose,
onCopyReference,
onSelect,
onFocus,
onExitAllIsolate,
onHideOther,
onHideAll,
onHide,
onReveal,
onResetZoom,
onZoomToFit,
onExpandSelected,
onCollapseSelected,
onExpandAll,
onCollapseAll
}) {
if (!menu || !positionStyle) {
return null;
}
const itemClassName = "text-xs";
const handleAction = (action) => {
action?.(menu);
onClose?.();
};
const selected = menu.selected === true;
const hidden = menu.hidden === true;
const focused = menu.focused === true;
return (
{
if (!nextOpen) {
onClose?.();
}
}}
>
{
event.preventDefault();
event.stopPropagation();
}}
>
{menu.global === true ? (
<>
{menu.showShowAll === true ? (
handleAction(onHideAll)}
>
Show all
) : null}
{menu.showShowAll === true && menu.showCameraActions !== false ? (
) : null}
{menu.showCameraActions !== false ? (
<>
handleAction(onResetZoom)}
>
Reset Zoom
>
) : null}
{menu.showCameraActions !== false && menu.showExpandCollapse === true ? (
) : null}
{menu.showShowAll === true && menu.showCameraActions === false && menu.showExpandCollapse === true ? (
) : null}
{menu.showExpandCollapse === true ? (
<>
handleAction(onExpandAll)}
>
Expand all
handleAction(onCollapseAll)}
>
Collapse all
>
) : null}
>
) : (
handleAction(onCopyReference)}
onSelect={() => handleAction(onSelect)}
onIsolate={() => handleAction(onFocus)}
onExitAllIsolate={() => handleAction(onExitAllIsolate)}
onHideOther={() => handleAction(onHideOther)}
onHideAll={() => handleAction(onHideAll)}
onToggleVisibility={() => handleAction(hidden ? onReveal : onHide)}
onResetZoom={() => handleAction(onResetZoom)}
onZoomToFit={() => handleAction(onZoomToFit)}
onExpandSelected={() => handleAction(onExpandSelected)}
onCollapseSelected={() => handleAction(onCollapseSelected)}
onExpandAll={() => handleAction(onExpandAll)}
onCollapseAll={() => handleAction(onCollapseAll)}
/>
)}
);
}
export default function CadRenderPane({
viewerRef,
renderFormat,
renderPartsIndividually = false,
selectedMeshData,
selectedDxfData,
selectedDxfMeshData,
dxfViewMode = "2d",
onDxfViewModeChange,
selectedImplicitModel,
implicitDynamicRenderActive = false,
implicitGraphicsSettings = null,
selectedKey,
selectedDxfKey,
missingFileRef = "",
viewerPerspective,
viewerPerspectiveRef,
themeSettings,
previewMode,
viewportFrameInsets,
viewerLoading,
viewerAlert,
stepUpdateInProgress,
referenceSelectionPending = false,
referenceSelectionUnavailable = false,
referenceSelectionDeferred = false,
viewPlaneOffsetRight = 16,
viewerMode,
assemblyPickingActive = false,
assemblyParts,
hiddenPartIds,
selectedPartIds,
hoveredPartId,
assemblyMates = EMPTY_LIST,
selectedMateIds = EMPTY_LIST,
hoveredMateId = "",
hoveredReferenceId,
selectedReferenceIds,
selectorRuntime,
displayEdgeRuntime,
stepParameters = null,
pickableFaces,
pickableEdges,
pickableVertices,
focusedPartIds = "",
displaySettings = null,
onProjectionChange,
onDisplayModeChange,
boundsAnimationActive = false,
drawToolActive,
drawingTool,
drawingStrokes,
handleDrawingStrokesChange,
handlePerspectiveChange,
handleModelHoverChange,
handleModelReferenceActivate,
handleModelReferenceDoubleActivate,
handleModelReferenceContext,
viewerContextMenu = null,
onViewerContextMenuClose,
onViewerContextMenuCopyReference,
onViewerContextMenuSelect,
onViewerContextMenuFocus,
onViewerContextMenuExitAllIsolate,
onViewerContextMenuHideOther,
onViewerContextMenuHideAll,
onViewerContextMenuHide,
onViewerContextMenuReveal,
onViewerContextMenuResetZoom,
onViewerContextMenuZoomToFit,
onViewerContextMenuExpandSelected,
onViewerContextMenuCollapseSelected,
onViewerContextMenuExpandAll,
onViewerContextMenuCollapseAll,
handleViewerAlertChange,
handleStepModuleTransformDetectedChange,
selectionCount,
copyButtonLabel,
handleCopySelection,
handleScreenshotCopy,
urdfPosePicker = null
}) {
const liveStepAnimation = useStepAnimationSnapshot();
const resolvedStepParameters = useMemo(() => {
if (!stepParameters?.animationState?.playing) {
return stepParameters;
}
const liveParameterValues = liveStepAnimation?.parameterValues;
if (!liveParameterValues || typeof liveParameterValues !== "object") {
return stepParameters;
}
return {
...stepParameters,
parameterValues: liveParameterValues,
animationState: {
...stepParameters.animationState,
elapsedSec: liveStepAnimation.elapsedSec
}
};
}, [stepParameters, liveStepAnimation]);
const viewerAlertIconLabel = "Viewer error. See the Issues section for details.";
const dxfMode = renderFormat === RENDER_FORMAT.DXF;
const gcodeMode = renderFormat === RENDER_FORMAT.GCODE;
const urdfMode = isRobotRenderFormat(renderFormat);
const implicitMode = renderFormat === RENDER_FORMAT.IMPLICIT;
const meshOnlyMode = isMeshRenderFormat(renderFormat);
const pathPreviewMode = meshOnlyMode || gcodeMode;
const dxf3dAvailable = !!selectedDxfMeshData;
const activeDxfViewMode = dxfViewMode === "3d" && dxf3dAvailable ? "3d" : "2d";
const dxfMeshPreviewReady = dxfMode && activeDxfViewMode === "3d" && dxf3dAvailable;
const activeMeshData = dxfMeshPreviewReady ? selectedDxfMeshData : selectedMeshData;
const stepDisplaySettingsActive = renderFormat === RENDER_FORMAT.STEP && !!displaySettings && !dxfMode && !pathPreviewMode;
const cadProjection = stepDisplaySettingsActive
? normalizeCameraProjection(displaySettings.projection)
: CAMERA_PROJECTION.PERSPECTIVE;
const activeModelKey = dxfMeshPreviewReady ? (selectedDxfKey || selectedKey) : selectedKey;
const stepBoundsAnimationActive = Boolean(resolvedStepParameters?.animationState?.playing);
const cadViewerBoundsAnimationActive = Boolean(boundsAnimationActive || stepBoundsAnimationActive);
const missingFileLabel = String(missingFileRef || "").trim();
const topologySelectionPending = Boolean(referenceSelectionPending && !dxfMode && !urdfMode && !pathPreviewMode);
const topologySelectionUnavailable = Boolean(referenceSelectionUnavailable && !dxfMode && !urdfMode && !pathPreviewMode);
const topologySelectionDeferred = Boolean(referenceSelectionDeferred && activeMeshData && !dxfMode && !urdfMode && !pathPreviewMode);
const urdfPosePickerActive = Boolean(urdfPosePicker?.active);
const urdfPosePickerPrompt = "Select target";
const posePickerExitStyle = {
left: `calc(${Math.max(Number(viewportFrameInsets?.left) || 0, 0)}px + 0.75rem)`,
top: `calc(${Math.max(Number(viewportFrameInsets?.top) || 0, 0)}px + 0.75rem)`
};
const ctaMode = !dxfMode && !pathPreviewMode && drawToolActive
? "screenshot"
: !dxfMode && !pathPreviewMode && selectionCount > 0
? "selection"
: "";
const dxfViewPlaneHeader = dxfMode ? (
) : null;
const bottomOverlayStyle = {
bottom: "1rem"
};
const modelViewportOverlayStyle = {
left: `${viewportInsetPx(viewportFrameInsets?.left)}px`,
right: `${viewportInsetPx(viewportFrameInsets?.right)}px`,
top: `${viewportInsetPx(viewportFrameInsets?.top)}px`,
bottom: `${viewportInsetPx(viewportFrameInsets?.bottom)}px`
};
const modelViewportBottomOverlayStyle = {
left: `${viewportInsetPx(viewportFrameInsets?.left)}px`,
right: `${viewportInsetPx(viewportFrameInsets?.right)}px`,
bottom: `calc(${viewportInsetPx(viewportFrameInsets?.bottom)}px + 1rem)`
};
const ctaOverlayStyle = {
...bottomOverlayStyle,
left: `calc(${viewportInsetPx(viewportFrameInsets?.left)}px + 1rem)`,
right: `calc(${viewportInsetPx(viewportFrameInsets?.right)}px + 1rem)`
};
const ctaLabel = ctaMode === "screenshot" ? "Copy Screenshot" : copyButtonLabel;
const ctaTitle = ctaMode === "screenshot" ? "Copy screenshot to clipboard" : copyButtonLabel;
const ctaDisabled = ctaMode === "screenshot" ? viewerLoading || !activeMeshData : false;
const viewportHasRenderableContent = implicitMode
? !!selectedImplicitModel
: dxfMode && !dxfMeshPreviewReady
? !!selectedDxfData
: !!activeMeshData;
const blockingViewerAlert = viewerAlert && viewerAlert.blocking !== false && (
viewerAlert.blocking ||
viewerAlert.severity !== "warning" ||
!viewportHasRenderableContent
)
? viewerAlert
: null;
const viewportIssueMeta = viewportIssueMetaForAlert(blockingViewerAlert);
const viewerContextMenuStyle = useMemo(
() => viewerContextMenuAnchorStyle(viewerContextMenu, viewportFrameInsets),
[viewerContextMenu, viewportFrameInsets]
);
useEffect(() => {
if (!urdfPosePickerActive || typeof window === "undefined" || typeof document === "undefined") {
return undefined;
}
const handleEscape = (event) => {
if (event.defaultPrevented) {
return;
}
if (event.key !== "Escape" && event.key !== "Esc" && event.code !== "Escape") {
return;
}
event.preventDefault();
event.stopPropagation();
urdfPosePicker?.onCancel?.();
};
window.addEventListener("keydown", handleEscape, true);
document.addEventListener("keydown", handleEscape, true);
return () => {
window.removeEventListener("keydown", handleEscape, true);
document.removeEventListener("keydown", handleEscape, true);
};
}, [urdfPosePicker, urdfPosePickerActive]);
return (
{implicitMode ? (
) : dxfMode && !dxfMeshPreviewReady ? (
) : (
)}
{!previewMode ? (
) : null}
{!previewMode && missingFileLabel ? (
File does not exist
File does not exist
{missingFileLabel}
) : null}
{!previewMode && blockingViewerAlert ? (
{viewportIssueMeta.label}
{viewerAlert.title || viewerAlert.summary || "Viewer issue"}
{viewerAlert.message ? (
{viewerAlert.message}
) : null}
{viewerAlert.resolution ? (
{viewerAlert.resolution}
) : null}
) : null}
{!previewMode && stepUpdateInProgress ? (
STEP changed. Updating/regenerating references...
) : null}
{!previewMode && !stepUpdateInProgress && topologySelectionPending ? (
Preparing selectable topology...
) : null}
{!previewMode && urdfPosePickerActive ? (
) : null}
{!previewMode && urdfPosePickerActive ? (
) : null}
{!previewMode && ctaMode && !stepUpdateInProgress && !topologySelectionPending && !topologySelectionUnavailable && !topologySelectionDeferred ? (
) : null}
);
}