@osamash/floor-planner 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/floor-planner.css +1 -1
  2. package/dist/floor-planner.es.js +44841 -38060
  3. package/dist/floor-planner.umd.js +65 -51
  4. package/dist/types/index.d.ts +3 -1
  5. package/dist/types/react-planner/ReactPlanner.d.ts +3 -1
  6. package/dist/types/react-planner/ReactPlannerLoader.d.ts +20 -1
  7. package/dist/types/react-planner/ai-chat/AIChatBubble.d.ts +6 -0
  8. package/dist/types/react-planner/ai-chat/AIChatPanel.d.ts +8 -0
  9. package/dist/types/react-planner/ai-chat/SelectionChips.d.ts +2 -0
  10. package/dist/types/react-planner/ai-chat/ThinkingIndicator.d.ts +5 -0
  11. package/dist/types/react-planner/ai-chat/aiService.d.ts +340 -0
  12. package/dist/types/react-planner/ai-chat/imageUpload.d.ts +6 -0
  13. package/dist/types/react-planner/ai-chat/plannerActions.d.ts +20 -0
  14. package/dist/types/react-planner/ai-chat/streamingClient.d.ts +31 -0
  15. package/dist/types/react-planner/ai-chat/useTypewriter.d.ts +19 -0
  16. package/dist/types/react-planner/catalog/catalog.d.ts +24 -0
  17. package/dist/types/react-planner/catalog/factories/AreaFactory.d.ts +1 -24
  18. package/dist/types/react-planner/catalog/factories/ItemFactory.d.ts +2 -0
  19. package/dist/types/react-planner/catalog/factories/WallFactory.d.ts +1 -26
  20. package/dist/types/react-planner/catalog/factories/area-factory-3d.d.ts +2 -2
  21. package/dist/types/react-planner/catalog/factories/area-patterns.d.ts +42 -0
  22. package/dist/types/react-planner/catalog/factories/room-types.d.ts +22 -0
  23. package/dist/types/react-planner/catalog/factories/wall-factory-3d.d.ts +1 -1
  24. package/dist/types/react-planner/catalog/unknown-element.d.ts +10 -0
  25. package/dist/types/react-planner/class/area.d.ts +28 -1
  26. package/dist/types/react-planner/class/hole.d.ts +13 -0
  27. package/dist/types/react-planner/class/index.d.ts +3 -1
  28. package/dist/types/react-planner/class/item.d.ts +22 -0
  29. package/dist/types/react-planner/class/line.d.ts +12 -3
  30. package/dist/types/react-planner/class/room.d.ts +18 -0
  31. package/dist/types/react-planner/class/surface.d.ts +34 -0
  32. package/dist/types/react-planner/class/vertex.d.ts +6 -1
  33. package/dist/types/react-planner/components/ErrorBoundary.d.ts +19 -0
  34. package/dist/types/react-planner/components/SearchInput.d.ts +2 -1
  35. package/dist/types/react-planner/constants.d.ts +3 -0
  36. package/dist/types/react-planner/footer-bar/Footerbar.d.ts +1 -1
  37. package/dist/types/react-planner/hooks/usePlanThumbnail.d.ts +13 -0
  38. package/dist/types/react-planner/models.d.ts +19 -0
  39. package/dist/types/react-planner/plugins/autosave.d.ts +1 -1
  40. package/dist/types/react-planner/plugins/keyboard.d.ts +1 -1
  41. package/dist/types/react-planner/sidebar/Panel.d.ts +2 -1
  42. package/dist/types/react-planner/sidebar/panel-element-editor/AreaAttributesEditor.d.ts +8 -0
  43. package/dist/types/react-planner/sidebar/panel-element-editor/AttributesEditor.d.ts +4 -1
  44. package/dist/types/react-planner/sidebar/panel-element-editor/HoleAttributesEditor.d.ts +3 -4
  45. package/dist/types/react-planner/sidebar/panel-element-editor/ItemAttributesEditor.d.ts +3 -4
  46. package/dist/types/react-planner/sidebar/panel-element-editor/SimilarElements.d.ts +15 -0
  47. package/dist/types/react-planner/sidebar/panel-element-editor/WallAttributesEditor.d.ts +8 -0
  48. package/dist/types/react-planner/sidebar/panel-element-editor/editor-ui.d.ts +93 -0
  49. package/dist/types/react-planner/sidebar/panel-element-editor/paint-palette.d.ts +6 -0
  50. package/dist/types/react-planner/toolbar/CatalogPicker.d.ts +8 -0
  51. package/dist/types/react-planner/toolbar/NumberStepper.d.ts +11 -0
  52. package/dist/types/react-planner/toolbar/ProjectMiniMap.d.ts +11 -0
  53. package/dist/types/react-planner/toolbar/ProjectPanel.d.ts +2 -0
  54. package/dist/types/react-planner/toolbar/ToolbarRail.d.ts +15 -0
  55. package/dist/types/react-planner/toolbar/ToolbarSubPanel.d.ts +23 -0
  56. package/dist/types/react-planner/toolbar/ZoomControls.d.ts +5 -0
  57. package/dist/types/react-planner/utils/angle.d.ts +9 -0
  58. package/dist/types/react-planner/utils/geometry.d.ts +15 -0
  59. package/dist/types/react-planner/utils/image-trim.d.ts +25 -0
  60. package/dist/types/react-planner/utils/index.d.ts +3 -1
  61. package/dist/types/react-planner/utils/item-footprint.d.ts +23 -0
  62. package/dist/types/react-planner/utils/item-snap.d.ts +56 -0
  63. package/dist/types/react-planner/utils/item-transform.d.ts +9 -0
  64. package/dist/types/react-planner/utils/line-family.d.ts +29 -0
  65. package/dist/types/react-planner/utils/name-generator.d.ts +1 -1
  66. package/dist/types/react-planner/utils/screenshot.d.ts +22 -1
  67. package/dist/types/react-planner/utils/three-cache.d.ts +17 -0
  68. package/dist/types/react-planner/utils/three-materials.d.ts +33 -0
  69. package/dist/types/react-planner/utils/threeCSG.es6.d.ts +1 -1
  70. package/dist/types/react-planner/utils/wall-miter.d.ts +53 -0
  71. package/dist/types/react-planner/utils/zoom.d.ts +2 -2
  72. package/dist/types/react-planner/viewer2d/Area.d.ts +7 -1
  73. package/dist/types/react-planner/viewer2d/Grids.d.ts +1 -1
  74. package/dist/types/react-planner/viewer2d/Ruler.d.ts +6 -1
  75. package/dist/types/react-planner/viewer2d/SelectionToolbar.d.ts +12 -0
  76. package/dist/types/react-planner/viewer2d/WallHoverHandles.d.ts +13 -0
  77. package/dist/types/react-planner/viewer3d/selection-gizmo.d.ts +57 -0
  78. package/dist/types/react-planner/viewer3d/viewer3d.d.ts +71 -0
  79. package/dist/types/shared-style.d.ts +6 -0
  80. package/dist/types/store/ReactPlannerStore.d.ts +102 -1
  81. package/package.json +1 -1
  82. package/dist/types/react-planner/components/PropertyString.d.ts +0 -10
  83. package/dist/types/react-planner/hooks/useScreenshotBeforeUnload.d.ts +0 -4
  84. package/dist/types/react-planner/toolbar/Popup.d.ts +0 -11
  85. package/dist/types/react-planner/viewer2d/GridHorizontalStreak.d.ts +0 -7
  86. package/dist/types/react-planner/viewer2d/GridVerticalStreak.d.ts +0 -7
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from "react";
2
+ import type Catalog from "../catalog/catalog";
3
+ export interface SubPanelOption {
4
+ key: string;
5
+ label: string;
6
+ icon: ReactNode;
7
+ active?: boolean;
8
+ disabled?: boolean;
9
+ hasChildren?: boolean;
10
+ onClick?: () => void;
11
+ }
12
+ interface ToolbarSubPanelProps {
13
+ title: string;
14
+ options?: SubPanelOption[];
15
+ cta?: string;
16
+ emptyState?: ReactNode;
17
+ catalog?: Catalog;
18
+ catalogPage?: string | null;
19
+ onBack?: () => void;
20
+ headerExtras?: ReactNode;
21
+ }
22
+ declare const ToolbarSubPanel: ({ title, options, cta, emptyState, catalog, catalogPage, onBack, headerExtras, }: ToolbarSubPanelProps) => import("react/jsx-runtime").JSX.Element;
23
+ export default ToolbarSubPanel;
@@ -0,0 +1,5 @@
1
+ interface ZoomControlsProps {
2
+ subPanelOpen?: boolean;
3
+ }
4
+ declare const ZoomControls: ({ subPanelOpen }: ZoomControlsProps) => import("react/jsx-runtime").JSX.Element;
5
+ export default ZoomControls;
@@ -0,0 +1,9 @@
1
+ export declare const RIGHT_ANGLE_TOLERANCE = 5;
2
+ /** Normalize to (-180, 180], the range the rest of the planner uses. */
3
+ export declare const normalizeAngle: (degrees: number) => number;
4
+ /**
5
+ * Pull an angle onto the nearest quarter turn if it is already close to one.
6
+ * Shared so that rotating an item feels the same however it is done — the 2D
7
+ * handle, the 3D gizmo, or anything added later.
8
+ */
9
+ export declare const snapToRightAngle: (degrees: number) => number;
@@ -141,6 +141,21 @@ export declare function extendLine(x1: number, y1: number, x2: number, y2: numbe
141
141
  y: number;
142
142
  };
143
143
  export declare function roundVertex(vertex: Map<string, number>, precision?: number): Map<string, number>;
144
+ /** Is (pointX, pointY) strictly inside `polygon` (a flat [x0,y0,x1,y1,…] ring)?
145
+ *
146
+ * A point *on* the boundary counts as outside. Callers depend on that: the
147
+ * hole detection in `Layer.detectAndUpdateAreas` asks whether one area sits
148
+ * inside another by testing one of its vertices, and neighbouring rooms
149
+ * always share corners — a shared corner has to answer "no".
150
+ *
151
+ * This used to be PolyK's `ContainsPoint`, whose "hit vertex" correction
152
+ * decremented the crossing count for a polygon vertex level with the query
153
+ * point *even when that vertex sat to the left of it and had therefore never
154
+ * been counted*, leaving the count at -1 and reporting a point far outside as
155
+ * inside. Floor plans are axis-aligned and grid-snapped, so points level with
156
+ * a vertex are the common case, not a corner case — every room in a grid of
157
+ * rooms is level with its neighbours' corners. Rewritten as a plain
158
+ * crossing-number test with an explicit boundary check up front. */
144
159
  export declare function ContainsPoint(polygon: number[], pointX: number, pointY: number): boolean;
145
160
  export declare function cosWithThreshold(alpha: number, threshold: number): number;
