Files
cadSet/text-to-cad/viewer/src/client/components/workbench/CadRenderPane.js
T
2026-07-22 19:38:36 +08:00

754 lines
29 KiB
JavaScript

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 (
<div className="cad-glass-surface rounded-md border border-sidebar-border p-0.5 shadow-sm">
<ToggleGroup
type="single"
variant="outline"
size="sm"
value={normalizedValue}
onValueChange={(nextValue) => {
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"
>
<ToggleGroupItem
value="2d"
className="!h-7 px-2 text-[11px] text-muted-foreground hover:text-foreground data-[state=on]:!bg-accent data-[state=on]:!text-foreground data-[state=on]:font-semibold"
title="Show DXF flat pattern"
>
2D
</ToggleGroupItem>
<ToggleGroupItem
value="3d"
disabled={!threeDimensionalAvailable}
className="!h-7 px-2 text-[11px] text-muted-foreground hover:text-foreground data-[state=on]:!bg-accent data-[state=on]:!text-foreground data-[state=on]:font-semibold"
title={threeDimensionalAvailable ? "Show DXF bend preview" : "3D bend preview unavailable"}
>
3D
</ToggleGroupItem>
</ToggleGroup>
</div>
);
}
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 (
<DropdownMenu
open={true}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onClose?.();
}
}}
>
<DropdownMenuTrigger asChild>
<button
type="button"
tabIndex={-1}
aria-hidden="true"
className="pointer-events-none fixed size-px opacity-0"
style={positionStyle}
/>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
sideOffset={4}
className="w-44"
onContextMenu={(event) => {
event.preventDefault();
event.stopPropagation();
}}
>
{menu.global === true ? (
<>
{menu.showShowAll === true ? (
<DropdownMenuItem
className={itemClassName}
onSelect={() => handleAction(onHideAll)}
>
Show all
</DropdownMenuItem>
) : null}
{menu.showShowAll === true && menu.showCameraActions !== false ? (
<DropdownMenuSeparator />
) : null}
{menu.showCameraActions !== false ? (
<>
<DropdownMenuItem
className={itemClassName}
disabled={menu.resetZoomDisabled === true}
onSelect={() => handleAction(onResetZoom)}
>
Reset Zoom
</DropdownMenuItem>
</>
) : null}
{menu.showCameraActions !== false && menu.showExpandCollapse === true ? (
<DropdownMenuSeparator />
) : null}
{menu.showShowAll === true && menu.showCameraActions === false && menu.showExpandCollapse === true ? (
<DropdownMenuSeparator />
) : null}
{menu.showExpandCollapse === true ? (
<>
<DropdownMenuItem
className={itemClassName}
disabled={menu.expandAllDisabled === true}
onSelect={() => handleAction(onExpandAll)}
>
Expand all
</DropdownMenuItem>
<DropdownMenuItem
className={itemClassName}
disabled={menu.collapseAllDisabled === true}
onSelect={() => handleAction(onCollapseAll)}
>
Collapse all
</DropdownMenuItem>
</>
) : null}
</>
) : (
<AssemblyContextMenuItems
Item={DropdownMenuItem}
Separator={DropdownMenuSeparator}
itemClassName={itemClassName}
selected={selected}
isolated={focused}
hidden={hidden}
actionCount={menu.actionCount}
copyReferenceDisabled={!String(menu.copyText || "").trim()}
selectDisabled={menu.selectDisabled === true}
showIsolate={menu.showIsolate !== false}
isolateDisabled={menu.isolateDisabled === true}
showExitAllIsolate={menu.showExitAllIsolate === true}
exitAllIsolateDisabled={menu.exitAllIsolateDisabled === true}
showHideOther={menu.showHideOther !== false}
hideOtherDisabled={menu.hideOtherDisabled === true}
showVisibility={menu.showVisibility !== false}
showHideAll={menu.showHideAll === true}
hideAllDisabled={menu.hideAllDisabled === true}
hideAllLabel={String(menu.hideAllLabel || "").trim() || "Show all"}
visibilityDisabled={menu.visibilityDisabled === true}
showCameraActions={menu.showCameraActions !== false}
resetZoomDisabled={menu.resetZoomDisabled === true}
zoomToFitDisabled={menu.zoomToFitDisabled === true}
showExpandCollapse={menu.showExpandCollapse === true}
expandSelectedDisabled={menu.expandSelectedDisabled !== false}
collapseSelectedDisabled={menu.collapseSelectedDisabled !== false}
expandAllDisabled={menu.expandAllDisabled !== false}
collapseAllDisabled={menu.collapseAllDisabled !== false}
onCopyReference={() => 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)}
/>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
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 ? (
<DxfViewModeControl
value={activeDxfViewMode}
threeDimensionalAvailable={dxf3dAvailable}
onChange={onDxfViewModeChange}
/>
) : 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 (
<div className="absolute inset-0">
{implicitMode ? (
<ImplicitCadViewer
key={`implicit:${activeModelKey}`}
ref={viewerRef}
model={selectedImplicitModel}
modelKey={activeModelKey}
isLoading={viewerLoading}
previewMode={previewMode}
viewportFrameInsets={viewportFrameInsets}
viewPlaneOffsetRight={viewPlaneOffsetRight}
themeSettings={themeSettings}
graphicsSettings={implicitGraphicsSettings}
dynamicRenderActive={implicitDynamicRenderActive}
perspective={viewerPerspective}
perspectiveRef={viewerPerspectiveRef}
onPerspectiveChange={handlePerspectiveChange}
onViewerAlertChange={handleViewerAlertChange}
/>
) : dxfMode && !dxfMeshPreviewReady ? (
<DxfViewer
ref={viewerRef}
dxfData={selectedDxfData}
modelKey={selectedDxfKey}
themeSettings={themeSettings}
viewPlaneOffsetRight={viewPlaneOffsetRight}
viewPlaneOffsetBottom="1rem"
viewPlaneHeader={dxfViewPlaneHeader}
onViewerAlertChange={handleViewerAlertChange}
/>
) : (
<CadViewer
ref={viewerRef}
meshData={activeMeshData}
modelKey={activeModelKey}
renderFormat={renderFormat}
perspective={viewerPerspective}
projection={cadProjection}
perspectiveRef={viewerPerspectiveRef}
onProjectionChange={stepDisplaySettingsActive ? onProjectionChange : undefined}
onDisplayModeChange={stepDisplaySettingsActive ? onDisplayModeChange : undefined}
showEdges={!gcodeMode}
recomputeNormals={false}
themeSettings={themeSettings}
displaySettings={stepDisplaySettingsActive ? displaySettings : null}
previewMode={dxfMode ? false : previewMode}
showViewPlane={dxfMode || gcodeMode ? true : !previewMode}
scale={urdfMode ? VIEWER_SCENE_SCALE.URDF : VIEWER_SCENE_SCALE.CAD}
viewPlaneOffsetRight={viewPlaneOffsetRight}
viewPlaneOffsetBottom="1rem"
viewPlaneHeader={dxfViewPlaneHeader}
compactViewPlane={false}
viewportFrameInsets={viewportFrameInsets}
isLoading={viewerLoading}
pickMode={urdfMode
? VIEWER_PICK_MODE.NONE
: viewerPickModeForRenderPane({
dxfMode,
pathPreviewMode,
topologySelectionPending,
topologySelectionUnavailable,
topologySelectionDeferred,
topologyPickingActive: Boolean(
pickableFaces?.length ||
pickableEdges?.length ||
pickableVertices?.length
),
viewerMode,
assemblyPickingActive,
focusedPartIds
})}
renderPartsIndividually={urdfMode ? true : (renderPartsIndividually || Boolean(resolvedStepParameters?.definition))}
pickableParts={dxfMode || urdfMode || pathPreviewMode ? EMPTY_LIST : assemblyParts}
hiddenPartIds={dxfMode || pathPreviewMode ? [] : hiddenPartIds}
selectedPartIds={dxfMode || pathPreviewMode ? [] : selectedPartIds}
hoveredPartId={dxfMode || pathPreviewMode ? "" : hoveredPartId}
assemblyMates={dxfMode || pathPreviewMode ? [] : assemblyMates}
selectedMateIds={dxfMode || pathPreviewMode ? [] : selectedMateIds}
hoveredMateId={dxfMode || pathPreviewMode ? "" : hoveredMateId}
hoveredReferenceId={dxfMode || pathPreviewMode ? "" : hoveredReferenceId}
selectedReferenceIds={dxfMode || pathPreviewMode ? [] : selectedReferenceIds}
selectorRuntime={dxfMode || pathPreviewMode ? null : selectorRuntime}
displayEdgeRuntime={dxfMode || pathPreviewMode ? null : displayEdgeRuntime}
stepParameters={dxfMode || pathPreviewMode ? null : resolvedStepParameters}
pickableFaces={dxfMode || pathPreviewMode ? [] : pickableFaces}
pickableEdges={dxfMode || pathPreviewMode ? [] : pickableEdges}
pickableVertices={dxfMode || pathPreviewMode ? [] : pickableVertices}
focusedPartId={dxfMode || pathPreviewMode ? "" : focusedPartIds}
boundsAnimationActive={cadViewerBoundsAnimationActive}
drawingEnabled={!dxfMode && !pathPreviewMode && drawToolActive}
drawingTool={drawingTool}
drawingStrokes={dxfMode || pathPreviewMode ? [] : drawingStrokes}
onDrawingStrokesChange={handleDrawingStrokesChange}
onPerspectiveChange={handlePerspectiveChange}
onHoverReferenceChange={handleModelHoverChange}
onActivateReference={handleModelReferenceActivate}
onDoubleActivateReference={handleModelReferenceDoubleActivate}
onContextReference={handleModelReferenceContext}
onViewerAlertChange={handleViewerAlertChange}
onStepModuleTransformDetectedChange={handleStepModuleTransformDetectedChange}
urdfPosePicker={urdfPosePicker}
/>
)}
{!previewMode ? (
<ViewerContextMenu
menu={viewerContextMenu}
positionStyle={viewerContextMenuStyle}
onClose={onViewerContextMenuClose}
onCopyReference={onViewerContextMenuCopyReference}
onSelect={onViewerContextMenuSelect}
onFocus={onViewerContextMenuFocus}
onExitAllIsolate={onViewerContextMenuExitAllIsolate}
onHideOther={onViewerContextMenuHideOther}
onHideAll={onViewerContextMenuHideAll}
onHide={onViewerContextMenuHide}
onReveal={onViewerContextMenuReveal}
onResetZoom={onViewerContextMenuResetZoom}
onZoomToFit={onViewerContextMenuZoomToFit}
onExpandSelected={onViewerContextMenuExpandSelected}
onCollapseSelected={onViewerContextMenuCollapseSelected}
onExpandAll={onViewerContextMenuExpandAll}
onCollapseAll={onViewerContextMenuCollapseAll}
/>
) : null}
{!previewMode && missingFileLabel ? (
<div
className="pointer-events-none absolute z-30 flex min-w-0 items-center justify-center px-4 py-4"
style={modelViewportOverlayStyle}
>
<Alert
variant="destructive"
className="cad-glass-popover pointer-events-auto w-full max-w-xl min-w-0 p-5 text-center shadow-lg"
>
<p className="col-start-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-destructive">
File does not exist
</p>
<AlertTitle className="col-start-1 mt-1 line-clamp-none text-lg text-foreground">File does not exist</AlertTitle>
<AlertDescription className="col-start-1 mt-1 text-sm leading-6 text-muted-foreground">
<code className="rounded-md bg-muted px-2 py-1 text-xs text-foreground">{missingFileLabel}</code>
</AlertDescription>
</Alert>
</div>
) : null}
{!previewMode && blockingViewerAlert ? (
<div
className="pointer-events-none absolute z-30 flex min-w-0 items-center justify-center px-3 py-3 sm:px-4"
style={modelViewportOverlayStyle}
>
<div
role="alert"
aria-label={viewerAlertIconLabel}
title={viewerAlertIconLabel}
className={cn(
"cad-glass-popover pointer-events-auto flex w-full max-w-sm min-w-0 flex-col items-center gap-2 rounded-md border px-4 py-3 text-center shadow-md",
viewportIssueMeta.borderClassName
)}
>
<span className={cn(
"flex size-9 shrink-0 items-center justify-center rounded-full border",
viewportIssueMeta.iconClassName
)}>
<CircleAlert className="size-5" strokeWidth={2} aria-hidden="true" />
</span>
<div className="min-w-0 max-w-full">
<span className={cn(
"text-[10px] font-medium uppercase tracking-[0.08em]",
viewportIssueMeta.labelClassName
)}>
{viewportIssueMeta.label}
</span>
<div className="mt-1 line-clamp-2 min-w-0 max-w-full break-words text-sm font-medium leading-5 text-foreground">
{viewerAlert.title || viewerAlert.summary || "Viewer issue"}
</div>
{viewerAlert.message ? (
<p className="mt-1 line-clamp-3 min-w-0 max-w-full break-words text-xs leading-5 text-muted-foreground">
{viewerAlert.message}
</p>
) : null}
{viewerAlert.resolution ? (
<p className="mt-1 line-clamp-2 min-w-0 max-w-full break-words text-xs leading-5 text-muted-foreground">
{viewerAlert.resolution}
</p>
) : null}
</div>
</div>
</div>
) : null}
{!previewMode && stepUpdateInProgress ? (
<div className="pointer-events-none absolute z-20 flex justify-center px-4" style={modelViewportBottomOverlayStyle}>
<Alert
role="status"
className="cad-glass-popover w-auto px-3 py-1.5 text-[11px] font-medium text-popover-foreground shadow-sm"
>
STEP changed. Updating/regenerating references...
</Alert>
</div>
) : null}
{!previewMode && !stepUpdateInProgress && topologySelectionPending ? (
<div className="pointer-events-none absolute z-20 flex justify-center px-4" style={modelViewportBottomOverlayStyle}>
<Alert
role="status"
className="cad-glass-popover w-auto px-3 py-1.5 text-[11px] font-medium text-popover-foreground shadow-sm"
>
Preparing selectable topology...
</Alert>
</div>
) : null}
{!previewMode && urdfPosePickerActive ? (
<Button
type="button"
variant="outline"
size="icon-xs"
className="cad-glass-popover pointer-events-auto absolute z-30 size-6 rounded-md border-sidebar-border p-0 text-popover-foreground shadow-sm"
style={posePickerExitStyle}
onClick={() => {
urdfPosePicker?.onCancel?.();
}}
aria-label="Exit Select Pose"
title="Exit Select Pose"
>
<X className="size-3.5" strokeWidth={2} aria-hidden="true" />
</Button>
) : null}
{!previewMode && urdfPosePickerActive ? (
<div className="pointer-events-none absolute z-20 flex justify-center px-4" style={modelViewportBottomOverlayStyle}>
<Alert
role="status"
className="cad-glass-popover w-auto px-3 py-1.5 text-[11px] font-medium text-popover-foreground shadow-sm"
>
{urdfPosePickerPrompt}
</Alert>
</div>
) : null}
{!previewMode && ctaMode && !stepUpdateInProgress && !topologySelectionPending && !topologySelectionUnavailable && !topologySelectionDeferred ? (
<div
className="pointer-events-none absolute z-20 flex min-w-0 justify-center"
style={ctaOverlayStyle}
>
<Button
type="button"
variant="default"
size="sm"
className="pointer-events-auto h-9 w-fit min-w-0 max-w-[min(28rem,100%)] shrink overflow-hidden border border-primary/20 bg-primary/85 px-4 text-[12px] font-semibold text-primary-foreground shadow-lg shadow-black/20 hover:bg-primary/75 focus-visible:ring-primary/35 max-sm:w-full"
disabled={ctaDisabled}
onClick={() => {
if (ctaMode === "screenshot") {
void handleScreenshotCopy?.();
return;
}
void handleCopySelection();
}}
title={ctaTitle}
>
<span className="block min-w-0 max-w-full truncate">{ctaLabel}</span>
</Button>
</div>
) : null}
</div>
);
}