PageScene: restore raycaster-based drop position

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.
This commit is contained in:
Tanish Manku committed 2026-05-01 01:11:11 -07:00
1 parent 3af5291150
commit b95f6fdf91
3 files changed
+69 -26

No files matched your search

@@ -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);
}
}
}
@@ -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";
@@ -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();
}