From b95f6fdf91ad4b5040c4bb9c1be720de6b697aa2 Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Fri, 1 May 2026 01:11:11 -0700 Subject: [PATCH] PageScene: restore raycaster-based drop position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DndAsset.onPointerUp captured event.pageX/Y but threw them away; both add* actions then defaulted position to origin. PageEditor's gallery onImageDrop tried an NDC unproject but gated on editorEngine.camera, which Phase 1's CameraController extraction left undefined — the guard short-circuited and worldPosition stayed undefined. Add pickDropPosition: a deps-pattern helper that mirrors the pre-refactor timeline.getPos() — raycasts from the camera through the cursor against scene children and returns the first hit (or origin if the ray misses or the camera/canvas isn't ready). Same idiom as CameraControllerDeps / GizmoControllerDeps from Phase 2; callers build the deps inline so the helper stays decoupled from Editor and any subsystem's import surface. Both drop sites — DndAsset.onPointerUp and PageEditor's 3D gallery onImageDrop — now call the helper and forward worldPosition to addCharacter / addObject / addShape. --- .../pages/PageScene/DragAndDrop/DndAsset.ts | 18 ++++++-- .../app/src/pages/PageScene/PageEditor.tsx | 36 ++++++---------- .../PageScene/engine/pickDropPosition.ts | 41 +++++++++++++++++++ 3 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 frontend/apps/artcraft/app/src/pages/PageScene/engine/pickDropPosition.ts diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/DragAndDrop/DndAsset.ts b/frontend/apps/artcraft/app/src/pages/PageScene/DragAndDrop/DndAsset.ts index 92db953d7e..466873216d 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/DragAndDrop/DndAsset.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/DragAndDrop/DndAsset.ts @@ -4,6 +4,7 @@ import { usePageSceneStore } from "~/pages/PageScene/PageSceneStore"; import { pageHeight, pageWidth } from "~/signals"; import { AssetType } from "~/enums"; import type Editor from "~/pages/PageScene/engine/editor"; +import { pickDropPosition } from "~/pages/PageScene/engine/pickDropPosition"; import { addCharacter, addObject, @@ -88,16 +89,27 @@ class DndAsset { const positionX = event.pageX; const positionY = event.pageY; if (this.overCanvas(positionX, positionY)) { + const worldPosition = pickDropPosition( + { + getCamera: () => editor.cameraController.camera, + getCanvas: () => editor.renderer?.domElement, + getRaycastTargets: () => editor.activeScene.scene.children, + removeTransformControls: () => + editor.utils.removeTransformControls(true), + }, + positionX, + positionY, + ); if (mediaItem.type === AssetType.CHARACTER) { - void addCharacter(editor, mediaItem); + void addCharacter(editor, mediaItem, worldPosition); } else if ( mediaItem.type === AssetType.OBJECT || mediaItem.type === AssetType.SPLAT || mediaItem.type === AssetType.SKYBOX ) { - void addObject(editor, mediaItem); + void addObject(editor, mediaItem, worldPosition); } else if (mediaItem.type === AssetType.SHAPE) { - void addShape(editor, mediaItem); + void addShape(editor, mediaItem, worldPosition); } } } diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/PageEditor.tsx b/frontend/apps/artcraft/app/src/pages/PageScene/PageEditor.tsx index 5fad357d7d..efc0822845 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/PageEditor.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageScene/PageEditor.tsx @@ -50,7 +50,7 @@ import { AssetType, AUTH_STATUS } from "~/enums"; import { v4 as uuidv4 } from "uuid"; import { MediaItem } from "~/pages/PageScene/models"; import { UploaderState } from "~/models"; -import * as THREE from "three"; +import { pickDropPosition } from "./engine/pickDropPosition"; import { GalleryItem, onImageDrop, @@ -303,33 +303,23 @@ export const PageEditor = () => { if (tabStore.activeTabId === "3D") { handler = onImageDrop( (item: GalleryItem, position: { x: number; y: number }) => { - console.log("3D Drop debug (event):", { - editorEngine, - camera: editorEngine?.camera, - renderer: editorEngine?.renderer, - position, - }); - - // Calculate world position from cursor immediately on drop - let worldPosition = undefined; - if (editorEngine?.camera && editorEngine?.renderer && position) { - const rect = - editorEngine.renderer.domElement.getBoundingClientRect(); - // Convert client coordinates to canvas coordinates - const canvasX = position.x - rect.left; - const canvasY = position.y - rect.top; - const ndcX = (canvasX / rect.width) * 2 - 1; - const ndcY = -(canvasY / rect.height) * 2 + 1; - const vector = new THREE.Vector3(ndcX, ndcY, 0.5); - vector.unproject(editorEngine.camera); - worldPosition = vector; - } - (async () => { if (!editorEngine) { console.warn("Cannot drop asset: editor engine not ready"); return; } + const worldPosition = pickDropPosition( + { + getCamera: () => editorEngine.cameraController.camera, + getCanvas: () => editorEngine.renderer?.domElement, + getRaycastTargets: () => + editorEngine.activeScene.scene.children, + removeTransformControls: () => + editorEngine.utils.removeTransformControls(true), + }, + position.x, + position.y, + ); try { if (item.mediaClass === "dimensional") { const isCharacter = item.assetType === "character"; diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/pickDropPosition.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/pickDropPosition.ts new file mode 100644 index 0000000000..59157ee01c --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/pickDropPosition.ts @@ -0,0 +1,41 @@ +import * as THREE from "three"; + +export type PickDropPositionDeps = { + getCamera: () => THREE.PerspectiveCamera | null | undefined; + getCanvas: () => HTMLCanvasElement | null | undefined; + getRaycastTargets: () => THREE.Object3D[]; + removeTransformControls: () => void; +}; + +// Cast a ray from the camera through the cursor and return the first +// scene hit. Falls back to origin if the camera/canvas isn't ready or +// nothing is hit. All dependencies arrive through `deps` so the picker +// stays decoupled from Editor and any subsystem's import surface +// (matches the Phase 2 deps pattern used by CameraController etc.). +export function pickDropPosition( + deps: PickDropPositionDeps, + pageX: number, + pageY: number, +): THREE.Vector3 { + const camera = deps.getCamera(); + const canvas = deps.getCanvas(); + if (!camera || !canvas) return new THREE.Vector3(); + + // Hide the gizmo so the raycast doesn't hit it. + deps.removeTransformControls(); + + const rect = canvas.getBoundingClientRect(); + const ndc = new THREE.Vector2( + ((pageX - rect.left) / rect.width) * 2 - 1, + -((pageY - rect.top) / rect.height) * 2 + 1, + ); + + const raycaster = new THREE.Raycaster(); + raycaster.layers.enable(0); + raycaster.layers.enable(1); + raycaster.setFromCamera(ndc, camera); + + const hits = raycaster.intersectObjects(deps.getRaycastTargets(), true); + if (hits.length > 0) return hits[0].point.clone(); + return new THREE.Vector3(); +}