free mode for slabs and ceiling

This commit is contained in:
wass08
2026-02-18 14:00:28 +09:00
parent 586bd9f16c
commit 5d61e60f95
4 changed files with 44 additions and 22 deletions
@@ -73,8 +73,10 @@ export const CeilingTool: React.FC = () => {
const [points, setPoints] = useState<Array<[number, number]>>([]) const [points, setPoints] = useState<Array<[number, number]>>([])
const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0]) const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
const [snappedCursorPosition, setSnappedCursorPosition] = useState<[number, number]>([0, 0])
const [levelY, setLevelY] = useState(0) const [levelY, setLevelY] = useState(0)
const previousSnappedPointRef = useRef<[number, number] | null>(null) const previousSnappedPointRef = useRef<[number, number] | null>(null)
const shiftPressed = useRef(false)
// Update cursor position and lines on grid move // Update cursor position and lines on grid move
useEffect(() => { useEffect(() => {
@@ -93,9 +95,10 @@ export const CeilingTool: React.FC = () => {
const ceilingY = event.position[1] + CEILING_HEIGHT const ceilingY = event.position[1] + CEILING_HEIGHT
const gridY = event.position[1] + GRID_OFFSET const gridY = event.position[1] + GRID_OFFSET
// Calculate snapped display position // Calculate snapped display position (bypass snap when Shift is held)
const lastPoint = points[points.length - 1] const lastPoint = points[points.length - 1]
const displayPoint = lastPoint ? calculateSnapPoint(lastPoint, gridPosition) : gridPosition const displayPoint = (shiftPressed.current || !lastPoint) ? gridPosition : calculateSnapPoint(lastPoint, gridPosition)
setSnappedCursorPosition(displayPoint)
// Play snap sound when the snapped position actually changes (only when drawing) // Play snap sound when the snapped position actually changes (only when drawing)
if (points.length > 0 && previousSnappedPointRef.current && if (points.length > 0 && previousSnappedPointRef.current &&
@@ -111,9 +114,8 @@ export const CeilingTool: React.FC = () => {
const onGridClick = (_event: GridEvent) => { const onGridClick = (_event: GridEvent) => {
if (!currentLevelId) return if (!currentLevelId) return
// Calculate snapped click point // Use the last displayed snapped position (respects Shift state from onGridMove)
const lastPoint = points[points.length - 1] const clickPoint = previousSnappedPointRef.current ?? cursorPosition
const clickPoint = lastPoint ? calculateSnapPoint(lastPoint, cursorPosition) : cursorPosition
// Check if clicking on the first point to close the shape // Check if clicking on the first point to close the shape
const firstPoint = points[0] const firstPoint = points[0]
@@ -148,12 +150,19 @@ export const CeilingTool: React.FC = () => {
setPoints([]) setPoints([])
} }
const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = true }
const onKeyUp = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = false }
document.addEventListener('keydown', onKeyDown)
document.addEventListener('keyup', onKeyUp)
emitter.on('grid:move', onGridMove) emitter.on('grid:move', onGridMove)
emitter.on('grid:click', onGridClick) emitter.on('grid:click', onGridClick)
emitter.on('grid:double-click', onGridDoubleClick) emitter.on('grid:double-click', onGridDoubleClick)
emitter.on('tool:cancel', onCancel) emitter.on('tool:cancel', onCancel)
return () => { return () => {
document.removeEventListener('keydown', onKeyDown)
document.removeEventListener('keyup', onKeyUp)
emitter.off('grid:move', onGridMove) emitter.off('grid:move', onGridMove)
emitter.off('grid:click', onGridClick) emitter.off('grid:click', onGridClick)
emitter.off('grid:double-click', onGridDoubleClick) emitter.off('grid:double-click', onGridDoubleClick)
@@ -172,8 +181,7 @@ export const CeilingTool: React.FC = () => {
} }
const ceilingY = levelY + CEILING_HEIGHT const ceilingY = levelY + CEILING_HEIGHT
const lastPoint = points[points.length - 1] const snappedCursor = snappedCursorPosition
const snappedCursor = lastPoint ? calculateSnapPoint(lastPoint, cursorPosition) : cursorPosition
// Build main line points // Build main line points
const linePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, ceilingY, z)) const linePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, ceilingY, z))
@@ -201,14 +209,13 @@ export const CeilingTool: React.FC = () => {
} else { } else {
closingLineRef.current.visible = false closingLineRef.current.visible = false
} }
}, [points, cursorPosition, levelY]) }, [points, snappedCursorPosition, levelY])
// Create preview shape when we have 3+ points // Create preview shape when we have 3+ points
const previewShape = useMemo(() => { const previewShape = useMemo(() => {
if (points.length < 3) return null if (points.length < 3) return null
const lastPoint = points[points.length - 1] const snappedCursor = snappedCursorPosition
const snappedCursor = lastPoint ? calculateSnapPoint(lastPoint, cursorPosition) : cursorPosition
const allPoints = [...points, snappedCursor] const allPoints = [...points, snappedCursor]
@@ -230,7 +237,7 @@ export const CeilingTool: React.FC = () => {
shape.closePath() shape.closePath()
return shape return shape
}, [points, cursorPosition]) }, [points, snappedCursorPosition])
return ( return (
<group> <group>
+18 -11
View File
@@ -71,8 +71,10 @@ export const SlabTool: React.FC = () => {
const [points, setPoints] = useState<Array<[number, number]>>([]) const [points, setPoints] = useState<Array<[number, number]>>([])
const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0]) const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
const [snappedCursorPosition, setSnappedCursorPosition] = useState<[number, number]>([0, 0])
const [levelY, setLevelY] = useState(0) const [levelY, setLevelY] = useState(0)
const previousSnappedPointRef = useRef<[number, number] | null>(null) const previousSnappedPointRef = useRef<[number, number] | null>(null)
const shiftPressed = useRef(false)
// Update cursor position and lines on grid move // Update cursor position and lines on grid move
useEffect(() => { useEffect(() => {
@@ -88,9 +90,10 @@ export const SlabTool: React.FC = () => {
setCursorPosition(gridPosition) setCursorPosition(gridPosition)
setLevelY(event.position[1]) setLevelY(event.position[1])
// Calculate snapped display position // Calculate snapped display position (bypass snap when Shift is held)
const lastPoint = points[points.length - 1] const lastPoint = points[points.length - 1]
const displayPoint = lastPoint ? calculateSnapPoint(lastPoint, gridPosition) : gridPosition const displayPoint = (shiftPressed.current || !lastPoint) ? gridPosition : calculateSnapPoint(lastPoint, gridPosition)
setSnappedCursorPosition(displayPoint)
// Play snap sound when the snapped position actually changes (only when drawing) // Play snap sound when the snapped position actually changes (only when drawing)
if (points.length > 0 && previousSnappedPointRef.current && if (points.length > 0 && previousSnappedPointRef.current &&
@@ -105,9 +108,8 @@ export const SlabTool: React.FC = () => {
const onGridClick = (_event: GridEvent) => { const onGridClick = (_event: GridEvent) => {
if (!currentLevelId) return if (!currentLevelId) return
// Calculate snapped click point // Use the last displayed snapped position (respects Shift state from onGridMove)
const lastPoint = points[points.length - 1] const clickPoint = previousSnappedPointRef.current ?? cursorPosition
const clickPoint = lastPoint ? calculateSnapPoint(lastPoint, cursorPosition) : cursorPosition
// Check if clicking on the first point to close the shape // Check if clicking on the first point to close the shape
const firstPoint = points[0] const firstPoint = points[0]
@@ -142,12 +144,19 @@ export const SlabTool: React.FC = () => {
setPoints([]) setPoints([])
} }
const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = true }
const onKeyUp = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = false }
document.addEventListener('keydown', onKeyDown)
document.addEventListener('keyup', onKeyUp)
emitter.on('grid:move', onGridMove) emitter.on('grid:move', onGridMove)
emitter.on('grid:click', onGridClick) emitter.on('grid:click', onGridClick)
emitter.on('grid:double-click', onGridDoubleClick) emitter.on('grid:double-click', onGridDoubleClick)
emitter.on('tool:cancel', onCancel) emitter.on('tool:cancel', onCancel)
return () => { return () => {
document.removeEventListener('keydown', onKeyDown)
document.removeEventListener('keyup', onKeyUp)
emitter.off('grid:move', onGridMove) emitter.off('grid:move', onGridMove)
emitter.off('grid:click', onGridClick) emitter.off('grid:click', onGridClick)
emitter.off('grid:double-click', onGridDoubleClick) emitter.off('grid:double-click', onGridDoubleClick)
@@ -166,8 +175,7 @@ export const SlabTool: React.FC = () => {
} }
const y = levelY + Y_OFFSET const y = levelY + Y_OFFSET
const lastPoint = points[points.length - 1] const snappedCursor = snappedCursorPosition
const snappedCursor = lastPoint ? calculateSnapPoint(lastPoint, cursorPosition) : cursorPosition
// Build main line points // Build main line points
const linePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, y, z)) const linePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, y, z))
@@ -195,14 +203,13 @@ export const SlabTool: React.FC = () => {
} else { } else {
closingLineRef.current.visible = false closingLineRef.current.visible = false
} }
}, [points, cursorPosition, levelY]) }, [points, snappedCursorPosition, levelY])
// Create preview shape when we have 3+ points // Create preview shape when we have 3+ points
const previewShape = useMemo(() => { const previewShape = useMemo(() => {
if (points.length < 3) return null if (points.length < 3) return null
const lastPoint = points[points.length - 1] const snappedCursor = snappedCursorPosition
const snappedCursor = lastPoint ? calculateSnapPoint(lastPoint, cursorPosition) : cursorPosition
const allPoints = [...points, snappedCursor] const allPoints = [...points, snappedCursor]
@@ -224,7 +231,7 @@ export const SlabTool: React.FC = () => {
shape.closePath() shape.closePath()
return shape return shape
}, [points, cursorPosition]) }, [points, snappedCursorPosition])
return ( return (
<group> <group>
@@ -1,6 +1,10 @@
export function CeilingHelper() { export function CeilingHelper() {
return ( return (
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md"> <div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
<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">Allow non-45° angles</span>
</div>
<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>
<span className="text-muted-foreground">Cancel</span> <span className="text-muted-foreground">Cancel</span>
@@ -1,6 +1,10 @@
export function SlabHelper() { export function SlabHelper() {
return ( return (
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md"> <div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
<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">Allow non-45° angles</span>
</div>
<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>
<span className="text-muted-foreground">Cancel</span> <span className="text-muted-foreground">Cancel</span>