"use client"; import * as React from "react"; import { DIRECT_CANVAS_OPERATION_TARGET, DIRECT_CANVAS_SOURCE_ASSET_CONTROL, } from "../../source-assets/source-asset-coordinator"; import { getToolcraftInfiniteCanvasBackgroundColor } from "../../state/canvas-background-state"; import type { ToolcraftState } from "../../state/types"; import { CanvasDefaultMediaLayer, getVisibleCanvasImageAssets } from "./canvas-default-media-layer"; import { CanvasViewportWorld } from "./canvas-viewport-world"; import { CanvasSceneSlot, CanvasSceneSurface } from "./canvas-scene-surface"; import { useCanvasDropImport, type ToolcraftCanvasUploadPresentationState, } from "./use-canvas-drop-import"; import { useCanvasViewportInteractions } from "./use-canvas-viewport-interactions"; import { useToolcraftCanvasFrame } from "./use-toolcraft-canvas-frame"; import { ToolcraftProductSceneSurface } from "./product-scene-surface"; import { useToolcraftStore } from "../app-shell/toolcraft-store-context"; import { useToolcraftSourceAssetCoordinator } from "../app-shell/toolcraft-source-asset-context"; import { useToolcraftCommittedSelector } from "../app-shell/toolcraft-selectors"; import { useToolcraftDispatch } from "../app-shell/use-toolcraft"; import { ToolcraftCanvasHandleLayers } from "../canvas-handles/canvas-handle-layer-registry"; import { getVisibleCanvasModelAssets, ToolcraftModelCanvasLayers, } from "../model-rendering/model-canvas-layer"; import { useToolcraftModelPresentationMode } from "../model-rendering/model-presentation-mode"; const directCanvasModelOperationTargets = [DIRECT_CANVAS_OPERATION_TARGET] as const; export type CanvasShellProps = { children?: React.ReactNode; infiniteCanvasContent?: React.ReactNode; renderDefaultMedia?: boolean; }; function isDragLeavingCurrentTarget(event: React.DragEvent): boolean { const nextTarget = event.relatedTarget; return !(nextTarget instanceof Node && event.currentTarget.contains(nextTarget)); } function mediaAssetListsEqual( previous: readonly MediaAsset[], next: readonly MediaAsset[], ): boolean { return ( previous.length === next.length && previous.every((mediaAsset, index) => mediaAsset === next[index]) ); } const selectCanvasSchema = (state: ToolcraftState) => state.schema.canvas; const selectInfiniteCanvasBackgroundColor = (state: ToolcraftState) => getToolcraftInfiniteCanvasBackgroundColor(state); const selectSelectedLayerId = (state: ToolcraftState) => state.selectedLayerId; export function CanvasShell({ children, infiniteCanvasContent, renderDefaultMedia = true, }: CanvasShellProps): React.JSX.Element { const dispatch = useToolcraftDispatch(); const store = useToolcraftStore(); const sourceAssetCoordinator = useToolcraftSourceAssetCoordinator(); const modelPresentation = useToolcraftModelPresentationMode(); const suppressedModelTargets = React.useMemo( () => modelPresentation.mode === "custom" ? modelPresentation.consumers.map(({ sourceTarget }) => sourceTarget) : [], [modelPresentation], ); const [dragOver, setDragOver] = React.useState(false); const [uploadPresentation, setUploadPresentation] = React.useState({ directOperation: false, feedback: null, }); const canvasSchema = useToolcraftCommittedSelector(selectCanvasSchema); const infiniteCanvasBackgroundColor = useToolcraftCommittedSelector( selectInfiniteCanvasBackgroundColor, ); const selectedLayerId = useToolcraftCommittedSelector(selectSelectedLayerId); const canvasFrame = useToolcraftCanvasFrame(); const visibleMediaAssets = useToolcraftCommittedSelector( getVisibleCanvasImageAssets, mediaAssetListsEqual, ); const visibleModelAssets = useToolcraftCommittedSelector( getVisibleCanvasModelAssets, mediaAssetListsEqual, ); const getDirectCanvasOperation = React.useCallback( () => sourceAssetCoordinator.getOperation(DIRECT_CANVAS_OPERATION_TARGET), [sourceAssetCoordinator], ); const directCanvasOperation = React.useSyncExternalStore( sourceAssetCoordinator.subscribe, getDirectCanvasOperation, getDirectCanvasOperation, ); const directCanvasPresentation = sourceAssetCoordinator.getPresentation( DIRECT_CANVAS_SOURCE_ASSET_CONTROL, visibleMediaAssets, directCanvasOperation, ); const uploadStatus = uploadPresentation.directOperation ? directCanvasPresentation?.status : undefined; const uploadFeedback = uploadPresentation.feedback ?? (uploadPresentation.directOperation ? (directCanvasPresentation?.feedback ?? null) : null); const uploadEnabled = canvasSchema.upload; const { handlePointerDown, handlePointerDownCapture, handlePointerMove, handlePointerMoveCapture, handlePointerUp, handlePointerUpCapture, viewportRef, } = useCanvasViewportInteractions({ draggable: canvasSchema.draggable, store, }); const handleDrop = useCanvasDropImport({ coordinator: sourceAssetCoordinator, onPresentationChange: setUploadPresentation, setDragOver, store, uploadEnabled, }); const hasCanvasContent = visibleMediaAssets.length > 0 || visibleModelAssets.length > 0; const hasCanvasSlot = React.Children.count(children) > 0; const renderEditableCanvas = canvasSchema.sizing.mode !== "intrinsic-media" || canvasSchema.sizeSource === "app" || hasCanvasContent || hasCanvasSlot; const beginDragOver = (event: React.DragEvent): void => { if (!uploadEnabled) { return; } event.preventDefault(); setDragOver(true); }; return (
{ if (isDragLeavingCurrentTarget(event)) { setDragOver(false); } }} onDragOver={beginDragOver} onDrop={handleDrop} onPointerCancel={handlePointerUp} onPointerCancelCapture={handlePointerUpCapture} onPointerDown={handlePointerDown} onPointerDownCapture={handlePointerDownCapture} onPointerMove={handlePointerMove} onPointerMoveCapture={handlePointerMoveCapture} onPointerUp={handlePointerUp} onPointerUpCapture={handlePointerUpCapture} ref={viewportRef} role="application" style={ infiniteCanvasBackgroundColor ? { backgroundColor: infiniteCanvasBackgroundColor } : undefined } > {canvasFrame.kind === "infinite" && infiniteCanvasContent ? (
{infiniteCanvasContent}
) : null} {renderEditableCanvas ? ( {renderDefaultMedia ? visibleMediaAssets.map((mediaAsset) => ( )) : null} {children ? ( {children} ) : null} ) : null} {uploadStatus || uploadFeedback ? (
{uploadStatus ? (

{uploadStatus.label} {uploadStatus.progress === undefined ? "" : ` ${Math.round(uploadStatus.progress * 100)}%`}

) : null} {uploadFeedback ? (

{uploadFeedback.message}

) : null}
) : null} ); }