fix mechanism

This commit is contained in:
wass08
2026-01-28 08:47:36 +09:00
parent 8646cc8748
commit dee61700b9
3 changed files with 126 additions and 15 deletions
+111 -4
View File
@@ -75,9 +75,35 @@ export const ItemTool: React.FC = () => {
revalidate() 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() revalidate()
// ---- Floor Handlers ---- // ---- Floor Handlers ----
@@ -113,10 +139,40 @@ export const ItemTool: React.FC = () => {
event.stopPropagation() event.stopPropagation()
applyTransition(result) applyTransition(result)
// Try to create draft immediately if placement is valid
if (!draftNode.current) {
ensureDraft(result)
}
} }
const onWallMove = (event: WallEvent) => { 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 if (!result) return
event.stopPropagation() event.stopPropagation()
@@ -161,17 +217,31 @@ export const ItemTool: React.FC = () => {
if (result.dirtyNodeId) { if (result.dirtyNodeId) {
useScene.getState().dirtyNodes.add(result.dirtyNodeId) useScene.getState().dirtyNodes.add(result.dirtyNodeId)
} }
draftNode.create(gridPosition.current, selectedItem)
// 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() revalidate()
} }
}
const onWallLeave = (event: WallEvent) => { const onWallLeave = (event: WallEvent) => {
const result = wallStrategy.leave(getContext()) const result = wallStrategy.leave(getContext())
if (!result) return if (!result) return
event.stopPropagation() event.stopPropagation()
// 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) applyTransition(result)
} }
}
// ---- Ceiling Handlers ---- // ---- Ceiling Handlers ----
@@ -182,9 +252,25 @@ export const ItemTool: React.FC = () => {
event.stopPropagation() event.stopPropagation()
applyTransition(result) applyTransition(result)
// Try to create draft immediately if placement is valid
if (!draftNode.current) {
ensureDraft(result)
}
} }
const onCeilingMove = (event: CeilingEvent) => { 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) const result = ceilingStrategy.move(getContext(), event)
if (!result) return if (!result) return
@@ -208,17 +294,31 @@ export const ItemTool: React.FC = () => {
event.stopPropagation() event.stopPropagation()
draftNode.commit(result.nodeUpdate) draftNode.commit(result.nodeUpdate)
draftNode.create(gridPosition.current, selectedItem)
// 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() revalidate()
} }
}
const onCeilingLeave = (event: CeilingEvent) => { const onCeilingLeave = (event: CeilingEvent) => {
const result = ceilingStrategy.leave(getContext()) const result = ceilingStrategy.leave(getContext())
if (!result) return if (!result) return
event.stopPropagation() event.stopPropagation()
// 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) applyTransition(result)
} }
}
// ---- Keyboard rotation ---- // ---- Keyboard rotation ----
@@ -287,6 +387,13 @@ export const ItemTool: React.FC = () => {
if (draftNode.current && placementState.current.surface === 'floor') { if (draftNode.current && placementState.current.surface === 'floor') {
const mesh = sceneRegistry.nodes.get(draftNode.current.id) const mesh = sceneRegistry.nodes.get(draftNode.current.id)
if (mesh) { 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) mesh.position.lerp(gridPosition.current, delta * 20)
} }
} }
@@ -95,7 +95,7 @@ export const wallStrategy = {
resolveLevelId: LevelResolver, resolveLevelId: LevelResolver,
nodes: Record<string, AnyNode>, nodes: Record<string, AnyNode>,
): TransitionResult | null { ): TransitionResult | null {
const attachTo = ctx.draftItem?.asset.attachTo const attachTo = ctx.asset.attachTo
if (attachTo !== 'wall' && attachTo !== 'wall-side') return null if (attachTo !== 'wall' && attachTo !== 'wall-side') return null
if (!isValidWallSideFace(event.normal)) return null if (!isValidWallSideFace(event.normal)) return null
@@ -121,7 +121,11 @@ export const wallStrategy = {
}, },
cursorRotationY: cursorRotation, cursorRotationY: cursorRotation,
gridPosition: [x, y, z], gridPosition: [x, y, z],
cursorPosition: [x, y, z], cursorPosition: [
snapToHalf(event.position[0]),
snapToHalf(event.position[1]),
snapToHalf(event.position[2]),
],
stopPropagation: true, stopPropagation: true,
} }
}, },
@@ -230,7 +234,7 @@ export const ceilingStrategy = {
resolveLevelId: LevelResolver, resolveLevelId: LevelResolver,
nodes: Record<string, AnyNode>, nodes: Record<string, AnyNode>,
): TransitionResult | null { ): TransitionResult | null {
if (ctx.draftItem?.asset.attachTo !== 'ceiling') return null if (ctx.asset.attachTo !== 'ceiling') return null
// Level guard // Level guard
const ceilingLevelId = resolveLevelId(event.node, nodes) const ceilingLevelId = resolveLevelId(event.node, nodes)
@@ -363,9 +363,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
0 0
], ],
"dimensions": [ "dimensions": [
1.9999999999999991, 2,
2.0000000000000004, 2,
0.39999999999999936 0.4
], ],
"attachTo": "wall" "attachTo": "wall"
}, },