import { CSSProperties, ReactNode, Ref } from 'react'; import { FloorPlan, OpeningKind, PlanParseResult, PlanPoint, PlanRoom, PlanSelection, PlanView } from '../floorPlan/index.js'; import { IconContent } from '../types/icon.types.js'; import { HtmlProps, SlotClassNames, SlotStyles } from '../types/slots.types.js'; /** * The drawing tools. * * Each opening kind is its own tool rather than one opening tool with a kind sub-mode: * that is one less piece of state, one less prop to wire up, and one less way to be in * a mode you did not realise you were in. * * There is deliberately no "room" tool. Rooms are the enclosed faces the walls form, so * drawing a closed shape with `Rect` or `Wall` already produces one. A mode that * declared which walls belonged to which room would reintroduce exactly the ownership * relation that made a door unable to cut a shared wall in the first place. */ export type RoomDrawerTool = 'select' | 'wall' | 'rect' | 'door' | 'window' | 'archway' | 'measure'; export type RoomDrawerChangeReason = 'draw-wall' | 'draw-rect' | 'move-node' | 'move-wall' | 'move-room' | 'nudge' | 'add-opening' | 'move-opening' | 'edit-opening' | 'edit-room' | 'edit-wall' | 'delete' | 'add-floor' | 'duplicate-floor' | 'remove-floor' | 'rename-floor' | 'move-floor' | 'edit-floor' | 'clear' | 'import' | 'undo' | 'redo' | 'external'; export interface RoomDrawerChangeMeta { reason: RoomDrawerChangeReason; /** * `false` for the intermediate values emitted while a pointer gesture is still in * flight, `true` for the one that is also pushed onto the undo stack. `onChangeEnd` * only ever fires with `true`. */ committed: boolean; /** Ids of whatever the change touched, for syncing an external selection. */ targetIds: string[]; } /** * Every string the drawer produces of its own, for localisation: its chrome, and the names it * gives the rooms and floors it creates, which are stored in the plan. */ export interface RoomDrawerLabels { /** The toolbar, read out by screen readers. */ tools?: string; /** Tooltip on the storey height field. */ storeyHeightHint?: string; /** Tooltip on an opening's sill height field. */ sillHeightHint?: string; select?: string; wall?: string; rect?: string; measure?: string; door?: string; window?: string; archway?: string; undo?: string; redo?: string; download?: string; upload?: string; underlay?: string; addFloor?: string; floorName?: string; duplicateFloor?: string; deleteFloor?: string; floorHeight?: string; moveFloorUp?: string; moveFloorDown?: string; /** Name of a new room. `n` counts the floor's rooms, skipping names already taken. */ newRoomName?: (n: number) => string; /** Name of a floor added with the `+` button. `n` counts the floors, skipping names already taken. */ newFloorName?: (n: number) => string; /** Name of a copied floor, from the name of the floor it copies. */ copiedFloorName?: (name: string) => string; /** Name of the one floor an empty plan starts with, uncontrolled or after `clear()`. */ initialFloorName?: string; /** The inspector's noun for a floor, shown when nothing is selected. */ selectedFloor?: string; /** Shown in the hint line when an opened file could not be read at all. */ importFailed?: string; snap?: string; /** The angle lock toggle, given `constrainAngle`. */ angleLock?: (angle: number) => string; grid?: string; zoomIn?: string; zoomOut?: string; fit?: string; roomName?: string; addCorner?: string; thickness?: string; width?: string; sill?: string; deleteSelection?: string; /** What the inspector calls several things selected at once. */ selectedCount?: (count: number) => string; /** What the inspector calls the current selection. */ selectedRoom?: string; selectedWall?: string; selectedCorner?: string; selectedDoor?: string; selectedWindow?: string; selectedArchway?: string; selectedGap?: string; /** Hint for the select tool with `multiSelect` on. */ hintSelect?: string; /** Hint for the select tool with `multiSelect` off. */ hintSelectSingle?: string; hintWall?: string; hintRect?: string; hintMeasure?: string; hintOpening?: string; /** A drawing angle measured from the previous wall, given the formatted angle. */ relativeAngle?: (angle: string) => string; } /** Every icon in the chrome is overridable. */ export interface RoomDrawerIcons { select?: IconContent; wall?: IconContent; rect?: IconContent; measure?: IconContent; door?: IconContent; window?: IconContent; archway?: IconContent; undo?: IconContent; redo?: IconContent; download?: IconContent; upload?: IconContent; underlay?: IconContent; addFloor?: IconContent; duplicateFloor?: IconContent; moveFloorUp?: IconContent; moveFloorDown?: IconContent; snap?: IconContent; angleLock?: IconContent; grid?: IconContent; zoomIn?: IconContent; zoomOut?: IconContent; fit?: IconContent; delete?: IconContent; } export type RoomDrawerSlots = 'root' | 'toolbar' | 'toolButton' | 'toolIcon' | 'viewControls' | 'viewButton' | 'zoomLabel' | 'canvasWrapper' | 'canvas' | 'grid' | 'rooms' | 'roomFill' | 'roomLabel' | 'walls' | 'wallBody' | 'openings' | 'opening' | 'overlay' | 'measureLine' | 'floorBar' | 'floorTab' | 'inspector' | 'inspectorLabel' | 'inspectorField' | 'hint'; export interface RoomDrawerHandle { /** The current plan, read synchronously. Useful in uncontrolled mode. */ getPlan: () => FloorPlan; /** Replaces the plan. Pushes one history entry unless `resetHistory` is set. */ setPlan: (plan: FloorPlan, options?: { resetHistory?: boolean; }) => void; /** Empties the plan, as one history entry. */ clear: () => void; undo: () => void; redo: () => void; canUndo: () => boolean; canRedo: () => boolean; /** Drops past and future without touching the plan. */ clearHistory: () => void; /** The floor being edited, and switching to another. */ getFloorId: () => string; setFloorId: (floorId: string) => void; /** Adds an empty storey above the current one and switches to it. Returns its id. */ addFloor: (options?: { label?: string; }) => string; /** Copies the current storey, geometry and all, and switches to the copy. */ duplicateFloor: () => string | null; /** Removes a storey. Refuses to remove the last one. */ removeFloor: (floorId?: string) => void; renameFloor: (floorId: string, label: string) => void; /** Moves a storey up or down the stack, re-stacking the elevations behind it. */ moveFloor: (floorId: string, toIndex: number) => void; /** Storey height in millimetres. Everything above moves to suit. */ setFloorHeight: (floorId: string, heightMm: number) => void; /** * Removes every ruler measurement. They are view state, not plan data, so they never * reach `onChange`, a file, or the undo stack. */ clearMeasurements: () => void; /** Writes the current plan to a JSON file, as the download button does. */ download: (fileName?: string) => void; /** * Reads JSON text as a plan and loads it, as one undoable entry. Returns what it made * of the text: a `null` plan means nothing was loaded. Use it to accept a file from * your own UI, a drop target, or a server. */ loadJson: (text: string) => PlanParseResult; /** Everything currently selected, in selection order. Empty when nothing is. */ getSelection: () => PlanSelection[]; /** Replaces the selection. A single entry or `null` is accepted for convenience. */ select: (selection: readonly PlanSelection[] | PlanSelection | null) => void; /** Adds to, removes from, or toggles entries without replacing the rest. */ addToSelection: (selection: readonly PlanSelection[] | PlanSelection) => void; toggleSelection: (entry: PlanSelection) => void; selectAll: () => void; /** Deletes everything selected, with the same rules as the Delete key. */ deleteSelection: () => void; getView: () => PlanView; setView: (view: PlanView) => void; zoomBy: (factor: number) => void; /** Fits the whole plan into the viewport. */ fitToContent: (options?: { padding?: number; }) => void; /** Screen (client) coordinates to world, and back. */ toWorld: (clientX: number, clientY: number) => PlanPoint; toScreen: (point: PlanPoint) => { x: number; y: number; }; getSvgElement: () => SVGSVGElement | null; } export interface RoomDrawerProps extends Omit, 'onChange' | 'onSelect' | 'defaultValue'> { /** Controlled plan. Omit for uncontrolled. */ value?: FloorPlan; /** Initial plan when uncontrolled. Defaults to an empty single-floor plan. */ defaultValue?: FloorPlan; /** * Fires for every mutation, including the intermediate values emitted during a drag * (`meta.committed === false`), so a controlled parent renders live feedback. Mirrors * `Slider`'s `onChange` / `onChangeEnd` pair. */ onChange?: (plan: FloorPlan, meta: RoomDrawerChangeMeta) => void; /** Fires once per completed gesture or discrete edit, always with `committed: true`. */ onChangeEnd?: (plan: FloorPlan, meta: RoomDrawerChangeMeta) => void; /** Id of the floor being edited. Controlled; pair it with `onFloorChange`. */ floorId?: string; /** Id of the floor to start on. Uncontrolled. Defaults to the lowest. */ defaultFloorId?: string; onFloorChange?: (floorId: string) => void; /** The floor tabs and their add button. Default `true`. */ showFloorBar?: boolean; /** * Trace the storey below behind the one being edited, so walls can be lined up between * floors. Default `true`; the toolbar toggle can still turn it off. */ showUnderlay?: boolean; tool?: RoomDrawerTool; defaultTool?: RoomDrawerTool; onToolChange?: (tool: RoomDrawerTool) => void; /** Restricts and orders the toolbar. Defaults to all six tools. */ tools?: RoomDrawerTool[]; /** * Controlled selection. An array, because the drawer supports selecting several things * at once; a single selection is a one-element array. */ selection?: readonly PlanSelection[]; defaultSelection?: readonly PlanSelection[]; onSelectionChange?: (selection: PlanSelection[]) => void; /** * Allow selecting more than one thing, by Ctrl/Cmd-clicking or by dragging a marquee * across empty canvas with the select tool. Default `true`. */ multiSelect?: boolean; onHoverChange?: (target: PlanSelection | null) => void; view?: PlanView; defaultView?: PlanView; onViewChange?: (view: PlanView) => void; /** Screen pixels per millimetre. */ minZoom?: number; maxZoom?: number; /** Fit the plan into the viewport on mount, and when it first becomes non-empty. */ fitOnMount?: boolean; /** Grid spacing in millimetres (default 100, i.e. 10cm). */ gridSize?: number; showGrid?: boolean; /** Snap to the grid and to existing geometry. Alt inverts it for one gesture. */ snap?: boolean; /** * Latch angle lock on, so drawing is constrained to multiples of `constrainAngle` * without holding Shift. Shift then momentarily RELEASES the lock. Default `false`. */ angleLock?: boolean; /** The angle step in degrees that angle lock snaps to (default 45). */ constrainAngle?: number; /** Draw the construction lines explaining what the pointer snapped to (default `true`). */ showSnapGuides?: boolean; /** Millimetres. Applied to newly drawn walls. */ wallThickness?: number; wallHeight?: number; /** Defaults for newly placed openings, per kind. */ openingDefaults?: Partial>; showMeasurements?: boolean; formatLength?: (lengthMm: number) => string; formatArea?: (areaMm2: number) => string; /** A drawing angle, in degrees from -180 to 180. */ formatAngle?: (degrees: number) => string; /** The zoom readout. `zoom` is screen pixels per millimetre, so `1` reads as 100%. */ formatZoom?: (zoom: number) => string; showToolbar?: boolean; showInspector?: boolean; showHint?: boolean; showHistoryControls?: boolean; /** Download and open buttons in the toolbar. Default `true`. */ showFileControls?: boolean; /** * Name for the downloaded file, with `.json` appended if it is missing. Defaults to the * plan's own `name`, or `floor-plan.json`. */ fileName?: string; /** * Fires after every file the user opens, whether it worked or not. `issues` is empty * only for a file that needed no repair; see `planFromJson`. Without this the drawer * still reports a bad file in its hint line. */ onImport?: (result: PlanParseResult) => void; /** * `'internal'` keeps an undo stack inside the component. `'none'` disables it and * hides the buttons, for a consumer that owns its own history. */ historyMode?: 'internal' | 'none'; historyLimit?: number; onHistoryChange?: (state: { canUndo: boolean; canRedo: boolean; }) => void; /** View and select, but no edits. */ readOnly?: boolean; disabled?: boolean; /** Height of the canvas, any CSS length. */ canvasHeight?: string; getRoomColor?: (room: PlanRoom, area: number) => string | undefined; renderRoomLabel?: (room: PlanRoom | null, context: { area: number; faceId: string; }) => ReactNode; labels?: RoomDrawerLabels; icons?: RoomDrawerIcons; 'aria-label'?: string; /** Imperative handle for driving the editor. */ ref?: Ref; className?: string; style?: CSSProperties; classNames?: SlotClassNames; styles?: SlotStyles; }