@pascal-app/core 0.1.12 → 0.2.0

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 (85) hide show
  1. package/dist/events/bus.d.ts +22 -2
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/scene-registry.d.ts +6 -1
  4. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  5. package/dist/hooks/scene-registry/scene-registry.js +11 -1
  6. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +17 -12
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.js +135 -38
  9. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +1 -1
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.js +16 -8
  12. package/dist/hooks/spatial-grid/spatial-grid.d.ts +3 -3
  13. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
  14. package/dist/hooks/spatial-grid/spatial-grid.js +2 -2
  15. package/dist/hooks/spatial-grid/use-spatial-query.d.ts +5 -0
  16. package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -1
  17. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +6 -3
  18. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -1
  19. package/dist/hooks/spatial-grid/wall-spatial-grid.js +35 -10
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -6
  23. package/dist/lib/space-detection.d.ts.map +1 -1
  24. package/dist/lib/space-detection.js +1 -1
  25. package/dist/schema/collections.d.ts +11 -0
  26. package/dist/schema/collections.d.ts.map +1 -0
  27. package/dist/schema/collections.js +2 -0
  28. package/dist/schema/index.d.ts +11 -7
  29. package/dist/schema/index.d.ts.map +1 -1
  30. package/dist/schema/index.js +11 -6
  31. package/dist/schema/nodes/ceiling.d.ts +1 -0
  32. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  33. package/dist/schema/nodes/ceiling.js +2 -0
  34. package/dist/schema/nodes/door.d.ts +78 -0
  35. package/dist/schema/nodes/door.d.ts.map +1 -0
  36. package/dist/schema/nodes/door.js +67 -0
  37. package/dist/schema/nodes/item.d.ts +245 -2
  38. package/dist/schema/nodes/item.d.ts.map +1 -1
  39. package/dist/schema/nodes/item.js +72 -0
  40. package/dist/schema/nodes/level.d.ts.map +1 -1
  41. package/dist/schema/nodes/level.js +11 -1
  42. package/dist/schema/nodes/roof-segment.d.ts +51 -0
  43. package/dist/schema/nodes/roof-segment.d.ts.map +1 -0
  44. package/dist/schema/nodes/roof-segment.js +36 -0
  45. package/dist/schema/nodes/roof.d.ts +1 -4
  46. package/dist/schema/nodes/roof.d.ts.map +1 -1
  47. package/dist/schema/nodes/roof.js +9 -16
  48. package/dist/schema/nodes/site.d.ts +52 -1
  49. package/dist/schema/nodes/site.d.ts.map +1 -1
  50. package/dist/schema/nodes/slab.d.ts +1 -0
  51. package/dist/schema/nodes/slab.d.ts.map +1 -1
  52. package/dist/schema/nodes/slab.js +1 -0
  53. package/dist/schema/nodes/window.d.ts +40 -0
  54. package/dist/schema/nodes/window.d.ts.map +1 -0
  55. package/dist/schema/nodes/window.js +38 -0
  56. package/dist/schema/types.d.ts +241 -6
  57. package/dist/schema/types.d.ts.map +1 -1
  58. package/dist/schema/types.js +6 -0
  59. package/dist/store/actions/node-actions.d.ts.map +1 -1
  60. package/dist/store/actions/node-actions.js +23 -4
  61. package/dist/store/use-interactive.d.ts +18 -0
  62. package/dist/store/use-interactive.d.ts.map +1 -0
  63. package/dist/store/use-interactive.js +50 -0
  64. package/dist/store/use-scene.d.ts +10 -1
  65. package/dist/store/use-scene.d.ts.map +1 -1
  66. package/dist/store/use-scene.js +180 -57
  67. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  68. package/dist/systems/ceiling/ceiling-system.js +20 -0
  69. package/dist/systems/door/door-system.d.ts +2 -0
  70. package/dist/systems/door/door-system.d.ts.map +1 -0
  71. package/dist/systems/door/door-system.js +211 -0
  72. package/dist/systems/item/item-system.d.ts.map +1 -1
  73. package/dist/systems/item/item-system.js +10 -5
  74. package/dist/systems/roof/roof-system.d.ts +11 -3
  75. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  76. package/dist/systems/roof/roof-system.js +705 -210
  77. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  78. package/dist/systems/slab/slab-system.js +18 -3
  79. package/dist/systems/wall/wall-mitering.js +2 -2
  80. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  81. package/dist/systems/wall/wall-system.js +11 -13
  82. package/dist/systems/window/window-system.d.ts +2 -0
  83. package/dist/systems/window/window-system.d.ts.map +1 -0
  84. package/dist/systems/window/window-system.js +147 -0
  85. package/package.json +10 -8
@@ -1,5 +1,5 @@
1
1
  import type { ThreeEvent } from '@react-three/fiber';
