free placement
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user