From 12d24719cf1b0fa7ff10bf3809dbebbda51d73ab Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 21:32:49 +0200 Subject: [PATCH] feat(floorplan-editor): polish height slider + add hand tool for canvas pan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related polish improvements after the swatch-column → vertical- slider swap. Slider - Wider track (18 px, was 14 px) for a more comfortable click area with the same on-screen footprint. - Min / max chips above and below the rail (HEIGHT_BRUSH_MIN / _MAX) so users know which end is high and which is low without hovering to discover. - Thumb now uses a warm amber radial gradient (#fff7c4 → #facc15 → #ca8a04) on a dark brown border with a soft drop shadow + inset highlight, instead of the flat yellow disc. Hover adds a white ring; drag swaps it for a darker ring — clear gesture feedback. - Track gains a hover/drag glow (inset white seam + amber outline via boxShadow) so you can tell the slider has focus before you even click. Hand tool (canvas pan) - New FloorplanToolbar button (FaHandPaper, sticky toggle, emerald fill when active) ties to a new state lifted into FloorplanEditorView. When the hand is active, plain left-click + drag pans the canvas instead of brushing tiles. Cursor flips to grab / grabbing accordingly. - FloorplanCanvasSVG's isPanGesture predicate becomes: middle-mouse OR Shift+left-click OR (panMode && left-click). Shift / middle still work whether or not the hand is on so power users keep their muscle memory. - No change to the reducer (panMode is a canvas-level UI flag, not a brush action — keeps state/types tight). --- .../floorplan-editor/FloorplanEditorView.tsx | 5 +- .../views/FloorplanCanvasSVG.tsx | 23 +++- .../views/FloorplanHeightPicker.tsx | 100 ++++++++++-------- .../views/FloorplanToolbar.tsx | 18 +++- 4 files changed, 97 insertions(+), 49 deletions(-) diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index d9bdd9c..c57ff6c 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -28,6 +28,7 @@ export const FloorplanEditorView: FC = () => const [ isVisible, setIsVisible ] = useState(false); const [ importExportVisible, setImportExportVisible ] = useState(false); const [ liveSync, setLiveSync ] = useState(true); + const [ panMode, setPanMode ] = useState(false); const { state, dispatch, loadFromServer, undo, redo, canUndo, canRedo } = useFloorplanReducer(); const originalRef = useRef<{ tilemap: string; @@ -232,11 +233,13 @@ export const FloorplanEditorView: FC = () => canRedo={ canRedo } onUndo={ undo } onRedo={ redo } + panMode={ panMode } + onTogglePanMode={ () => setPanMode(v => !v) } /> dispatch({ type: 'BRUSH_SET', h }) } /> - + diff --git a/src/components/floorplan-editor/views/FloorplanCanvasSVG.tsx b/src/components/floorplan-editor/views/FloorplanCanvasSVG.tsx index a553e23..19411a9 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasSVG.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasSVG.tsx @@ -9,6 +9,13 @@ import { TILE_SIZE, MAX_NUM_TILE_PER_AXIS } from '../state/constants'; type Props = { state: FloorplanState; dispatch: Dispatch; + /** + * When true, left-click + drag pans the canvas instead of + * brushing. Driven by the hand-tool toggle in the toolbar. + * Shift+drag and middle-mouse drag always pan regardless of + * this flag. + */ + panMode?: boolean; }; const VIEWBOX_W = 2048; @@ -76,7 +83,7 @@ const computeRoomBounds = (state: FloorplanState): { x: number; y: number; w: nu }; }; -export const FloorplanCanvasSVG: FC = ({ state, dispatch }) => +export const FloorplanCanvasSVG: FC = ({ state, dispatch, panMode }) => { const svgRef = useRef(null); const [ zoom, setZoom ] = useState(1); @@ -204,14 +211,24 @@ export const FloorplanCanvasSVG: FC = ({ state, dispatch }) => }; }, [ visW ]); - const isPanGesture = (e: ReactPointerEvent): boolean => e.button === 1 || (e.button === 0 && e.shiftKey); + // Pan gestures: middle-mouse, Shift+left-click, and (when the + // hand-tool is active) plain left-click. The hand-tool toggle + // is the toolbar affordance — Shift / middle still work even + // when the hand isn't on, so power users keep their muscle + // memory. + const isPanGesture = (e: ReactPointerEvent): boolean => + e.button === 1 + || (e.button === 0 && e.shiftKey) + || (e.button === 0 && Boolean(panMode)); + + const cursorClass = isPanning ? 'cursor-grabbing' : panMode ? 'cursor-grab' : ''; return (
{ diff --git a/src/components/floorplan-editor/views/FloorplanHeightPicker.tsx b/src/components/floorplan-editor/views/FloorplanHeightPicker.tsx index f1341f4..758dec8 100644 --- a/src/components/floorplan-editor/views/FloorplanHeightPicker.tsx +++ b/src/components/floorplan-editor/views/FloorplanHeightPicker.tsx @@ -7,30 +7,38 @@ type Props = { onSelect: (h: number) => void; }; -const TRACK_W = 14; +const TRACK_W = 18; const TRACK_H = 260; -const THUMB_DIAM = 24; +const THUMB_DIAM = 28; +const RAIL_GUTTER = 4; /** - * Vertical brush-height slider. The track is a top-down gradient - * built from the real tile-fill colours, so the user still sees - * which colour maps to which height — the swatch column it - * replaces communicated the same mapping a swatch at a time. - * The thumb shows the currently picked height as a number and is - * fully drag-aware (click anywhere on the track to jump, then - * drag without releasing). + * Vertical brush-height slider. + * + * Track - discrete-step gradient built from the real tile-fill + * colours, top = HEIGHT_BRUSH_MAX, bottom = HEIGHT_BRUSH_MIN. + * Each height owns a clear band so colour <-> height stays + * legible at a glance, exactly like the swatch column it + * replaces. + * Min/max - small chip labels float above and below the rail so the + * user knows what the endpoints mean without trial and + * error. + * Thumb - amber radial gradient on a soft drop shadow, white ring + * when hovered, darker ring while dragging. Renders the + * current value in the middle so the user reads the + * number directly off the handle. + * Gesture - click the rail to jump, click-and-drag the thumb (or + * rail) to scrub. Window-level pointer listeners keep + * the drag alive even when the cursor leaves the narrow + * strip. Vertical scroll on touch is suppressed. */ export const FloorplanHeightPicker: FC = ({ selectedH, onSelect }) => { const count = HEIGHT_BRUSH_MAX - HEIGHT_BRUSH_MIN + 1; const trackRef = useRef(null); const [ isDragging, setIsDragging ] = useState(false); + const [ isHovering, setIsHovering ] = useState(false); - // Top of the gradient is HEIGHT_BRUSH_MAX (matches the legacy - // swatch column layout). Each step is `100 / (count - 1)` % of - // the track height. Building hard stops gives a discrete-step - // gradient (clear band per height) rather than a smooth blend - // — closer to the original swatches, easier for users to read. const gradient = useMemo(() => { const stops: string[] = []; @@ -75,8 +83,6 @@ export const FloorplanHeightPicker: FC = ({ selectedH, onSelect }) => setIsDragging(true); }, [ heightFromClientY, onSelect, selectedH ]); - // While dragging, listen on window so the slider keeps tracking - // even when the pointer leaves the narrow track strip. useEffect(() => { if(!isDragging) return; @@ -100,44 +106,52 @@ export const FloorplanHeightPicker: FC = ({ selectedH, onSelect }) => }; }, [ isDragging, heightFromClientY, onSelect, selectedH ]); - // Thumb centre as a % of the track height. At selectedH == - // HEIGHT_BRUSH_MAX the thumb sits at 0 % (top), at the min it - // sits at 100 % (bottom). Translating Y by -50 % then re- - // centres the circle on that point. const thumbPct = ((HEIGHT_BRUSH_MAX - selectedH) / (count - 1)) * 100; return (
-
-
- { selectedH } + + { HEIGHT_BRUSH_MAX } + +
+
setIsHovering(true) } + onPointerLeave={ () => setIsHovering(false) } + /> +
+ { selectedH } +
+ + { HEIGHT_BRUSH_MIN } +
); }; diff --git a/src/components/floorplan-editor/views/FloorplanToolbar.tsx b/src/components/floorplan-editor/views/FloorplanToolbar.tsx index dae9bf8..d941722 100644 --- a/src/components/floorplan-editor/views/FloorplanToolbar.tsx +++ b/src/components/floorplan-editor/views/FloorplanToolbar.tsx @@ -1,5 +1,5 @@ import { Dispatch, FC } from 'react'; -import { FaRedo, FaUndo } from 'react-icons/fa'; +import { FaHandPaper, FaRedo, FaUndo } from 'react-icons/fa'; import { LocalizeText } from '../../../api'; import { Base, Flex, Text } from '../../../common'; import { FloorplanAction, FloorActionMode, FloorplanState } from '../state/types'; @@ -11,6 +11,8 @@ type Props = { canRedo?: boolean; onUndo?: () => void; onRedo?: () => void; + panMode?: boolean; + onTogglePanMode?: () => void; }; const BRUSH_BUTTONS: { id: string; mode: FloorActionMode; iconClass: string }[] = [ @@ -21,7 +23,7 @@ const BRUSH_BUTTONS: { id: string; mode: FloorActionMode; iconClass: string }[] { id: 'tool-door', mode: 'DOOR', iconClass: 'icon-set-door' } ]; -export const FloorplanToolbar: FC = ({ state, dispatch, canUndo, canRedo, onUndo, onRedo }) => +export const FloorplanToolbar: FC = ({ state, dispatch, canUndo, canRedo, onUndo, onRedo, panMode, onTogglePanMode }) => { return ( @@ -48,6 +50,18 @@ export const FloorplanToolbar: FC = ({ state, dispatch, canUndo, canRedo, className={ `nitro-icon icon-set-squaresselect ${ state.squareSelect ? 'border border-primary' : '' }` } onClick={ () => dispatch({ type: 'SQUARE_SELECT_TOGGLE' }) } /> + { onTogglePanMode && ( + + + + ) } { (onUndo || onRedo) && (