diff --git a/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareViewerClient.tsx b/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareViewerClient.tsx index 7cd0a3bf..c3d9470b 100644 --- a/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareViewerClient.tsx +++ b/furniture-3d-system/src/app/installation-demo-share/[token]/InstallationDemoShareViewerClient.tsx @@ -183,7 +183,6 @@ export function InstallationDemoShareViewerClient({ () => Array.from(new Set(numberingModelParts.flatMap((part) => getPartNodeNames(part)))), [numberingModelParts], ) - const numberingVisibleNodeSignature = numberingVisibleNodeNames.join('\u001f') const storedStepMaterialSnapshot = activeStep ? snapshot.stepMaterialsByStepId[activeStep.id] ?? null : null @@ -343,27 +342,6 @@ export function InstallationDemoShareViewerClient({ return () => window.clearTimeout(timer) }, [activeStage.kind, numberingModelParts.length, numberingRevealCount, playing]) - useEffect(() => { - if (!isModelReady || preview.stageView !== 'numbering' || !previewEngine) return - if (numberingVisibleNodeNames.length === 0) return - - const timer = window.setTimeout(() => { - const framed = previewEngine.fitToNodeNames(numberingVisibleNodeNames, 1.08) - if (!framed) { - previewEngine.fitToView(1.08) - } - }, 180) - - return () => window.clearTimeout(timer) - }, [ - isModelReady, - numberingVisibleNodeNames, - numberingVisibleNodeNames.length, - numberingVisibleNodeSignature, - preview.stageView, - previewEngine, - ]) - useEffect(() => { if (!isModelReady || !playing || !activeStage) return const timer = window.setTimeout(() => { @@ -517,6 +495,7 @@ export function InstallationDemoShareViewerClient({ stepMaterialMarkerParts={previewStepMaterialMarkerParts} viewportInstanceKey={shareViewportInstanceKey} showStepModeToggle={false} + showNumberingToolbar={false} showStatusBar={false} canEdit={false} gizmoMode="translate" diff --git a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationNumberingPresentation.tsx b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationNumberingPresentation.tsx new file mode 100644 index 00000000..78f3cca3 --- /dev/null +++ b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationNumberingPresentation.tsx @@ -0,0 +1,29 @@ +import type { InstallationPartItem } from '../types' +import { InstallationPartsInventoryBoard } from './InstallationPartsInventoryBoard' + +interface InstallationNumberingPresentationProps { + parts: InstallationPartItem[] + thumbnailUrls: Record + snapshotUrls: Record + activePartId: string | null +} + +export function InstallationNumberingPresentation({ + parts, + thumbnailUrls, + snapshotUrls, + activePartId, +}: InstallationNumberingPresentationProps) { + return ( +
+ +
+ ) +} diff --git a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPartsInventoryBoard.tsx b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPartsInventoryBoard.tsx index 56a03d0d..83cbced2 100644 --- a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPartsInventoryBoard.tsx +++ b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPartsInventoryBoard.tsx @@ -4,11 +4,16 @@ import { getPartNodeNames } from '../lib/installationPartCatalogHelpers' interface InstallationPartsInventoryBoardProps { parts: InstallationPartItem[] thumbnailUrls?: Record + snapshotUrls?: Record visiblePartIds?: string[] + activePartId?: string | null title?: string compact?: boolean + presentation?: boolean + animateCells?: boolean showTitle?: boolean snapshotUrl?: string | null + onSelectPart?: (partId: string) => void } export function resolveInstallationPartDisplayQuantity(part: InstallationPartItem) { @@ -127,40 +132,53 @@ export function InstallationNumberingSnapshotFigure({ export function InstallationPartsInventoryBoard({ parts, thumbnailUrls = {}, + snapshotUrls = {}, visiblePartIds, + activePartId = null, title = '配件', compact = false, + presentation = false, + animateCells = false, showTitle = true, snapshotUrl = null, + onSelectPart, }: InstallationPartsInventoryBoardProps) { - const visiblePartIdSet = visiblePartIds ? new Set(visiblePartIds) : null - const visibleParts = visiblePartIdSet - ? parts.filter((part) => visiblePartIdSet.has(part.id)) - : parts + const highlightedPartIdSet = visiblePartIds ? new Set(visiblePartIds) : null + const visibleParts = parts if (visibleParts.length === 0) return null - const panelClassName = compact - ? 'pointer-events-auto w-full max-w-[680px] rounded-[8px] bg-white/96 p-1.5 shadow-[0_10px_30px_rgba(45,51,53,0.12)] ring-1 ring-black/[0.1]' - : 'pointer-events-auto w-[min(920px,calc(100vw-560px))] max-w-[calc(100vw-32px)] rounded-[10px] bg-white/96 p-3 shadow-[0_20px_70px_rgba(45,51,53,0.18)] ring-1 ring-black/[0.1] backdrop-blur-xl' - const headerClassName = compact - ? 'flex h-6 items-center bg-[#1f2526] px-3 text-[11px] font-black tracking-[0.06em] text-white' - : 'flex h-10 items-center bg-[#1f2526] px-4 text-[16px] font-black tracking-[0.08em] text-white' - const gridClassName = compact - ? 'grid max-h-[116px] grid-cols-4 overflow-y-auto border-l border-t border-[#2d3335]/20 bg-white xl:grid-cols-5' - : 'grid max-h-[min(66vh,620px)] grid-cols-2 overflow-y-auto border-l border-t border-[#2d3335]/20 bg-white sm:grid-cols-3 xl:grid-cols-4' - const cellClassName = compact - ? 'relative min-h-[86px] border-b border-r border-[#2d3335]/20 bg-[#fbfbfa]' - : 'relative min-h-[150px] border-b border-r border-[#2d3335]/20 bg-[#fbfbfa]' - const imageWrapClassName = compact - ? 'flex h-[54px] items-center justify-center px-6 pt-4' - : 'flex h-[112px] items-center justify-center px-8 pt-5' + const panelClassName = presentation + ? 'pointer-events-auto w-full overflow-hidden rounded-[8px] bg-white p-2 shadow-[0_20px_70px_rgba(45,51,53,0.16)] ring-1 ring-black/[0.12]' + : compact + ? 'pointer-events-auto w-full max-w-[680px] rounded-[8px] bg-white/96 p-1.5 shadow-[0_10px_30px_rgba(45,51,53,0.12)] ring-1 ring-black/[0.1]' + : 'pointer-events-auto w-full overflow-hidden rounded-[10px] bg-white/96 p-2.5 shadow-[0_20px_70px_rgba(45,51,53,0.18)] ring-1 ring-black/[0.1] backdrop-blur-xl' + const headerClassName = presentation + ? 'flex h-10 items-center overflow-hidden bg-[#1f2526] px-4 text-[15px] font-black tracking-[0.08em] text-white' + : compact + ? 'flex h-6 items-center bg-[#1f2526] px-3 text-[11px] font-black tracking-[0.06em] text-white' + : 'flex h-9 items-center overflow-hidden bg-[#1f2526] px-4 text-[15px] font-black tracking-[0.08em] text-white' + const gridClassName = presentation + ? 'grid max-h-[min(72vh,760px)] grid-cols-2 overflow-y-auto border-l border-t border-[#2d3335]/20 bg-white sm:grid-cols-3 xl:grid-cols-4 max-md:flex max-md:max-h-[184px] max-md:snap-x max-md:overflow-x-auto max-md:overflow-y-hidden' + : compact + ? 'grid max-h-[116px] grid-cols-4 overflow-y-auto border-l border-t border-[#2d3335]/20 bg-white xl:grid-cols-5' + : 'grid max-h-[min(58vh,620px)] grid-cols-2 overflow-y-auto border-l border-t border-[#2d3335]/20 bg-white sm:grid-cols-3 2xl:grid-cols-4 max-md:flex max-md:max-h-[180px] max-md:snap-x max-md:overflow-x-auto max-md:overflow-y-hidden' + const baseCellClassName = presentation + ? 'relative min-h-[190px] border-b border-r border-[#2d3335]/20 bg-[#fbfbfa] text-left xl:min-h-[220px] max-md:min-h-[166px] max-md:w-[152px] max-md:shrink-0 max-md:snap-start' + : compact + ? 'relative min-h-[86px] border-b border-r border-[#2d3335]/20 bg-[#fbfbfa]' + : 'relative min-h-[148px] border-b border-r border-[#2d3335]/20 bg-[#fbfbfa] text-left transition max-md:min-h-[154px] max-md:w-[142px] max-md:shrink-0 max-md:snap-start' + const imageWrapClassName = presentation + ? 'flex h-[148px] items-center justify-center px-8 pt-6 xl:h-[178px] max-md:h-[124px] max-md:px-5 max-md:pt-5' + : compact + ? 'flex h-[54px] items-center justify-center px-6 pt-4' + : 'flex h-[108px] items-center justify-center px-7 pt-5 max-md:h-[112px] max-md:px-5' const fallbackClassName = compact ? 'flex h-12 w-12 items-center justify-center rounded-[6px] bg-white text-[16px] font-black text-[#2d3335] ring-1 ring-black/[0.08]' : 'flex h-20 w-20 items-center justify-center rounded-[8px] bg-white text-[22px] font-black text-[#2d3335] ring-1 ring-black/[0.08]' const footerClassName = compact ? 'flex min-h-7 items-center justify-between gap-1 border-t border-[#2d3335]/10 px-2 py-1' - : 'flex min-h-9 items-center justify-between gap-2 border-t border-[#2d3335]/10 px-3 py-2' + : 'flex min-h-10 items-center justify-between gap-2 border-t border-[#2d3335]/10 px-3 py-2' const nameClassName = compact ? 'min-w-0 truncate text-[9px] font-bold text-[#5a6062]' : 'min-w-0 truncate text-[11px] font-bold text-[#5a6062]' @@ -171,7 +189,23 @@ export function InstallationPartsInventoryBoard({ return (
- {showTitle ?
{title}
: null} + {animateCells ? ( + + ) : null} + {showTitle ? ( +
+ {title} + +
+ ) : null} {snapshotUrl ? (
{snapshotPrimaryPart ? ( @@ -181,11 +215,36 @@ export function InstallationPartsInventoryBoard({ ) : null} {snapshotUrl ? null : (
- {visibleParts.map((part) => { + {visibleParts.map((part, index) => { const quantity = resolveInstallationPartDisplayQuantity(part) - const thumbnailUrl = thumbnailUrls[part.id] ?? null + const thumbnailUrl = snapshotUrls[part.id] ?? thumbnailUrls[part.id] ?? null + const isActive = + activePartId === part.id || Boolean(highlightedPartIdSet?.has(part.id)) + const cellClassName = [ + baseCellClassName, + onSelectPart ? 'cursor-pointer hover:bg-white' : '', + isActive ? 'z-10 ring-2 ring-[#e23d2c] ring-inset' : '', + animateCells ? 'installation-numbering-cell-enter' : '', + ] + .filter(Boolean) + .join(' ') return ( -
+
+ ) })}
diff --git a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPreviewStage.tsx b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPreviewStage.tsx index fedb986d..6e13abcd 100644 --- a/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPreviewStage.tsx +++ b/furniture-3d-system/src/app/installation-demo-workbench/components/InstallationPreviewStage.tsx @@ -1,7 +1,7 @@ 'use client' -import { useRef, useState, type ComponentProps } from 'react' -import { AlertCircle, Box, ChevronDown, ChevronUp, ListChecks, MousePointer2 } from 'lucide-react' +import { useRef, type ComponentProps } from 'react' +import { AlertCircle, Box, MousePointer2 } from 'lucide-react' import { AssetWorkbenchEmptyBackdrop } from '@/components/platform/AssetWorkbenchEmptyBackdrop' import { ModelPreviewPanel } from '@/components/platform/ModelPreviewPanel' import type { NodePositionOffset, NodeRotationState, ThreeDEngine } from '@/lib/3d/engine' @@ -19,10 +19,8 @@ import { useNumberingOverlaySafeInsets, } from '../hooks/useNumberingOverlaySafeInsets' import { InstallationNumberingOverlay } from './InstallationNumberingOverlay' -import { - InstallationNumberingPartInfoCard, - InstallationPartsInventoryBoard, -} from './InstallationPartsInventoryBoard' +import { InstallationNumberingPresentation } from './InstallationNumberingPresentation' +import { InstallationPartsInventoryBoard } from './InstallationPartsInventoryBoard' import { InstallationStepMaterialCard } from './InstallationStepMaterialCard' import { NumberingPreviewToolbar } from './NumberingPreviewToolbar' import { SelectionModeToolbar } from './SelectionModeToolbar' @@ -76,6 +74,7 @@ interface InstallationPreviewStageProps { stepMaterialMarkerParts: InstallationPartItem[] viewportInstanceKey?: string showStepModeToggle?: boolean + showNumberingToolbar?: boolean showStatusBar?: boolean canEdit: boolean gizmoMode: 'translate' | 'rotate' @@ -134,6 +133,7 @@ export function InstallationPreviewStage({ stepMaterialMarkerParts, viewportInstanceKey, showStepModeToggle = true, + showNumberingToolbar = true, showStatusBar = true, canEdit, gizmoMode, @@ -157,7 +157,6 @@ export function InstallationPreviewStage({ onNextNumbering, onResetNumbering, }: InstallationPreviewStageProps) { - const [numberingInventoryExpanded, setNumberingInventoryExpanded] = useState(false) const stageRef = useRef(null) const numberingToolbarRef = useRef(null) const numberingInventoryPanelRef = useRef(null) @@ -169,16 +168,11 @@ export function InstallationPreviewStage({ const showStepMaterials = (view === 'steps' || view === 'preview') && stepMaterialItems.length > 0 const showExplodedNumberingOverlay = view === 'exploded' && numberingParts.length > 0 - const showNumberingInventoryBoard = view === 'numbering' && numberingParts.length > 0 - const numberingInventoryCount = - numberingVisiblePartIds.length > 0 ? numberingVisiblePartIds.length : numberingParts.length - const numberingInventoryPanelWidthClassName = numberingInventoryExpanded - ? 'w-[min(520px,72%)] max-md:w-[92%]' - : 'w-[184px]' + const showNumberingPresentation = + view === 'numbering' && numberingParts.length > 0 && (!canEdit || mode === 'preview') + const showNumberingInventoryBoard = + view === 'numbering' && numberingParts.length > 0 && !showNumberingPresentation const activeNumberingPartId = numberingVisiblePartIds[0] ?? selectedNumberingPartId - const activeNumberingSnapshotUrl = activeNumberingPartId - ? numberingSnapshotUrls[activeNumberingPartId] ?? null - : null const activeNumberingPart = activeNumberingPartId ? numberingParts.find((part) => part.id === activeNumberingPartId) ?? null : null @@ -218,21 +212,21 @@ export function InstallationPreviewStage({ view === 'preview' ? mode : 'edit', ].join(':') const measuredNumberingSafeInsets = useNumberingOverlaySafeInsets({ - enabled: view === 'numbering', + enabled: showNumberingInventoryBoard, stageRef, topOverlayRef: numberingToolbarRef, rightOverlayRef: numberingInventoryPanelRef, bottomOverlayRef: statusBarRef, leftOverlayRef: numberingInfoCardRef, refreshKey: [ - numberingInventoryExpanded, showStatusBar, showNumberingInventoryBoard, + numberingParts.length, activeNumberingPartId ?? 'none', ].join(':'), }) const numberingOverlaySafeInsets = - view === 'numbering' ? measuredNumberingSafeInsets : DEFAULT_NUMBERING_SAFE_INSETS + showNumberingInventoryBoard ? measuredNumberingSafeInsets : DEFAULT_NUMBERING_SAFE_INSETS return (
@@ -244,8 +238,8 @@ export function InstallationPreviewStage({ repairDisplayNormals showFpsBadge={false} className="h-full" - viewportControlsEnabled={view !== 'preview'} - showCameraActionButtons + viewportControlsEnabled={view !== 'preview' && !showNumberingPresentation} + showCameraActionButtons={!showNumberingPresentation} enableTranslateGizmo={explosionEditingEnabled} gizmoMode={gizmoMode} onGizmoModeChange={onGizmoModeChange} @@ -276,13 +270,16 @@ export function InstallationPreviewStage({ disabled={!canEdit || !modelUrl} /> ) : null} - {view === 'numbering' ? ( + {view === 'numbering' && showNumberingToolbar ? (
0} currentIndex={numberingCurrentIndex} totalCount={numberingTotalCount} + showModeToggle={canEdit} + onModeChange={onModeChange} onTogglePlaying={onToggleNumberingPlaying} onPrevious={onPreviousNumbering} onNext={onNextNumbering} @@ -306,7 +303,14 @@ export function InstallationPreviewStage({
} overlayCenter={ - showNumberingInventoryBoard ? ( + showNumberingPresentation ? ( + + ) : showNumberingInventoryBoard ? ( <>
-
- - {numberingInventoryExpanded ? ( -
- -
- ) : null} -
+
) : showExplodedNumberingOverlay ? ( @@ -374,15 +354,7 @@ export function InstallationPreviewStage({ ) : null } overlayLeft={ - view === 'numbering' && activeNumberingPart ? ( -
- -
- ) : showStepMaterials ? ( + showStepMaterials ? ( void onTogglePlaying: () => void onPrevious: () => void onNext: () => void @@ -14,10 +18,13 @@ interface NumberingPreviewToolbarProps { } export function NumberingPreviewToolbar({ + mode, playing, canPreview, currentIndex, totalCount, + showModeToggle = true, + onModeChange, onTogglePlaying, onPrevious, onNext, @@ -27,6 +34,22 @@ export function NumberingPreviewToolbar({ return (
+ {showModeToggle + ? (['edit', 'preview'] as const).map((item) => ( + + )) + : null} + {showModeToggle ? : null}
{counter}