free placement

This commit is contained in:
wass08
2026-02-17 13:22:17 +09:00
parent a5dc766b1a
commit 101db7b34d
2 changed files with 33 additions and 8 deletions
@@ -74,6 +74,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
const placementState = useRef<PlacementState>( const placementState = useRef<PlacementState>(
config.initialState ?? { surface: 'floor', wallId: null, ceilingId: null }, config.initialState ?? { surface: 'floor', wallId: null, ceilingId: null },
) )
const shiftFreeRef = useRef(false)
// Store config callbacks in refs to avoid re-running effect when they change // Store config callbacks in refs to avoid re-running effect when they change
const configRef = useRef(config) const configRef = useRef(config)
@@ -104,8 +105,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
state: { ...placementState.current }, state: { ...placementState.current },
}) })
const getActiveValidators = () => shiftFreeRef.current
? { canPlaceOnFloor: () => ({ valid: true }), canPlaceOnWall: () => ({ valid: true }), canPlaceOnCeiling: () => ({ valid: true }) }
: validators
const revalidate = (): boolean => { const revalidate = (): boolean => {
const placeable = checkCanPlace(getContext(), validators) const placeable = shiftFreeRef.current || checkCanPlace(getContext(), validators)
const color = placeable ? 0x22c55e : 0xef4444 // green-500 : red-500 const color = placeable ? 0x22c55e : 0xef4444 // green-500 : red-500
edgeMaterial.color.setHex(color) edgeMaterial.color.setHex(color)
basePlaneMaterial.color.setHex(color) basePlaneMaterial.color.setHex(color)
@@ -196,7 +201,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
} }
const onGridClick = (event: GridEvent) => { const onGridClick = (event: GridEvent) => {
const result = floorStrategy.click(getContext(), event, validators) const result = floorStrategy.click(getContext(), event, getActiveValidators())
if (!result) return if (!result) return
// Preserve cursor rotation for the next draft // Preserve cursor rotation for the next draft
@@ -213,7 +218,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
const onWallEnter = (event: WallEvent) => { const onWallEnter = (event: WallEvent) => {
const nodes = useScene.getState().nodes const nodes = useScene.getState().nodes
const result = wallStrategy.enter(getContext(), event, resolveLevelId, nodes, validators) const result = wallStrategy.enter(getContext(), event, resolveLevelId, nodes, getActiveValidators())
if (!result) return if (!result) return
event.stopPropagation() event.stopPropagation()
@@ -235,7 +240,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
if (ctx.state.surface !== 'wall') { if (ctx.state.surface !== 'wall') {
const nodes = useScene.getState().nodes const nodes = useScene.getState().nodes
const enterResult = wallStrategy.enter(ctx, event, resolveLevelId, nodes, validators) const enterResult = wallStrategy.enter(ctx, event, resolveLevelId, nodes, getActiveValidators())
if (!enterResult) return if (!enterResult) return
event.stopPropagation() event.stopPropagation()
@@ -251,7 +256,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
if (!draftNode.current) { if (!draftNode.current) {
const nodes = useScene.getState().nodes const nodes = useScene.getState().nodes
const setup = wallStrategy.enter(getContext(), event, resolveLevelId, nodes, validators) const setup = wallStrategy.enter(getContext(), event, resolveLevelId, nodes, getActiveValidators())
if (!setup) return if (!setup) return
event.stopPropagation() event.stopPropagation()
@@ -259,7 +264,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
return return
} }
const result = wallStrategy.move(ctx, event, validators) const result = wallStrategy.move(ctx, event, getActiveValidators())
if (!result) return if (!result) return
event.stopPropagation() event.stopPropagation()
@@ -312,7 +317,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
} }
const onWallClick = (event: WallEvent) => { const onWallClick = (event: WallEvent) => {
const result = wallStrategy.click(getContext(), event, validators) const result = wallStrategy.click(getContext(), event, getActiveValidators())
if (!result) return if (!result) return
event.stopPropagation() event.stopPropagation()
@@ -423,7 +428,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
} }
const onCeilingClick = (event: CeilingEvent) => { const onCeilingClick = (event: CeilingEvent) => {
const result = ceilingStrategy.click(getContext(), event, validators) const result = ceilingStrategy.click(getContext(), event, getActiveValidators())
if (!result) return if (!result) return
event.stopPropagation() event.stopPropagation()
@@ -474,6 +479,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
const ROTATION_STEP = Math.PI / 2 const ROTATION_STEP = Math.PI / 2
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Shift') {
shiftFreeRef.current = true
revalidate()
return
}
const draft = draftNode.current const draft = draftNode.current
if (!draft) return if (!draft) return
@@ -495,7 +506,16 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
revalidate() revalidate()
} }
} }
const onKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Shift') {
shiftFreeRef.current = false
revalidate()
}
}
window.addEventListener('keydown', onKeyDown) window.addEventListener('keydown', onKeyDown)
window.addEventListener('keyup', onKeyUp)
// ---- tool:cancel (Escape / programmatic) ---- // ---- tool:cancel (Escape / programmatic) ----
const onCancel = () => { const onCancel = () => {
@@ -550,6 +570,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
emitter.off('ceiling:leave', onCeilingLeave) emitter.off('ceiling:leave', onCeilingLeave)
emitter.off('tool:cancel', onCancel) emitter.off('tool:cancel', onCancel)
window.removeEventListener('keydown', onKeyDown) window.removeEventListener('keydown', onKeyDown)
window.removeEventListener('keyup', onKeyUp)
window.removeEventListener('contextmenu', onContextMenu) window.removeEventListener('contextmenu', onContextMenu)
} }
}, [asset, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling, draftNode]) }, [asset, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling, draftNode])
@@ -13,6 +13,10 @@ export function ItemHelper({ showEsc }: ItemHelperProps) {
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">T</kbd> <kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">T</kbd>
<span className="text-muted-foreground">Rotate clockwise</span> <span className="text-muted-foreground">Rotate clockwise</span>
</div> </div>
<div className="flex items-center gap-2 text-sm">
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Shift</kbd>
<span className="text-muted-foreground">Free place</span>
</div>
{showEsc && ( {showEsc && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd> <kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd>