From dee61700b998bd0910b11410bbe1dca6814d348b Mon Sep 17 00:00:00 2001 From: wass08 Date: Wed, 28 Jan 2026 08:47:36 +0900 Subject: [PATCH] fix mechanism --- .../components/tools/item/item-tool.tsx | 125 ++++++++++++++++-- .../tools/item/placement-strategies.ts | 10 +- .../ui/item-catalog/catalog-items.tsx | 6 +- 3 files changed, 126 insertions(+), 15 deletions(-) diff --git a/apps/editor/components/tools/item/item-tool.tsx b/apps/editor/components/tools/item/item-tool.tsx index 9707a2a6..2ce0e668 100644 --- a/apps/editor/components/tools/item/item-tool.tsx +++ b/apps/editor/components/tools/item/item-tool.tsx @@ -75,9 +75,35 @@ export const ItemTool: React.FC = () => { revalidate() } - // ---- Create initial draft ---- + /** + * Create a draft from a transition result on the first valid move. + * If placement is invalid at this position, the draft is immediately destroyed + * so no item appears in the scene until the cursor reaches a valid spot. + */ + const ensureDraft = (result: TransitionResult) => { + gridPosition.current.set(...result.gridPosition) + cursorRef.current.position.set(...result.cursorPosition) + cursorRef.current.rotation.y = result.cursorRotationY - draftNode.create(gridPosition.current, selectedItem) + draftNode.create(gridPosition.current, selectedItem) + + const draft = draftNode.current + if (draft) { + Object.assign(draft, result.nodeUpdate) + useScene.getState().updateNode(draft.id, result.nodeUpdate) + } + + if (!revalidate()) { + draftNode.destroy() + } + } + + // ---- Create initial draft (floor items only) ---- + // Wall/ceiling items are created on surface enter to avoid floating items. + + if (!selectedItem.attachTo) { + draftNode.create(gridPosition.current, selectedItem) + } revalidate() // ---- Floor Handlers ---- @@ -113,10 +139,40 @@ export const ItemTool: React.FC = () => { event.stopPropagation() applyTransition(result) + + // Try to create draft immediately if placement is valid + if (!draftNode.current) { + ensureDraft(result) + } } const onWallMove = (event: WallEvent) => { - const result = wallStrategy.move(getContext(), event) + const ctx = getContext() + + // If not yet on wall surface (e.g. entered via invalid top face), + // promote this move to an enter when hitting a valid side face. + if (ctx.state.surface !== 'wall') { + const nodes = useScene.getState().nodes + const enterResult = wallStrategy.enter(ctx, event, resolveLevelId, nodes) + if (!enterResult) return + + event.stopPropagation() + applyTransition(enterResult) + return + } + + // No draft yet (first move after enter) — create at current position if valid + if (!draftNode.current) { + const nodes = useScene.getState().nodes + const setup = wallStrategy.enter(getContext(), event, resolveLevelId, nodes) + if (!setup) return + + event.stopPropagation() + ensureDraft(setup) + return + } + + const result = wallStrategy.move(ctx, event) if (!result) return event.stopPropagation() @@ -161,8 +217,15 @@ export const ItemTool: React.FC = () => { if (result.dirtyNodeId) { useScene.getState().dirtyNodes.add(result.dirtyNodeId) } - draftNode.create(gridPosition.current, selectedItem) - revalidate() + + // Re-enter the wall — applyTransition creates the next draft at the correct position + const nodes = useScene.getState().nodes + const enterResult = wallStrategy.enter(getContext(), event, resolveLevelId, nodes) + if (enterResult) { + applyTransition(enterResult) + } else { + revalidate() + } } const onWallLeave = (event: WallEvent) => { @@ -170,7 +233,14 @@ export const ItemTool: React.FC = () => { if (!result) return event.stopPropagation() - applyTransition(result) + + // Wall/ceiling items: destroy draft so it doesn't float on the floor + if (selectedItem.attachTo) { + draftNode.destroy() + Object.assign(placementState.current, result.stateUpdate) + } else { + applyTransition(result) + } } // ---- Ceiling Handlers ---- @@ -182,9 +252,25 @@ export const ItemTool: React.FC = () => { event.stopPropagation() applyTransition(result) + + // Try to create draft immediately if placement is valid + if (!draftNode.current) { + ensureDraft(result) + } } const onCeilingMove = (event: CeilingEvent) => { + // No draft yet (first move after enter) — create at current position if valid + if (!draftNode.current && placementState.current.surface === 'ceiling') { + const nodes = useScene.getState().nodes + const setup = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes) + if (!setup) return + + event.stopPropagation() + ensureDraft(setup) + return + } + const result = ceilingStrategy.move(getContext(), event) if (!result) return @@ -208,8 +294,15 @@ export const ItemTool: React.FC = () => { event.stopPropagation() draftNode.commit(result.nodeUpdate) - draftNode.create(gridPosition.current, selectedItem) - revalidate() + + // Re-enter the ceiling — applyTransition creates the next draft at the correct position + const nodes = useScene.getState().nodes + const enterResult = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes) + if (enterResult) { + applyTransition(enterResult) + } else { + revalidate() + } } const onCeilingLeave = (event: CeilingEvent) => { @@ -217,7 +310,14 @@ export const ItemTool: React.FC = () => { if (!result) return event.stopPropagation() - applyTransition(result) + + // Wall/ceiling items: destroy draft so it doesn't float on the floor + if (selectedItem.attachTo) { + draftNode.destroy() + Object.assign(placementState.current, result.stateUpdate) + } else { + applyTransition(result) + } } // ---- Keyboard rotation ---- @@ -287,6 +387,13 @@ export const ItemTool: React.FC = () => { if (draftNode.current && placementState.current.surface === 'floor') { const mesh = sceneRegistry.nodes.get(draftNode.current.id) if (mesh) { + // If distance is large, snap immediately + const distance = mesh.position.distanceToSquared(gridPosition.current) + if (distance > 1) { + mesh.position.copy(gridPosition.current) + return + } + // Otherwise, lerp smoothly mesh.position.lerp(gridPosition.current, delta * 20) } } diff --git a/apps/editor/components/tools/item/placement-strategies.ts b/apps/editor/components/tools/item/placement-strategies.ts index e11025fc..8224e75b 100644 --- a/apps/editor/components/tools/item/placement-strategies.ts +++ b/apps/editor/components/tools/item/placement-strategies.ts @@ -95,7 +95,7 @@ export const wallStrategy = { resolveLevelId: LevelResolver, nodes: Record, ): TransitionResult | null { - const attachTo = ctx.draftItem?.asset.attachTo + const attachTo = ctx.asset.attachTo if (attachTo !== 'wall' && attachTo !== 'wall-side') return null if (!isValidWallSideFace(event.normal)) return null @@ -121,7 +121,11 @@ export const wallStrategy = { }, cursorRotationY: cursorRotation, gridPosition: [x, y, z], - cursorPosition: [x, y, z], + cursorPosition: [ + snapToHalf(event.position[0]), + snapToHalf(event.position[1]), + snapToHalf(event.position[2]), + ], stopPropagation: true, } }, @@ -230,7 +234,7 @@ export const ceilingStrategy = { resolveLevelId: LevelResolver, nodes: Record, ): TransitionResult | null { - if (ctx.draftItem?.asset.attachTo !== 'ceiling') return null + if (ctx.asset.attachTo !== 'ceiling') return null // Level guard const ceilingLevelId = resolveLevelId(event.node, nodes) diff --git a/apps/editor/components/ui/item-catalog/catalog-items.tsx b/apps/editor/components/ui/item-catalog/catalog-items.tsx index 743f0c9a..ecf64c96 100644 --- a/apps/editor/components/ui/item-catalog/catalog-items.tsx +++ b/apps/editor/components/ui/item-catalog/catalog-items.tsx @@ -363,9 +363,9 @@ export const CATALOG_ITEMS: AssetInput[] = [ 0 ], "dimensions": [ - 1.9999999999999991, - 2.0000000000000004, - 0.39999999999999936 + 2, + 2, + 0.4 ], "attachTo": "wall" },