diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index c6b28fa2e..20721db11 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -5184,6 +5184,14 @@ iframe.preview-frame { pointer-events: none; } +.game-resource-book-scene--child { + pointer-events: auto; +} + +.game-resource-book-scene--child.is-measuring { + pointer-events: none; +} + .game-resource-book-scene.is-measuring { opacity: 0; } @@ -5197,6 +5205,7 @@ iframe.preview-frame { #fffdfa; content: ''; opacity: 0; + pointer-events: none; transition: opacity 180ms ease; } @@ -5338,6 +5347,12 @@ iframe.preview-frame { box-shadow 180ms ease; } +.game-resource-book-scene--idle + .game-resource-book-scene-card + .game-resource-card { + transition: box-shadow 180ms ease; +} + .game-resource-book-scene--main .game-resource-book-scene-card .game-resource-card { diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 3b4ccbd04..1ada65aa3 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -326,15 +326,6 @@ function defaultResourceCanvasViewports(): ResourceCanvasViewportBySortMode { const RESOURCE_CANVAS_FALLBACK_WIDTH = 900; const RESOURCE_CANVAS_FALLBACK_HEIGHT = 640; -const RESOURCE_CANVAS_MIN_DRAG_COORDINATE = -1_000_000; -const RESOURCE_CANVAS_MAX_DRAG_COORDINATE = 1_000_000; - -function normalizeResourceCanvasDragCoordinate(value: number) { - return Math.min( - RESOURCE_CANVAS_MAX_DRAG_COORDINATE, - Math.max(RESOURCE_CANVAS_MIN_DRAG_COORDINATE, Math.round(value)), - ); -} function resourceCanvasViewportElement(element: HTMLElement | null) { const pageCanvas = element?.querySelector( @@ -979,9 +970,13 @@ function ResourceBookScene({ positions, cardSizes, visibleResourceIds, - sceneSize, + viewport, renderCard, onCollapse, + onPointerDown, + onPointerMove, + onPointerUp, + onPointerCancel, }: { resources: ProjectResource[]; visibleCategoryOrder: readonly ResourceCategory[]; @@ -992,7 +987,7 @@ function ResourceBookScene({ positions: Map; cardSizes: Map; visibleResourceIds: Set; - sceneSize: { width: number; height: number }; + viewport: CanvasViewport; renderCard: ( resource: ProjectResource, layout: { @@ -1001,9 +996,15 @@ function ResourceBookScene({ width: number; height: number; rotation: number; + dragX: number; + dragY: number; }, ) => ReactNode; onCollapse: () => void; + onPointerDown: (event: ReactPointerEvent) => void; + onPointerMove: (event: ReactPointerEvent) => void; + onPointerUp: (event: ReactPointerEvent) => void; + onPointerCancel: (event: ReactPointerEvent) => void; }) { const resourcesByCategory = new Map(); resources.forEach((resource) => { @@ -1019,6 +1020,10 @@ function ResourceBookScene({ return (
{visibleCategoryOrder.map((category) => { const rect = overviewRects.get(category); @@ -1083,60 +1088,18 @@ function ResourceBookScene({ const overviewTop = (rect?.top ?? 0) + 58 + Math.min(stackIndex, 2) * 5; const overflow = stackIndex >= 3; - const positionedItems = categoryResources.map((item) => ({ - resource: item, - position: positions.get(item.id), - size: cardSizes.get(item.id) ?? { - width: 180, - height: 128, - }, - })); - const positioned = positionedItems.filter( - (item) => item.position, - ) as Array<{ - resource: ProjectResource; - position: { x: number; y: number }; - size: ResourceCanvasCardSize; - }>; - const minX = positioned.length - ? Math.min(...positioned.map((item) => item.position.x)) - : 0; - const minY = positioned.length - ? Math.min(...positioned.map((item) => item.position.y)) - : 0; - const maxX = positioned.length - ? Math.max( - ...positioned.map( - (item) => item.position.x + item.size.width, - ), - ) - : 720; - const maxY = positioned.length - ? Math.max( - ...positioned.map( - (item) => item.position.y + item.size.height, - ), - ) - : 480; - const availableWidth = Math.max(320, sceneSize.width - 96); - const availableHeight = Math.max(240, sceneSize.height - 96); - const expandedScale = Math.min( - 1, - availableWidth / Math.max(1, maxX - minX), - availableHeight / Math.max(1, maxY - minY), - ); - const expandedLeft = position - ? 48 + (position.x - minX) * expandedScale - : 48 + (stackIndex % 4) * 180; - const expandedTop = position - ? 48 + (position.y - minY) * expandedScale - : 48 + Math.floor(stackIndex / 4) * 150; const expanded = active; const layout = { - x: expanded ? expandedLeft : overviewLeft, - y: expanded ? expandedTop : overviewTop, - width: expanded ? size.width * expandedScale : 92, - height: expanded ? size.height * expandedScale : 64, + x: expanded + ? (position?.x ?? 0) * viewport.scale + viewport.x + : overviewLeft, + y: expanded + ? (position?.y ?? 0) * viewport.scale + viewport.y + : overviewTop, + width: expanded ? size.width * viewport.scale : 92, + height: expanded ? size.height * viewport.scale : 64, + dragX: expanded ? (position?.x ?? 0) : 0, + dragY: expanded ? (position?.y ?? 0) : 0, rotation: expanded ? 0 : (stackIndex % 2 ? -1 : 1) * (stackIndex + 0.5), @@ -2443,6 +2406,15 @@ export default function ProjectDevelopmentView({ } if (resourceBookView === 'child') { + const switchFitBounds = resourceCanvasContentBounds( + resourcePositionsByCategory.get(category) ?? [], + resourceCardSizeByResourceId, + ); + const switchFitViewport = fitResourceCanvasViewportToContent({ + bounds: switchFitBounds, + canvasSize: resourceBookSceneSize, + padding: RESOURCE_CANVAS_FIT_PADDING, + }); setResourceBookTransition('returning'); scheduleResourceBookTransition(() => { setActiveResourceCategory(category); @@ -2450,6 +2422,7 @@ export default function ProjectDevelopmentView({ setResourceBookView('main'); setResourceBookTransition('idle'); window.requestAnimationFrame(() => { + setResourceCanvasViewport(category, switchFitViewport); setResourceBookView('child'); setResourceBookTransition('entering'); scheduleResourceBookTransition(() => { @@ -2460,6 +2433,18 @@ export default function ProjectDevelopmentView({ return; } + const fitBounds = resourceCanvasContentBounds( + resourcePositionsByCategory.get(category) ?? [], + resourceCardSizeByResourceId, + ); + setResourceCanvasViewport( + category, + fitResourceCanvasViewportToContent({ + bounds: fitBounds, + canvasSize: resourceBookSceneSize, + padding: RESOURCE_CANVAS_FIT_PADDING, + }), + ); setActiveResourceCategory(category); setResourceBookCategory(category); setResourceBookView('child'); @@ -2474,8 +2459,12 @@ export default function ProjectDevelopmentView({ cancelResourceCardDrag, resourceBookCategory, resourceBookManagerRef, + resourceBookSceneSize, resourceBookTransition, resourceBookView, + resourceCardSizeByResourceId, + resourcePositionsByCategory, + setResourceCanvasViewport, scheduleResourceBookTransition, ], ); @@ -3497,7 +3486,7 @@ export default function ProjectDevelopmentView({ } drag.changed = true; const coordinate = (start: number, delta: number) => - normalizeResourceCanvasDragCoordinate(start + delta / drag.startScale); + start + delta / drag.startScale; const preview = { resourceId: drag.resourceId, x: coordinate(drag.startX, deltaX), @@ -3528,14 +3517,12 @@ export default function ProjectDevelopmentView({ const preview = drag.changed ? { resourceId: drag.resourceId, - x: normalizeResourceCanvasDragCoordinate( + x: drag.startX + - (event.clientX - drag.startClientX) / drag.startScale, - ), - y: normalizeResourceCanvasDragCoordinate( + (event.clientX - drag.startClientX) / drag.startScale, + y: drag.startY + - (event.clientY - drag.startClientY) / drag.startScale, - ), + (event.clientY - drag.startClientY) / drag.startScale, } : null; setResourceCardDragPreview(null); @@ -3577,7 +3564,7 @@ export default function ProjectDevelopmentView({ const target = event.target as HTMLElement; if ( target.closest( - 'button, input, textarea, select, a, .game-resource-card', + 'button, input, textarea, select, a, .game-resource-card, .game-resource-book-scene-titlebar', ) ) { return; @@ -4780,6 +4767,8 @@ export default function ProjectDevelopmentView({ width: number; height: number; rotation: number; + dragX: number; + dragY: number; }, ) => { const previewIdentity = resourceCardPreviews.identityByResourceId.get( @@ -4792,6 +4781,13 @@ export default function ProjectDevelopmentView({ resourceCardDragPreview?.resourceId === resource.id ? resourceCardDragPreview : null; + const viewport = activeResourceCanvasViewport; + const previewX = dragPreview + ? dragPreview.x * viewport.scale + viewport.x + : undefined; + const previewY = dragPreview + ? dragPreview.y * viewport.scale + viewport.y + : undefined; return ( - handleResourceCardPointerDown(event, resource, layout.x, layout.y) + handleResourceCardPointerDown( + event, + resource, + layout.dragX, + layout.dragY, + ) } onPointerMove={handleResourceCardPointerMove} onPointerUp={handleResourceCardPointerUp} @@ -4831,6 +4832,7 @@ export default function ProjectDevelopmentView({ }, [ activeCardMediaIdentity, + activeResourceCanvasViewport, handleCardMediaPlaybackChange, handleCardMediaReady, handleCardMediaToggle, @@ -5379,9 +5381,13 @@ export default function ProjectDevelopmentView({ positions={resourcePositionById} cardSizes={resourceCardSizeByResourceId} visibleResourceIds={visibleResourceIds} - sceneSize={resourceBookSceneSize} + viewport={activeResourceCanvasViewport} renderCard={renderResourceBookCard} onCollapse={returnToResourceBookMain} + onPointerDown={handleResourceCanvasPointerDown} + onPointerMove={handleResourceCanvasPointerMove} + onPointerUp={stopResourceCanvasPan} + onPointerCancel={stopResourceCanvasPan} />