@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.
- package/dist/events/bus.d.ts +22 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +6 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +11 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +17 -12
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +135 -38
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +16 -8
- package/dist/hooks/spatial-grid/spatial-grid.d.ts +3 -3
- package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid.js +2 -2
- package/dist/hooks/spatial-grid/use-spatial-query.d.ts +5 -0
- package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +6 -3
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/wall-spatial-grid.js +35 -10
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +1 -1
- package/dist/schema/collections.d.ts +11 -0
- package/dist/schema/collections.d.ts.map +1 -0
- package/dist/schema/collections.js +2 -0
- package/dist/schema/index.d.ts +11 -7
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +11 -6
- package/dist/schema/nodes/ceiling.d.ts +1 -0
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +2 -0
- package/dist/schema/nodes/door.d.ts +78 -0
- package/dist/schema/nodes/door.d.ts.map +1 -0
- package/dist/schema/nodes/door.js +67 -0
- package/dist/schema/nodes/item.d.ts +245 -2
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +72 -0
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +11 -1
- package/dist/schema/nodes/roof-segment.d.ts +51 -0
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment.js +36 -0
- package/dist/schema/nodes/roof.d.ts +1 -4
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +9 -16
- package/dist/schema/nodes/site.d.ts +52 -1
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +1 -0
- package/dist/schema/nodes/window.d.ts +40 -0
- package/dist/schema/nodes/window.d.ts.map +1 -0
- package/dist/schema/nodes/window.js +38 -0
- package/dist/schema/types.d.ts +241 -6
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +6 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +23 -4
- package/dist/store/use-interactive.d.ts +18 -0
- package/dist/store/use-interactive.d.ts.map +1 -0
- package/dist/store/use-interactive.js +50 -0
- package/dist/store/use-scene.d.ts +10 -1
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +180 -57
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +20 -0
- package/dist/systems/door/door-system.d.ts +2 -0
- package/dist/systems/door/door-system.d.ts.map +1 -0
- package/dist/systems/door/door-system.js +211 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +10 -5
- package/dist/systems/roof/roof-system.d.ts +11 -3
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +705 -210
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +18 -3
- package/dist/systems/wall/wall-mitering.js +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +11 -13
- package/dist/systems/window/window-system.d.ts +2 -0
- package/dist/systems/window/window-system.d.ts.map +1 -0
- package/dist/systems/window/window-system.js +147 -0
- package/package.json +10 -8
package/dist/events/bus.d.ts
CHANGED
|
@@ -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
|
package/dist/events/bus.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/events/bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD,OAAO,KAAK,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
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":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA6C,MAAM,cAAc,CAAA;
|
|
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 (
|
|
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 &&
|
|
54
|
-
|
|
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 &&
|
|
118
|
-
|
|
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
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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 =
|
|
443
|
+
let maxElevation = Number.NEGATIVE_INFINITY;
|
|
399
444
|
for (const slab of slabMap.values()) {
|
|
400
|
-
if (slab.polygon.length >= 3 &&
|
|
401
|
-
|
|
402
|
-
if (
|
|
403
|
-
|
|
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 ===
|
|
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 =
|
|
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 ===
|
|
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
|
|
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,
|
|
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 (
|
|
15
|
-
current =
|
|
15
|
+
if (current.parentId) {
|
|
16
|
+
current = nodes[current.parentId];
|
|
16
17
|
}
|
|
17
18
|
else {
|
|
18
|
-
current =
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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
|
|
6
|
-
private
|
|
7
|
-
private
|
|
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;
|
|
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)
|
|
51
|
+
this.cells.get(key)?.itemIds.add(itemId);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
// Remove an item
|