2
- import type { BuildingNode, CeilingNode, ItemNode, LevelNode, RoofNode, SiteNode, SlabNode, WallNode, ZoneNode } from '../schema';
2
+ import type { BuildingNode, CeilingNode, DoorNode, ItemNode, LevelNode, RoofNode, RoofSegmentNode, SiteNode, SlabNode, WallNode, WindowNode, ZoneNode } from '../schema';
3
3
  import type { AnyNode } from '../schema/types';
4
4
  export interface GridEvent {
5
5
  position: [number, number, number];
@@ -22,6 +22,9 @@ export type ZoneEvent = NodeEvent<ZoneNode>;
22
22
  export type SlabEvent = NodeEvent<SlabNode>;
23
23
  export type CeilingEvent = NodeEvent<CeilingNode>;
24
24
  export type RoofEvent = NodeEvent<RoofNode>;
25
+ export type RoofSegmentEvent = NodeEvent<RoofSegmentNode>;
26
+ export type WindowEvent = NodeEvent<WindowNode>;
27
+ export type DoorEvent = NodeEvent<DoorNode>;
25
28
  export declare const eventSuffixes: readonly ["click", "move", "enter", "leave", "pointerdown", "pointerup", "context-menu", "double-click"];
26
29
  export type EventSuffix = (typeof eventSuffixes)[number];
27
30
  type NodeEvents<T extends string, E> = {
@@ -33,14 +36,31 @@ type GridEvents = {
33
36
  export interface CameraControlEvent {
34
37
  nodeId: AnyNode['id'];
35
38
  }
39
+ export interface ThumbnailGenerateEvent {
40
+ projectId: string;
41
+ }
36
42
  type CameraControlEvents = {
37
43
  'camera-controls:view': CameraControlEvent;
38
44
  'camera-controls:capture': CameraControlEvent;
39
45
  'camera-controls:top-view': undefined;
40
46
  'camera-controls:orbit-cw': undefined;
41
47
  'camera-controls:orbit-ccw': undefined;
48
+ 'camera-controls:generate-thumbnail': ThumbnailGenerateEvent;
49
+ };
50
+ type ToolEvents = {
51
+ 'tool:cancel': undefined;
52
+ };
53
+ type PresetEvents = {
54
+ 'preset:generate-thumbnail': {
55
+ presetId: string;
56
+ nodeId: string;
57
+ };
58
+ 'preset:thumbnail-updated': {
59
+ presetId: string;
60
+ thumbnailUrl: string;
61
+ };
42
62
  };
43
- type EditorEvents = GridEvents & NodeEvents<'wall', WallEvent> & NodeEvents<'item', ItemEvent> & NodeEvents<'site', SiteEvent> & NodeEvents<'building', BuildingEvent> & NodeEvents<'level', LevelEvent> & NodeEvents<'zone', ZoneEvent> & NodeEvents<'slab', SlabEvent> & NodeEvents<'ceiling', CeilingEvent> & NodeEvents<'roof', RoofEvent> & CameraControlEvents;
63
+ type EditorEvents = GridEvents & NodeEvents<'wall', WallEvent> & NodeEvents<'item', ItemEvent> & NodeEvents<'site', SiteEvent> & NodeEvents<'building', BuildingEvent> & NodeEvents<'level', LevelEvent> & NodeEvents<'zone', ZoneEvent> & NodeEvents<'slab', SlabEvent> & NodeEvents<'ceiling', CeilingEvent> & NodeEvents<'roof', RoofEvent> & NodeEvents<'roof-segment', RoofSegmentEvent> & NodeEvents<'window', WindowEvent> & NodeEvents<'door', DoorEvent> & CameraControlEvents & ToolEvents & PresetEvents;
44
64
  export declare const emitter: import("mitt").Emitter<EditorEvents>;
45
65
  export {};
46
66
  //# sourceMappingURL=bus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/events/bus.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/events/bus.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type * as THREE from "three";
1
+ import type * as THREE from 'three';
2
2
  export declare const sceneRegistry: {
3
3
  nodes: Map<string, THREE.Object3D<THREE.Object3DEventMap>>;
4
4
  byType: {
@@ -11,9 +11,14 @@ export declare const sceneRegistry: {
11
11
  slab: Set<string>;
12
12
  zone: Set<string>;
13
13
  roof: Set<string>;
14
+ 'roof-segment': Set<string>;
14
15
  scan: Set<string>;
15
16
  guide: Set<string>;
17
+ window: Set<string>;
18
+ door: Set<string>;
16
19
  };
20
+ /** Remove all entries. Call when unloading a scene to prevent stale 3D refs. */
21
+ clear(): void;
17
22
  };
18
23
  export declare function useRegistry(id: string, type: keyof typeof sceneRegistry.byType, ref: React.RefObject<THREE.Object3D>): void;
19
24
  //# sourceMappingURL=scene-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-registry.d.ts","sourceRoot":"","sources":["../../../src/hooks/scene-registry/scene-registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;CAmBzB,CAAC;AAEF,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,OAAO,aAAa,CAAC,MAAM,EACvC,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,QAkBrC"}
1
+ {"version":3,"file":"scene-registry.d.ts","sourceRoot":"","sources":["../../../src/hooks/scene-registry/scene-registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAEnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;IAuBxB,gFAAgF;;CAOjF,CAAA;AAED,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,OAAO,aAAa,CAAC,MAAM,EACvC,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,QAkBrC"}
@@ -1,4 +1,4 @@
1
- import { useLayoutEffect } from "react";
1
+ import { useLayoutEffect } from 'react';
2
2
  export const sceneRegistry = {
3
3
  // Master lookup: ID -> Object3D
4
4
  nodes: new Map(),
@@ -14,8 +14,18 @@ export const sceneRegistry = {
14
14
  slab: new Set(),
15
15
  zone: new Set(),
16
16
  roof: new Set(),
17
+ 'roof-segment': new Set(),
17
18
  scan: new Set(),
18
19
  guide: new Set(),
20
+ window: new Set(),
21
+ door: new Set(),
22
+ },
23
+ /** Remove all entries. Call when unloading a scene to prevent stale 3D refs. */
24
+ clear() {
25
+ this.nodes.clear();
26
+ for (const set of Object.values(this.byType)) {
27
+ set.clear();
28
+ }
19
29
  },
20
30
  };
21
31
  export function useRegistry(id, type, ref) {
@@ -20,14 +20,14 @@ export declare function itemOverlapsPolygon(position: [number, number, number],
20
20
  */
21
21
  export declare function wallOverlapsPolygon(start: [number, number], end: [number, number], polygon: Array<[number, number]>): boolean;
22
22
  export declare class SpatialGridManager {
23
- private cellSize;
24
- private floorGrids;
25
- private wallGrids;
26
- private walls;
27
- private slabsByLevel;
28
- private ceilingGrids;
29
- private ceilings;
30
- private itemCeilingMap;
23
+ private readonly floorGrids;
24
+ private readonly wallGrids;
25
+ private readonly walls;
26
+ private readonly slabsByLevel;
27
+ private readonly ceilingGrids;
28
+ private readonly ceilings;
29
+ private readonly itemCeilingMap;
30
+ private readonly cellSize;
31
31
  constructor(cellSize?: number);
32
32
  private getFloorGrid;
33
33
  private getWallGrid;
@@ -56,28 +56,33 @@ export declare class SpatialGridManager {
56
56
  canPlaceOnWall(levelId: string, wallId: string, localX: number, localY: number, dimensions: [number, number, number], attachType?: 'wall' | 'wall-side', side?: 'front' | 'back', ignoreIds?: string[]): {
57
57
  valid: boolean;
58
58
  conflictIds: string[];
59
+ adjustedY: number;
60
+ wasAdjusted: boolean;
61
+ } | {
62
+ valid: boolean;
63
+ conflictIds: never[];
59
64
  };
60
65
  getWallForItem(levelId: string, itemId: string): string | undefined;
61
66
  /**
62
67
  * Get the total slab elevation at a given (x, z) position on a level.
63
- * Returns the highest slab elevation if the point is inside any slab polygon, otherwise 0.
68
+ * Returns the highest slab elevation if the point is inside any slab polygon (but not in any holes), otherwise 0.
64
69
  */
65
70
  getSlabElevationAt(levelId: string, x: number, z: number): number;
66
71
  /**
67
72
  * Get the slab elevation for an item using its full footprint (bounding box).
68
- * Checks if any part of the item's rotated footprint overlaps with any slab polygon.
73
+ * Checks if any part of the item's rotated footprint overlaps with any slab polygon (excluding holes).
69
74
  * Returns the highest overlapping slab elevation, or 0 if none.
70
75
  */
71
76
  getSlabElevationForItem(levelId: string, position: [number, number, number], dimensions: [number, number, number], rotation: [number, number, number]): number;
72
77
  /**
73
- * Get the slab elevation for a wall by checking if it overlaps with any slab polygon.
78
+ * Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
74
79
  * Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
75
80
  * Returns the highest slab elevation found, or 0 if none.
76
81
  */
77
82
  getSlabElevationForWall(levelId: string, start: [number, number], end: [number, number]): number;
78
83
  /**
79
84
  * Check if an item can be placed on a ceiling.
80
- * Validates that the footprint is within the ceiling polygon and doesn't overlap other ceiling items.
85
+ * Validates that the footprint is within the ceiling polygon (but not in any holes) and doesn't overlap other ceiling items.
81
86
  */
82
87
  canPlaceOnCeiling(ceilingId: string, position: [number, number, number], dimensions: [number, number, number], rotation: [number, number, number], ignoreIds?: string[]): {
83
88
  valid: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"spatial-grid-manager.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"spatial-grid-manager.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid-manager.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { getScaledDimensions } from '../../schema';
1
2
  import { SpatialGrid } from './spatial-grid';
2
3
  import { WallSpatialGrid } from './wall-spatial-grid';
3
4
  // ============================================================================
@@ -12,7 +13,7 @@ export function pointInPolygon(px, pz, polygon) {
12
13
  for (let i = 0, j = n - 1; i < n; j = i++) {
13
14
  const xi = polygon[i][0], zi = polygon[i][1];
14
15
  const xj = polygon[j][0], zj = polygon[j][1];
15
- if ((zi > pz) !== (zj > pz) && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
16
+ if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
16
17
  inside = !inside;
17
18
  }
18
19
  }
@@ -45,13 +46,14 @@ function segmentsIntersect(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
45
46
  const d2 = cross(bx1, bz1, bx2, bz2, ax2, az2);
46
47
  const d3 = cross(ax1, az1, ax2, az2, bx1, bz1);
47
48
  const d4 = cross(ax1, az1, ax2, az2, bx2, bz2);
48
- if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
49
- ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
49
+ if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
50
50
  return true;
51
51
  }
52
52
  // Collinear touching cases
53
- const onSeg = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) <= rx && rx <= Math.max(px, qx) &&
54
- Math.min(pz, qz) <= rz && rz <= Math.max(pz, qz);
53
+ const onSeg = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) <= rx &&
54
+ rx <= Math.max(px, qx) &&
55
+ Math.min(pz, qz) <= rz &&
56
+ rz <= Math.max(pz, qz);
55
57
  if (d1 === 0 && onSeg(bx1, bz1, bx2, bz2, ax1, az1))
56
58
  return true;
57
59
  if (d2 === 0 && onSeg(bx1, bz1, bx2, bz2, ax2, az2))
@@ -114,8 +116,10 @@ function segmentsCollinearAndOverlap(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
114
116
  return false; // Not collinear
115
117
  }
116
118
  // Check if a point is on segment b
117
- const onSegment = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) - EPSILON <= rx && rx <= Math.max(px, qx) + EPSILON &&
118
- Math.min(pz, qz) - EPSILON <= rz && rz <= Math.max(pz, qz) + EPSILON;
119
+ const onSegment = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) - EPSILON <= rx &&
120
+ rx <= Math.max(px, qx) + EPSILON &&
121
+ Math.min(pz, qz) - EPSILON <= rz &&
122
+ rz <= Math.max(pz, qz) + EPSILON;
119
123
  // BOTH endpoints of wall (a) must be on edge (b) for substantial overlap
120
124
  const a1OnB = onSegment(bx1, bz1, bx2, bz2, ax1, az1);
121
125
  const a2OnB = onSegment(bx1, bz1, bx2, bz2, ax2, az2);
@@ -132,11 +136,41 @@ function segmentsCollinearAndOverlap(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
132
136
  * is NOT considered overlapping (adjacent only).
133
137
  */
134
138
  export function wallOverlapsPolygon(start, end, polygon) {
135
- const startInside = pointInPolygon(start[0], start[1], polygon);
136
- const endInside = pointInPolygon(end[0], end[1], polygon);
137
- // At least one endpoint strictly inside the polygon
138
- if (startInside || endInside)
139
- return true;
139
+ const dx = end[0] - start[0];
140
+ const dz = end[1] - start[1];
141
+ const len = Math.sqrt(dx * dx + dz * dz);
142
+ // Nudge endpoint test points a tiny step inward along the wall direction before
143
+ // testing containment. pointInPolygon (ray casting) produces false positives for
144
+ // points exactly on polygon vertices or edges — specifically the minimum-z corner
145
+ // of an axis-aligned polygon returns "inside" because the ray hits the opposite
146
+ // vertical edge exactly at its base. Nudging by 1e-6 m avoids this: a wall that
147
+ // merely starts at a slab corner and extends outward will have its nudged point
148
+ // clearly outside, while a wall that genuinely starts inside stays inside.
149
+ if (len > 1e-10) {
150
+ const step = Math.min(1e-6, len * 0.01);
151
+ const nx = (dx / len) * step;
152
+ const nz = (dz / len) * step;
153
+ if (pointInPolygon(start[0] + nx, start[1] + nz, polygon))
154
+ return true;
155
+ if (pointInPolygon(end[0] - nx, end[1] - nz, polygon))
156
+ return true;
157
+ // Also nudge perpendicular to the wall (into the slab interior) for walls that
158
+ // lie exactly on the slab boundary. The along-wall nudge keeps points on the
159
+ // boundary where pointInPolygon is unreliable; a perpendicular inward nudge
160
+ // moves the point clearly inside (or outside) the polygon.
161
+ // Sample the wall at 1/4, 1/2, 3/4 positions with a perpendicular nudge.
162
+ const PERP_STEP = 1e-4;
163
+ const pnx = (-nz / step) * PERP_STEP; // perpendicular left
164
+ const pnz = (nx / step) * PERP_STEP;
165
+ for (const t of [0.25, 0.5, 0.75]) {
166
+ const bx = start[0] + dx * t;
167
+ const bz = start[1] + dz * t;
168
+ if (pointInPolygon(bx + pnx, bz + pnz, polygon))
169
+ return true;
170
+ if (pointInPolygon(bx - pnx, bz - pnz, polygon))
171
+ return true;
172
+ }
173
+ }
140
174
  // Check if midpoint is inside (catches walls crossing through)
141
175
  const midX = (start[0] + end[0]) / 2;
142
176
  const midZ = (start[1] + end[1]) / 2;
@@ -155,7 +189,6 @@ export function wallOverlapsPolygon(start, end, polygon) {
155
189
  return false;
156
190
  }
157
191
  export class SpatialGridManager {
158
- cellSize;
159
192
  floorGrids = new Map(); // levelId -> grid
160
193
  wallGrids = new Map(); // levelId -> wall grid
161
194
  walls = new Map(); // wallId -> wall data (for length calculations)
@@ -163,6 +196,7 @@ export class SpatialGridManager {
163
196
  ceilingGrids = new Map(); // ceilingId -> grid
164
197
  ceilings = new Map(); // ceilingId -> ceiling data
165
198
  itemCeilingMap = new Map(); // itemId -> ceilingId (reverse lookup)
199
+ cellSize;
166
200
  constructor(cellSize = 0.5) {
167
201
  this.cellSize = cellSize;
168
202
  }
@@ -222,14 +256,14 @@ export class SpatialGridManager {
222
256
  if (wallId && this.walls.has(wallId)) {
223
257
  const wallLength = this.getWallLength(wallId);
224
258
  if (wallLength > 0) {
225
- const [width, height] = item.asset.dimensions;
259
+ const [width, height] = getScaledDimensions(item);
226
260
  const halfW = width / wallLength / 2;
227
261
  // Calculate t from local X position (position[0] is distance along wall)
228
262
  const t = item.position[0] / wallLength;
229
263
  // position[1] is the bottom of the item
230
264
  this.getWallGrid(levelId).insert({
231
265
  itemId: item.id,
232
- wallId: wallId,
266
+ wallId,
233
267
  tStart: t - halfW,
234
268
  tEnd: t + halfW,
235
269
  yStart: item.position[1],
@@ -244,13 +278,13 @@ export class SpatialGridManager {
244
278
  // Ceiling item - use parentId as the ceiling ID
245
279
  const ceilingId = item.parentId;
246
280
  if (ceilingId && this.ceilings.has(ceilingId)) {
247
- this.getCeilingGrid(ceilingId).insert(item.id, item.position, item.asset.dimensions, item.rotation);
281
+ this.getCeilingGrid(ceilingId).insert(item.id, item.position, getScaledDimensions(item), item.rotation);
248
282
  this.itemCeilingMap.set(item.id, ceilingId);
249
283
  }
250
284
  }
251
285
  else if (!item.asset.attachTo) {
252
286
  // Floor item
253
- this.getFloorGrid(levelId).insert(item.id, item.position, item.asset.dimensions, item.rotation);
287
+ this.getFloorGrid(levelId).insert(item.id, item.position, getScaledDimensions(item), item.rotation);
254
288
  }
255
289
  }
256
290
  }
@@ -274,14 +308,14 @@ export class SpatialGridManager {
274
308
  if (wallId && this.walls.has(wallId)) {
275
309
  const wallLength = this.getWallLength(wallId);
276
310
  if (wallLength > 0) {
277
- const [width, height] = item.asset.dimensions;
311
+ const [width, height] = getScaledDimensions(item);
278
312
  const halfW = width / wallLength / 2;
279
313
  // Calculate t from local X position (position[0] is distance along wall)
280
314
  const t = item.position[0] / wallLength;
281
315
  // position[1] is the bottom of the item
282
316
  this.getWallGrid(levelId).insert({
283
317
  itemId: item.id,
284
- wallId: wallId,
318
+ wallId,
285
319
  tStart: t - halfW,
286
320
  tEnd: t + halfW,
287
321
  yStart: item.position[1],
@@ -302,12 +336,12 @@ export class SpatialGridManager {
302
336
  // Insert into new ceiling grid
303
337
  const ceilingId = item.parentId;
304
338
  if (ceilingId && this.ceilings.has(ceilingId)) {
305
- this.getCeilingGrid(ceilingId).insert(item.id, item.position, item.asset.dimensions, item.rotation);
339
+ this.getCeilingGrid(ceilingId).insert(item.id, item.position, getScaledDimensions(item), item.rotation);
306
340
  this.itemCeilingMap.set(item.id, ceilingId);
307
341
  }
308
342
  }
309
343
  else if (!item.asset.attachTo) {
310
- this.getFloorGrid(levelId).update(item.id, item.position, item.asset.dimensions, item.rotation);
344
+ this.getFloorGrid(levelId).update(item.id, item.position, getScaledDimensions(item), item.rotation);
311
345
  }
312
346
  }
313
347
  }
@@ -369,7 +403,7 @@ export class SpatialGridManager {
369
403
  }
370
404
  /**
371
405
  * Get the total slab elevation at a given (x, z) position on a level.
372
- * Returns the highest slab elevation if the point is inside any slab polygon, otherwise 0.
406
+ * Returns the highest slab elevation if the point is inside any slab polygon (but not in any holes), otherwise 0.
373
407
  */
374
408
  getSlabElevationAt(levelId, x, z) {
375
409
  const slabMap = this.slabsByLevel.get(levelId);
@@ -378,9 +412,20 @@ export class SpatialGridManager {
378
412
  let maxElevation = 0;
379
413
  for (const slab of slabMap.values()) {
380
414
  if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
381
- const elevation = slab.elevation ?? 0.05;
382
- if (elevation > maxElevation) {
383
- maxElevation = elevation;
415
+ // Check if point is in any hole
416
+ let inHole = false;
417
+ const holes = slab.holes || [];
418
+ for (const hole of holes) {
419
+ if (hole.length >= 3 && pointInPolygon(x, z, hole)) {
420
+ inHole = true;
421
+ break;
422
+ }
423
+ }
424
+ if (!inHole) {
425
+ const elevation = slab.elevation ?? 0.05;
426
+ if (elevation > maxElevation) {
427
+ maxElevation = elevation;
428
+ }
384
429
  }
385
430
  }
386
431
  }
@@ -388,26 +433,40 @@ export class SpatialGridManager {
388
433
  }
389
434
  /**
390
435
  * Get the slab elevation for an item using its full footprint (bounding box).
391
- * Checks if any part of the item's rotated footprint overlaps with any slab polygon.
436
+ * Checks if any part of the item's rotated footprint overlaps with any slab polygon (excluding holes).
392
437
  * Returns the highest overlapping slab elevation, or 0 if none.
393
438
  */
394
439
  getSlabElevationForItem(levelId, position, dimensions, rotation) {
395
440
  const slabMap = this.slabsByLevel.get(levelId);
396
441
  if (!slabMap)
397
442
  return 0;
398
- let maxElevation = -Infinity;
443
+ let maxElevation = Number.NEGATIVE_INFINITY;
399
444
  for (const slab of slabMap.values()) {
400
- if (slab.polygon.length >= 3 && itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) {
401
- const elevation = slab.elevation ?? 0.05;
402
- if (elevation > maxElevation) {
403
- maxElevation = elevation;
445
+ if (slab.polygon.length >= 3 &&
446
+ itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) {
447
+ // Check if item is entirely within a hole (if so, ignore this slab)
448
+ // We consider it entirely in a hole if the item center is in the hole
449
+ let inHole = false;
450
+ const [cx, , cz] = position;
451
+ const holes = slab.holes || [];
452
+ for (const hole of holes) {
453
+ if (hole.length >= 3 && pointInPolygon(cx, cz, hole)) {
454
+ inHole = true;
455
+ break;
456
+ }
457
+ }
458
+ if (!inHole) {
459
+ const elevation = slab.elevation ?? 0.05;
460
+ if (elevation > maxElevation) {
461
+ maxElevation = elevation;
462
+ }
404
463
  }
405
464
  }
406
465
  }
407
- return maxElevation === -Infinity ? 0 : maxElevation;
466
+ return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation;
408
467
  }
409
468
  /**
410
- * Get the slab elevation for a wall by checking if it overlaps with any slab polygon.
469
+ * Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
411
470
  * Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
412
471
  * Returns the highest slab elevation found, or 0 if none.
413
472
  */
@@ -415,22 +474,52 @@ export class SpatialGridManager {
415
474
  const slabMap = this.slabsByLevel.get(levelId);
416
475
  if (!slabMap)
417
476
  return 0;
418
- let maxElevation = -Infinity;
477
+ let maxElevation = Number.NEGATIVE_INFINITY;
419
478
  for (const slab of slabMap.values()) {
420
479
  if (slab.polygon.length < 3)
421
480
  continue;
422
- if (wallOverlapsPolygon(start, end, slab.polygon)) {
481
+ if (!wallOverlapsPolygon(start, end, slab.polygon))
482
+ continue;
483
+ const holes = slab.holes || [];
484
+ if (holes.length === 0) {
485
+ // No holes: wall is on this slab
423
486
  const elevation = slab.elevation ?? 0.05;
424
- if (elevation > maxElevation) {
487
+ if (elevation > maxElevation)
425
488
  maxElevation = elevation;
489
+ continue;
490
+ }
491
+ // Sample multiple points along the wall to check whether any portion lies on
492
+ // solid slab (not inside any hole). Checking only the midpoint fails when the
493
+ // midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
494
+ const dx = end[0] - start[0];
495
+ const dz = end[1] - start[1];
496
+ let hasValidPoint = false;
497
+ for (const t of [0, 0.25, 0.5, 0.75, 1]) {
498
+ const px = start[0] + dx * t;
499
+ const pz = start[1] + dz * t;
500
+ let inHole = false;
501
+ for (const hole of holes) {
502
+ if (hole.length >= 3 && pointInPolygon(px, pz, hole)) {
503
+ inHole = true;
504
+ break;
505
+ }
426
506
  }
507
+ if (!inHole) {
508
+ hasValidPoint = true;
509
+ break;
510
+ }
511
+ }
512
+ if (hasValidPoint) {
513
+ const elevation = slab.elevation ?? 0.05;
514
+ if (elevation > maxElevation)
515
+ maxElevation = elevation;
427
516
  }
428
517
  }
429
- return maxElevation === -Infinity ? 0 : maxElevation;
518
+ return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation;
430
519
  }
431
520
  /**
432
521
  * Check if an item can be placed on a ceiling.
433
- * Validates that the footprint is within the ceiling polygon and doesn't overlap other ceiling items.
522
+ * Validates that the footprint is within the ceiling polygon (but not in any holes) and doesn't overlap other ceiling items.
434
523
  */
435
524
  canPlaceOnCeiling(ceilingId, position, dimensions, rotation, ignoreIds) {
436
525
  const ceiling = this.ceilings.get(ceilingId);
@@ -444,6 +533,14 @@ export class SpatialGridManager {
444
533
  return { valid: false, conflictIds: [] };
445
534
  }
446
535
  }
536
+ // Check if item center is in any hole (if so, it cannot be placed)
537
+ const [centerX, , centerZ] = position;
538
+ const holes = ceiling.holes || [];
539
+ for (const hole of holes) {
540
+ if (hole.length >= 3 && pointInPolygon(centerX, centerZ, hole)) {
541
+ return { valid: false, conflictIds: [] };
542
+ }
543
+ }
447
544
  // Check for overlaps with other ceiling items
448
545
  return this.getCeilingGrid(ceilingId).canPlace(position, dimensions, rotation, ignoreIds);
449
546
  }
@@ -1,4 +1,4 @@
1
- import type { AnyNode } from '../../schema';
1
+ import { type AnyNode } from '../../schema';
2
2
  export declare function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): string;
3
3
  export declare function initSpatialGridSync(): void;
4
4
  //# sourceMappingURL=spatial-grid-sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spatial-grid-sync.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA2C,MAAM,cAAc,CAAA;AAIpF,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAmBpF;AAGD,wBAAgB,mBAAmB,SAmElC"}
1
+ {"version":3,"file":"spatial-grid-sync.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid-sync.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAMb,MAAM,cAAc,CAAA;AAQrB,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAmBpF;AAGD,wBAAgB,mBAAmB,SA8ElC"}
@@ -1,5 +1,6 @@
1
+ import { getScaledDimensions, } from '../../schema';
1
2
  import useScene from '../../store/use-scene';
2
- import { itemOverlapsPolygon, spatialGridManager, wallOverlapsPolygon } from './spatial-grid-manager';
3
+ import { itemOverlapsPolygon, spatialGridManager, wallOverlapsPolygon, } from './spatial-grid-manager';
3
4
  export function resolveLevelId(node, nodes) {
4
5
  // If the node itself is a level
5
6
  if (node.type === 'level')
@@ -11,11 +12,11 @@ export function resolveLevelId(node, nodes) {
11
12
  if (current.type === 'level')
12
13
  return current.id;
13
14
  // Find parent (you might need to add parentId to your schema or derive it)
14
- if (!current.parentId) {
15
- current = undefined;
15
+ if (current.parentId) {
16
+ current = nodes[current.parentId];
16
17
  }
17
18
  else {
18
- current = nodes[current.parentId];
19
+ current = undefined;
19
20
  }
20
21
  }
21
22
  return 'default'; // fallback for orphaned items
@@ -61,16 +62,23 @@ export function initSpatialGridSync() {
61
62
  if (!prev)
62
63
  continue;
63
64
  if (node.type === 'item' && prev.type === 'item') {
64
- if (!arraysEqual(node.position, prev.position) ||
65
- !arraysEqual(node.rotation, prev.rotation) ||
65
+ if (!(arraysEqual(node.position, prev.position) &&
66
+ arraysEqual(node.rotation, prev.rotation) &&
67
+ arraysEqual(node.scale, prev.scale)) ||
66
68
  node.parentId !== prev.parentId ||
67
69
  node.side !== prev.side) {
68
70
  const levelId = resolveLevelId(node, state.nodes);
69
71
  spatialGridManager.handleNodeUpdated(node, levelId);
72
+ // Scale changes affect footprint size — mark dirty so slab elevation recalculates
73
+ if (!arraysEqual(node.scale, prev.scale)) {
74
+ markDirty(node.id);
75
+ }
70
76
  }
71
77
  }
72
78
  else if (node.type === 'slab' && prev.type === 'slab') {
73
- if (node.polygon !== prev.polygon || node.elevation !== prev.elevation) {
79
+ if (node.polygon !== prev.polygon ||
80
+ node.elevation !== prev.elevation ||
81
+ node.holes !== prev.holes) {
74
82
  const levelId = resolveLevelId(node, state.nodes);
75
83
  spatialGridManager.handleNodeUpdated(node, levelId);
76
84
  // Mark nodes overlapping old polygon and new polygon as dirty
@@ -99,7 +107,7 @@ function markNodesOverlappingSlab(slab, nodes, markDirty) {
99
107
  continue;
100
108
  if (resolveLevelId(node, nodes) !== slabLevelId)
101
109
  continue;
102
- if (itemOverlapsPolygon(item.position, item.asset.dimensions, item.rotation, slab.polygon, 0.01)) {
110
+ if (itemOverlapsPolygon(item.position, getScaledDimensions(item), item.rotation, slab.polygon, 0.01)) {
103
111
  markDirty(node.id);
104
112
  }
105
113
  }
@@ -2,9 +2,9 @@ interface SpatialGridConfig {
2
2
  cellSize: number;
3
3
  }
4
4
  export declare class SpatialGrid {
5
- private config;
6
- private cells;
7
- private itemCells;
5
+ private readonly cells;
6
+ private readonly itemCells;
7
+ private readonly config;
8
8
  constructor(config: SpatialGridConfig);
9
9
  private posToCell;
10
10
  private cellKey;
@@ -1 +1 @@
1
- {"version":3,"file":"spatial-grid.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid.ts"],"names":[],"mappings":"AAMA,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,WAAW;IAIV,OAAO,CAAC,MAAM;IAH1B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,SAAS,CAAkC;gBAE/B,MAAM,EAAE,iBAAiB;IAE7C,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,YAAY;IAsCpB,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAepC,MAAM,CAAC,MAAM,EAAE,MAAM;IAiBrB,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAOpC,QAAQ,CACN,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,SAAS,GAAE,MAAM,EAAO,GACvB;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE;IAuB5C,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;IAkB3D,YAAY,IAAI,MAAM;CAGvB"}
1
+ {"version":3,"file":"spatial-grid.d.ts","sourceRoot":"","sources":["../../../src/hooks/spatial-grid/spatial-grid.ts"],"names":[],"mappings":"AAMA,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAE5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;gBAE9B,MAAM,EAAE,iBAAiB;IAIrC,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,YAAY;IAsCpB,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAepC,MAAM,CAAC,MAAM,EAAE,MAAM;IAiBrB,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAOpC,QAAQ,CACN,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,SAAS,GAAE,MAAM,EAAO,GACvB;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE;IAuB5C,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;IAkB3D,YAAY,IAAI,MAAM;CAGvB"}
@@ -1,7 +1,7 @@
1
1
  export class SpatialGrid {
2
- config;
3
2
  cells = new Map();
4
3
  itemCells = new Map(); // reverse lookup
4
+ config;
5
5
  constructor(config) {
6
6
  this.config = config;
7
7
  }
@@ -48,7 +48,7 @@ export class SpatialGrid {
48
48
  if (!this.cells.has(key)) {
49
49
  this.cells.set(key, { itemIds: new Set() });
50
50
  }
51
- this.cells.get(key).itemIds.add(itemId);
51
+ this.cells.get(key)?.itemIds.add(itemId);
52
52
  }
53
53
  }
54
54
  // Remove an item