146
161
  export declare function sinWithThreshold(alpha: number, threshold: number): number;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Where an image's visible pixels actually are inside the rectangle it is
3
+ * drawn in.
4
+ *
5
+ * Catalog artwork is usually a PNG with transparent margin around the object,
6
+ * and `<image>` letterboxes it on top of that when the file's proportions
7
+ * don't match the rectangle. Both leave empty space that nothing in the DOM
8
+ * reports — `getBBox()` returns the rectangle as declared — so an item snapped
9
+ * flush by its bounding box still looks like it is floating off the wall.
10
+ *
11
+ * The scan is done once per URL and cached for the life of the page.
12
+ */
13
+ export interface Rect {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+ /**
20
+ * The rectangle an image's visible pixels occupy, in the same coordinates
21
+ * `rect` is given in. Null when it can't be worked out — a cross-origin asset
22
+ * without CORS headers, a `slice` fit, an image that failed to load — in which
23
+ * case the caller should stay with the declared rectangle.
24
+ */
25
+ export declare function visibleImageRect(src: string, rect: Rect, preserveAspectRatio: string): Promise<Rect | null>;
@@ -5,9 +5,10 @@ import * as SnapUtils from "./snap";
5
5
  import * as SnapSceneUtils from "./snap-scene";
6
6
  import * as history from "./history";
7
7
  import * as ObjectUtils from "./objects-utils";
8
+ import * as LineFamilyUtils from "./line-family";
8
9
  import IDBroker from "./id-broker";
9
10
  import NameGenerator from "./name-generator";
10
- export { GeometryUtils, GraphInnerCycles, MathUtils, SnapUtils, SnapSceneUtils, history, IDBroker, NameGenerator, ObjectUtils, };
11
+ export { GeometryUtils, GraphInnerCycles, MathUtils, SnapUtils, SnapSceneUtils, history, IDBroker, NameGenerator, ObjectUtils, LineFamilyUtils, };
11
12
  declare const _default: {
12
13
  GeometryUtils: typeof GeometryUtils;
13
14
  GraphInnerCycles: typeof GraphInnerCycles;
@@ -18,5 +19,6 @@ declare const _default: {
18
19
  IDBroker: typeof IDBroker;
19
20
  NameGenerator: typeof NameGenerator;
20
21
  ObjectUtils: typeof ObjectUtils;
22
+ LineFamilyUtils: typeof LineFamilyUtils;
21
23
  };
22
24
  export default _default;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What each item actually measures on the plan.
3
+ *
4
+ * Catalog elements draw themselves at whatever size they like and the `Item`
5
+ * record carries no width or height, so an item's footprint is only knowable
6
+ * from what it rendered. `viewer2d/Item` measures that anyway — for handle and
7
+ * label placement — and files the result here so the rest of the planner can
8
+ * reason about item geometry without touching the DOM.
9
+ *
10
+ * The box is in the item's own frame, after per-instance scaling and
11
+ * mirroring, before rotation: `x`/`y` are its corner relative to the item's
12
+ * origin, which is not necessarily its centre.
13
+ */
14
+ export interface ItemFootprint {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+ export declare function rememberItemFootprint(itemID: string, box: ItemFootprint): void;
21
+ export declare function forgetItemFootprint(itemID: string): void;
22
+ /** Null for an item that hasn't been drawn in 2D yet. */
23
+ export declare function getItemFootprint(itemID: string): ItemFootprint | null;
@@ -0,0 +1,56 @@
1
+ import { type ItemFootprint } from "./item-footprint";
2
+ export declare const ITEM_WALL_SNAP_DISTANCE = 25;
3
+ export declare const ITEM_ITEM_SNAP_DISTANCE = 25;
4
+ /** The face an item snapped against, for the viewer to draw a guide along. */
5
+ export interface ItemSnapGuide {
6
+ x1: number;
7
+ y1: number;
8
+ x2: number;
9
+ y2: number;
10
+ }
11
+ export interface ItemWallSnap {
12
+ x: number;
13
+ y: number;
14
+ rotation: number;
15
+ lineID: string;
16
+ guides: ItemSnapGuide[];
17
+ /**
18
+ * The world direction the item is still free to slide along after the snap —
19
+ * the wall's own direction, since going flush only fixes the axis across it.
20
+ * Null once a corner snap has fixed that one too. What is left here is what
21
+ * a neighbouring item is allowed to move the item on.
22
+ */
23
+ free: {
24
+ x: number;
25
+ y: number;
26
+ } | null;
27
+ }
28
+ /**
29
+ * Where the item would sit if it snapped to a wall, or null if none is close
30
+ * enough.
31
+ *
32
+ * The item goes flush against the nearest wall face and squares up with it,
33
+ * then — if it is also near the wall at the other end — slides along until it
34
+ * sits in the corner. It stays free on any axis it hasn't snapped on, so the
35
+ * pointer still leads there.
36
+ */
37
+ export declare function snapItemToWalls(layer: any, catalog: any, footprint: ItemFootprint | null, x: number, y: number, rotation: number): ItemWallSnap | null;
38
+ /** Where the item lands, or null if nothing was close enough to snap to. */
39
+ export interface ItemSnap {
40
+ x: number;
41
+ y: number;
42
+ rotation: number;
43
+ guides: ItemSnapGuide[];
44
+ }
45
+ /**
46
+ * Everything a dragged item snaps to: the walls first, then the items already
47
+ * placed.
48
+ *
49
+ * The two compose rather than compete. A wall snap fixes the axis across the
50
+ * wall and squares the item up; the items on the layer then get to decide
51
+ * where along that wall it sits — so a nightstand pushed towards a corner goes
52
+ * flush against the wall *and* flush against the bed in one gesture. Once a
53
+ * corner snap has fixed the second axis too there is nothing left to offer and
54
+ * the walls have it. With no wall in range, both axes are the items' to claim.
55
+ */
56
+ export declare function snapItem(layer: any, catalog: any, itemID: string, footprint: ItemFootprint | null, x: number, y: number, rotation: number): ItemSnap | null;
@@ -0,0 +1,9 @@
1
+ export declare const ITEM_MIN_SCALE = 0.2;
2
+ export declare const ITEM_MAX_SCALE = 8;
3
+ export interface ItemTransform {
4
+ scale: number;
5
+ flipHorizontal: boolean;
6
+ flipVertical: boolean;
7
+ }
8
+ export declare function getItemTransform(item: any): ItemTransform;
9
+ export declare function clampItemScale(scale: number): number;
@@ -0,0 +1,29 @@
1
+ /** Surfaces (patios, lawns, decking…) are drawn with the very same `lines` +
2
+ * `vertices` machinery as walls — but they are not structure. A patio laid
3
+ * over a room must not weld itself onto the walls it crosses, must not split
4
+ * them, and must not turn the room's floor into part of the patio.
5
+ *
6
+ * So every graph operation works inside one *family* at a time: two lines
7
+ * only share a vertex, split each other, merge when colinear, or take part in
8
+ * the same area cycle when their families match. Snapping is deliberately
9
+ * exempt — a surface should still be able to land exactly on a wall corner,
10
+ * it just must not become attached to it.
11
+ */
12
+ export type LineFamily = "surface" | "structure";
13
+ export declare const SURFACE_LINE_TYPE = "surface";
14
+ export declare function lineFamily(type?: string | null): LineFamily;
15
+ export declare function lineFamilyOf(line: any): LineFamily;
16
+ /** Whether a vertex may be shared with a line of `family`.
17
+ *
18
+ * A vertex nothing is hanging off yet takes anything. Otherwise it belongs to
19
+ * the families of the lines already attached — which is normally exactly one,
20
+ * but plans saved before families existed can have vertices where a wall and
21
+ * a surface already meet. Those stay usable by both rather than being split
22
+ * apart underneath a scene that is already drawn.
23
+ */
24
+ export declare function vertexAcceptsFamily(vertex: any, layer: any, family: LineFamily): boolean;
25
+ /** The family of an area, read off the lines meeting at its corners. Mirrors
26
+ * `getAreaKind`'s rule: an area is a surface only when nothing structural
27
+ * borders it. */
28
+ export declare function areaFamily(area: any, layer: any): LineFamily;
29
+ export declare const LINE_FAMILIES: LineFamily[];
@@ -1,3 +1,3 @@
1
1
  export default class NameGenerator {
2
- static generateName(_: any, type: string): string;
2
+ static generateName(_: any, type: unknown): string;
3
3
  }
@@ -1 +1,22 @@
1
- export declare const getSVGScreenshot: () => Promise<string>;
1
+ export interface ScreenshotOptions {
2
+ /** Cap output width in px (height scales to match). Omit for 1:1 with the SVG. */
3
+ maxWidth?: number;
4
+ /** Output MIME type. Default "image/png". */
5
+ type?: string;
6
+ /** Quality 0-1, for lossy types only. Ignored for PNG. */
7
+ quality?: number;
8
+ /**
9
+ * Reject if the largest SVG on the page is narrower than this. The viewer2d
10
+ * SVG is unmounted in 3D mode, and without this the "largest SVG" would be a
11
+ * ~28px toolbar icon, silently producing a thumbnail of an icon.
12
+ */
13
+ minWidth?: number;
14
+ /** Overall budget in ms. Default 15000. */
15
+ timeout?: number;
16
+ }
17
+ /**
18
+ * Rasterize the plan viewer's SVG to a data URI. Rejects (rather than hanging)
19
+ * when there's nothing capturable on screen, an asset can't be decoded, or the
20
+ * whole thing overruns `timeout`.
21
+ */
22
+ export declare const getSVGScreenshot: (options?: ScreenshotOptions) => Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { Texture } from "three";
2
+ import type { ColorSpace } from "three";
3
+ /**
4
+ * One decoded image per URL, handed out as clones.
5
+ *
6
+ * Callers set their own `repeat`/`wrap` — a wall tiles differently from a
7
+ * floor — and those live on the Texture, so a single shared object won't do.
8
+ * A clone carries its own settings but shares `source` with the base, and the
9
+ * renderer keys GPU uploads on `source` (refcounted, so disposing a clone is
10
+ * safe): the bytes are fetched, decoded and uploaded once no matter how many
11
+ * walls ask for them.
12
+ *
13
+ * `colorSpace` defaults to sRGB because most callers want an albedo map, and
14
+ * one left in the renderer's linear working space comes out visibly washed
15
+ * out. Data maps — normals, roughness — must pass `NoColorSpace`.
16
+ */
17
+ export declare function loadTexture(uri: string, colorSpace?: ColorSpace): Texture;
@@ -0,0 +1,33 @@
1
+ import { Object3D, Texture } from "three";
2
+ /**
3
+ * Mark a texture as holding colour rather than data.
4
+ *
5
+ * Image files store colour sRGB-encoded, but lighting has to happen in linear
6
+ * space — the renderer only knows to convert if the texture says so. Left at
7
+ * the default, an albedo map renders washed out and the wrong brightness.
8
+ * Normal/roughness/metalness maps are numbers, not colour, and must stay
9
+ * linear: those get `NoColorSpace`.
10
+ */
11
+ export declare function asColorMap<T extends Texture>(texture: T): T;
12
+ export declare function asDataMap<T extends Texture>(texture: T): T;
13
+ /**
14
+ * Bring a freshly imported model in line with how the viewer lights the scene.
15
+ *
16
+ * Model files describe their materials in whatever the exporting tool used,
17
+ * and the loaders reproduce that faithfully — which is why an .obj arrives
18
+ * Phong and a .glb arrives Standard. Anything already physically based is left
19
+ * alone beyond its colour spaces; the rest is converted so one lighting rig
20
+ * can light all of it consistently.
21
+ */
22
+ export declare function normalizeImportedMaterials<T extends Object3D>(object: T): T;
23
+ interface ShadowOptions {
24
+ cast?: boolean;
25
+ receive?: boolean;
26
+ }
27
+ /**
28
+ * Contact shadows are most of what makes furniture look like it's standing on
29
+ * the floor rather than pasted over it. Floors only receive — a flat surface
30
+ * casting onto itself is all acne and no shadow.
31
+ */
32
+ export declare function enableShadows<T extends Object3D>(object: T, { cast, receive }?: ShadowOptions): T;
33
+ export {};
@@ -21,7 +21,7 @@ declare class Polygon {
21
21
  clone(): Polygon;
22
22
  flip(): this;
23
23
  classifyVertex(vertex: THREE.Vector3Like): 0 | 1 | 2;
24
- classifySide(polygon: Polygon): 0 | 1 | 2 | 3;
24
+ classifySide(polygon: Polygon): 0 | 1 | 3 | 2;
25
25
  splitPolygon(polygon: Polygon, coplanar_front: any, coplanar_back: any, front: any, back: any): void;
26
26
  }
27
27
  declare class Node {
@@ -0,0 +1,53 @@
1
+ export declare const MITER_LIMIT = 4;
2
+ export interface MiterShifts {
3
+ /** Shift along the wall axis for the +y (local top) side. Positive cuts the
4
+ * corner back into the wall, negative extends it past the vertex. */
5
+ topShift: number;
6
+ /** Same, for the −y (local bottom) side. */
7
+ botShift: number;
8
+ }
9
+ /**
10
+ * Compute miter shifts at one end of a wall.
11
+ *
12
+ * - axisDirAway: unit vector from `vertex` toward the wall's OTHER end.
13
+ * - perpTop: the wall's local +y direction in world coords (fixed for both
14
+ * ends of the wall — 90° CCW from the wall's start→end direction).
15
+ *
16
+ * Independent shifts on the two perpendicular sides let the wall's end become
17
+ * a diagonal miter cut instead of a straight rectangle end.
18
+ */
19
+ export declare function miterShifts(currentLineID: string, vertex: any, axisDirAway: {
20
+ x: number;
21
+ y: number;
22
+ }, perpTop: {
23
+ x: number;
24
+ y: number;
25
+ }, layer: any, halfThickness: number, miterLimit?: number): MiterShifts;
26
+ export interface WallGeometry {
27
+ /** Endpoint the local frame starts from (the smaller-x one). */
28
+ vStart: any;
29
+ vEnd: any;
30
+ /** Centerline length, vertex to vertex. */
31
+ length: number;
32
+ thickness: number;
33
+ halfThickness: number;
34
+ /** Local-frame x of the four corners. The wall is the quadrilateral
35
+ * (topStart, +h) (topEnd, +h) (botEnd, −h) (botStart, −h). */
36
+ topStart: number;
37
+ topEnd: number;
38
+ botStart: number;
39
+ botEnd: number;
40
+ }
41
+ /**
42
+ * The wall as it is actually drawn: a quadrilateral whose ends are diagonal
43
+ * miter cuts. Everything is in the wall's local frame — x runs along the wall
44
+ * from the smaller-x endpoint, y is perpendicular with +y = 90° CCW from the
45
+ * wall direction. `viewer2d/Line` establishes exactly this frame, which is why
46
+ * the start/end ordering below has to match it.
47
+ *
48
+ * The two face lengths a floor plan quotes as "inside" and "outside" are
49
+ * `topEnd - topStart` and `botEnd - botStart`: a corner cuts one face back by
50
+ * the miter length and extends the other past the vertex by the same amount,
51
+ * so they straddle the centerline length.
52
+ */
53
+ export declare function wallGeometry(element: any, layer: any): WallGeometry | null;
@@ -1,5 +1,5 @@
1
- export declare const MIN_ZOOM = 0.6;
1
+ export declare const MIN_ZOOM = 0.25;
2
2
  export declare const MAX_ZOOM = 5;
3
- export declare const SVG_MIN_ZOOM = 0.6;
3
+ export declare const SVG_MIN_ZOOM = 0.25;
4
4
  export declare const SVG_MAX_ZOOM = 5;
5
5
  export declare function zoomToPercentage(zoom: number): number;
@@ -3,6 +3,12 @@ interface AreaProps {
3
3
  area: any;
4
4
  layer: any;
5
5
  catalog: Catalog;
6
+ /** Which of the two passes this is. Fills and labels are drawn separately
7
+ * so every label lands above every fill — a surface laid over a room is
8
+ * meant to cover the room's floor, but not to swallow its name. Both
9
+ * passes iterate the same ordered list in
10
+ * [Layer.tsx](src/react-planner/viewer2d/Layer.tsx). */
11
+ part?: "fill" | "label";
6
12
  }
7
- export default function Area({ layer, area, catalog }: AreaProps): import("react/jsx-runtime").JSX.Element;
13
+ export default function Area({ layer, area, catalog, part, }: AreaProps): import("react/jsx-runtime").JSX.Element | null;
8
14
  export {};
@@ -1,5 +1,5 @@
1
1
  interface GridsProps {
2
2
  scene: any;
3
3
  }
4
- export default function Grids({ scene }: GridsProps): import("react/jsx-runtime").JSX.Element;
4
+ export default function Grids({ scene }: GridsProps): import("react/jsx-runtime").JSX.Element | null;
5
5
  export {};
@@ -3,7 +3,12 @@ interface RulerProps {
3
3
  length: number;
4
4
  unit: string;
5
5
  transform: string;
6
+ /** Which side of the rule the measurement sits on. "below" is for a ruler
7
+ * drawn under the thing it measures, so the label falls away from it
8
+ * instead of landing on top of it. */
9
+ labelPlacement?: "above" | "below";
6
10
  }
7
- declare function Ruler({ length, unit, transform }: RulerProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare const formatDistance: (length: number, unit: string) => string;
12
+ declare function Ruler({ length, unit, transform, labelPlacement, }: RulerProps): import("react/jsx-runtime").JSX.Element;
8
13
  declare const _default: React.MemoExoticComponent<typeof Ruler>;
9
14
  export default _default;
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ interface SelectionToolbarProps {
3
+ containerRef: React.RefObject<HTMLDivElement | null>;
4
+ }
5
+ /**
6
+ * Floating action bar for the current selection. It is plain HTML positioned
7
+ * over the SVG — the element's rendered bounding box is read straight from the
8
+ * DOM, which keeps it correct for any catalog element whatever the zoom, pan
9
+ * or rotation.
10
+ */
11
+ declare const SelectionToolbar: ({ containerRef }: SelectionToolbarProps) => import("react/jsx-runtime").JSX.Element | null;
12
+ export default SelectionToolbar;
@@ -0,0 +1,13 @@
1
+ interface WallHoverHandlesProps {
2
+ layer: any;
3
+ }
4
+ /**
5
+ * Endpoint handles for the wall currently under the pointer — drag one to
6
+ * lengthen or shorten the wall, no selection needed.
7
+ *
8
+ * They render as the layer's last child rather than inside the wall's own
9
+ * group: SVG has no z-index, only document order, so anything drawn from
10
+ * inside the wall would be painted over by walls and items that come later.
11
+ */
12
+ export default function WallHoverHandles({ layer }: WallHoverHandlesProps): import("react/jsx-runtime").JSX.Element | null;
13
+ export {};
@@ -0,0 +1,57 @@
1
+ import * as THREE from "three";
2
+ export type GizmoHandle = "rotate" | "lift";
3
+ export interface GizmoTarget {
4
+ /** Centre of the item's footprint, in world space. */
5
+ centre: THREE.Vector3;
6
+ /** Floor the item stands on, in world space. */
7
+ floorY: number;
8
+ /** Where the lift handle sits — around the middle of the item. */
9
+ handleY: number;
10
+ /** Half the footprint's diagonal. */
11
+ radius: number;
12
+ /** The item's heading, so the ring's arrow reads as its front. */
13
+ rotationY: number;
14
+ /** Underside of the item, for deciding whether it is off the floor at all. */
15
+ bottomY: number;
16
+ /** Whether the lift handle is being dragged right now. */
17
+ lifting: boolean;
18
+ }
19
+ export default class SelectionGizmo {
20
+ readonly group: THREE.Group<THREE.Object3DEventMap>;
21
+ /** Turns with the item: the ring and the arrow marking its front. */
22
+ private readonly base;
23
+ private readonly ring;
24
+ private readonly rotateGrab;
25
+ private readonly rotateArrow;
26
+ /** Always faces the camera, always the same size on screen. */
27
+ private readonly lift;
28
+ private readonly liftGrab;
29
+ /** Dashed line from the handle down to the floor, when the item is lifted. */
30
+ private readonly plumb;
31
+ private readonly plumbTexture;
32
+ private readonly materials;
33
+ private readonly geometries;
34
+ private readonly textures;
35
+ constructor();
36
+ private keep;
37
+ private keepTexture;
38
+ /**
39
+ * Place the gizmo around `target`, or hide it when there is nothing
40
+ * selected. `worldPerPixel` converts a screen size at a given distance into
41
+ * world units, which is what keeps the lift handle the same size however far
42
+ * away the camera is.
43
+ */
44
+ update(target: GizmoTarget | null, camera: THREE.Camera, worldPerPixel: (distance: number) => number): void;
45
+ /**
46
+ * The line that says how far off the floor the item is. Drawn from the
47
+ * handle straight down to where the ring is, for the length of the lift
48
+ * drag only — it answers "how high am I putting this", which is a question
49
+ * that stops being asked the moment the handle is let go. It also needs a
50
+ * gap to describe: an item still sitting on the floor doesn't get one.
51
+ */
52
+ private updatePlumb;
53
+ /** Which handle, if any, a ray hits. The lift handle wins ties: it sits on
54
+ * top of the ring and is the smaller target. */
55
+ pick(raycaster: THREE.Raycaster): GizmoHandle | null;
56
+ dispose(): void;
57
+ }
@@ -9,6 +9,7 @@ interface Viewer3DProps {
9
9
  [key: string]: any;
10
10
  };
11
11
  catalog: Catalog;
12
+ showGrid: boolean;
12
13
  }
13
14
  export default class Scene3DViewer extends React.Component<Viewer3DProps> {
14
15
  private canvasWrapperRef;
@@ -23,8 +24,78 @@ export default class Scene3DViewer extends React.Component<Viewer3DProps> {
23
24
  private lastMousePosition;
24
25
  private mouseDownEvent;
25
26
  private mouseUpEvent;
27
+ private lastBoundingBox;
28
+ private userMovedCamera;
29
+ private sun;
30
+ private shadowsDirtyUntil;
31
+ private gizmo;
32
+ private gizmoDrag;
33
+ private gizmoMoveEvent;
34
+ private gizmoUpEvent;
35
+ private readonly scratchBox;
36
+ private readonly scratchVector;
37
+ private readonly scratchSize;
38
+ private gizmoRadiusFor;
39
+ private gizmoRadius;
26
40
  constructor(props: Viewer3DProps);
27
41
  componentDidMount(): void;
42
+ /** World units one screen pixel covers at `distance` from the camera. */
43
+ private worldPerPixel;
44
+ /** The item the gizmo should be wrapped around, if there is one. */
45
+ private selectedItemTarget;
46
+ private beginGizmoDrag;
47
+ /**
48
+ * Move the item itself rather than the store: a drag is one gesture and
49
+ * should be one entry in the history, and an altitude change rebuilds the
50
+ * item's whole model — neither is something to do per mouse move. The store
51
+ * hears about it once, when the drag ends.
52
+ */
53
+ private updateGizmoDrag;
54
+ private endGizmoDrag;
55
+ /**
56
+ * Aim the sun at the plan and size its shadow frustum to cover it.
57
+ *
58
+ * A directional light shadows through an orthographic camera, and anything
59
+ * outside that box simply casts nothing — so the box has to follow the plan
60
+ * as it grows. Sizing it to the sphere around the bounding box means the
61
+ * coverage holds from any angle.
62
+ */
63
+ private positionSun;
64
+ /**
65
+ * Re-render the shadow map.
66
+ *
67
+ * Not just on the next frame: meshes and their textures arrive over the
68
+ * following moments, so keep it refreshing for a beat rather than baking
69
+ * shadows for a scene that is still half loaded.
70
+ */
71
+ private invalidateShadows;
72
+ /**
73
+ * Bound the dolly range to the size of what's being looked at, so you can
74
+ * neither fly halfway to the sun nor end up inside a wall.
75
+ *
76
+ * Re-derived when the extent changes, because the box measured at mount is
77
+ * the grid's — the size of the whole scene rather than of the plan drawn in
78
+ * it — and limits taken from that stop the camera well short of a small
79
+ * plan.
80
+ */
81
+ private applyZoomLimits;
82
+ /**
83
+ * Dolly toward (factor > 1) or away from (factor < 1) the orbit target.
84
+ * OrbitControls' own min/maxDistance still bound the result, so the buttons
85
+ * can't push the camera anywhere the scroll wheel couldn't.
86
+ */
87
+ zoomBy(factor: number): void;
88
+ /**
89
+ * Frame the whole plan: keep looking from the current direction, but re-aim
90
+ * at the plan's center and back off far enough for it to fit.
91
+ */
92
+ fitToScene(): void;
93
+ /**
94
+ * Re-aim at the plan's center and back off far enough to fit it in frame.
95
+ * `direction` is where the camera sits relative to that center; omitted, it
96
+ * keeps looking from wherever it already is.
97
+ */
98
+ private frameScene;
28
99
  componentWillUnmount(): void;
29
100
  componentDidUpdate(prevProps: Viewer3DProps): void;
30
101
  render(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
@@ -1,6 +1,9 @@
1
1
  export declare const COLORS: {
2
2
  white: string;
3
3
  black: string;
4
+ wall: string;
5
+ threeBG: string;
6
+ canvasBG: string;
4
7
  };
5
8
  export declare const MATERIAL_COLORS: {
6
9
  500: {
@@ -53,6 +56,9 @@ declare const SharedStyle: {
53
56
  COLORS: {
54
57
  white: string;
55
58
  black: string;
59
+ wall: string;
60
+ threeBG: string;
61
+ canvasBG: string;
56
62
  };
57
63
  MATERIAL_COLORS: {
58
64
  500: {