@pascal-app/core 0.6.0 → 0.7.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 (81) hide show
  1. package/dist/events/bus.d.ts +38 -2
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
  4. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  5. package/dist/hooks/scene-registry/scene-registry.js +2 -0
  6. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.js +164 -6
  8. package/dist/index.d.ts +8 -15
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +7 -14
  11. package/dist/lib/door-operation.d.ts +7 -0
  12. package/dist/lib/door-operation.d.ts.map +1 -0
  13. package/dist/lib/door-operation.js +25 -0
  14. package/dist/lib/slab-polygon.d.ts +3 -0
  15. package/dist/lib/slab-polygon.d.ts.map +1 -0
  16. package/dist/lib/slab-polygon.js +58 -0
  17. package/dist/material-library.d.ts +5 -3
  18. package/dist/material-library.d.ts.map +1 -1
  19. package/dist/material-library.js +26 -49
  20. package/dist/schema/asset-url.d.ts +34 -0
  21. package/dist/schema/asset-url.d.ts.map +1 -0
  22. package/dist/schema/asset-url.js +79 -0
  23. package/dist/schema/asset-url.test.d.ts +2 -0
  24. package/dist/schema/asset-url.test.d.ts.map +1 -0
  25. package/dist/schema/asset-url.test.js +138 -0
  26. package/dist/schema/index.d.ts +7 -5
  27. package/dist/schema/index.d.ts.map +1 -1
  28. package/dist/schema/index.js +5 -3
  29. package/dist/schema/material.d.ts +3 -2
  30. package/dist/schema/material.d.ts.map +1 -1
  31. package/dist/schema/material.js +13 -11
  32. package/dist/schema/nodes/ceiling.d.ts +1 -1
  33. package/dist/schema/nodes/column.d.ts +520 -0
  34. package/dist/schema/nodes/column.d.ts.map +1 -0
  35. package/dist/schema/nodes/column.js +385 -0
  36. package/dist/schema/nodes/door.d.ts +73 -1
  37. package/dist/schema/nodes/door.d.ts.map +1 -1
  38. package/dist/schema/nodes/door.js +39 -2
  39. package/dist/schema/nodes/fence.d.ts +1 -1
  40. package/dist/schema/nodes/guide.d.ts +17 -0
  41. package/dist/schema/nodes/guide.d.ts.map +1 -1
  42. package/dist/schema/nodes/guide.js +11 -1
  43. package/dist/schema/nodes/item.d.ts +8 -0
  44. package/dist/schema/nodes/item.d.ts.map +1 -1
  45. package/dist/schema/nodes/item.js +18 -1
  46. package/dist/schema/nodes/level.d.ts +1 -1
  47. package/dist/schema/nodes/level.d.ts.map +1 -1
  48. package/dist/schema/nodes/level.js +6 -0
  49. package/dist/schema/nodes/roof-segment.d.ts +1 -1
  50. package/dist/schema/nodes/roof.d.ts +4 -4
  51. package/dist/schema/nodes/scan.d.ts.map +1 -1
  52. package/dist/schema/nodes/scan.js +2 -1
  53. package/dist/schema/nodes/site.d.ts +1 -0
  54. package/dist/schema/nodes/site.d.ts.map +1 -1
  55. package/dist/schema/nodes/slab.d.ts +1 -1
  56. package/dist/schema/nodes/spawn.d.ts +24 -0
  57. package/dist/schema/nodes/spawn.d.ts.map +1 -0
  58. package/dist/schema/nodes/spawn.js +8 -0
  59. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  60. package/dist/schema/nodes/stair.d.ts +8 -8
  61. package/dist/schema/nodes/wall.d.ts +3 -3
  62. package/dist/schema/nodes/window.d.ts +56 -1
  63. package/dist/schema/nodes/window.d.ts.map +1 -1
  64. package/dist/schema/nodes/window.js +29 -0
  65. package/dist/schema/types.d.ts +324 -21
  66. package/dist/schema/types.d.ts.map +1 -1
  67. package/dist/schema/types.js +4 -0
  68. package/dist/store/actions/node-actions.d.ts.map +1 -1
  69. package/dist/store/actions/node-actions.js +6 -5
  70. package/dist/store/use-interactive.d.ts +43 -0
  71. package/dist/store/use-interactive.d.ts.map +1 -1
  72. package/dist/store/use-interactive.js +66 -0
  73. package/dist/store/use-scene.d.ts.map +1 -1
  74. package/dist/store/use-scene.js +60 -2
  75. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  76. package/dist/systems/stair/stair-opening-sync.js +81 -20
  77. package/dist/systems/stair/stair-opening-sync.test.d.ts +2 -0
  78. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +1 -0
  79. package/dist/systems/stair/stair-opening-sync.test.js +65 -0
  80. package/dist/systems/stair/stair-system.js +1 -1
  81. package/package.json +31 -3
@@ -1,6 +1,7 @@
1
1
  import z from 'zod';
2
2
  import { BuildingNode } from './nodes/building';
3
3
  import { CeilingNode } from './nodes/ceiling';
4
+ import { ColumnNode } from './nodes/column';
4
5
  import { DoorNode } from './nodes/door';
5
6
  import { FenceNode } from './nodes/fence';
6
7
  import { GuideNode } from './nodes/guide';
@@ -11,6 +12,7 @@ import { RoofSegmentNode } from './nodes/roof-segment';
11
12
  import { ScanNode } from './nodes/scan';
12
13
  import { SiteNode } from './nodes/site';
13
14
  import { SlabNode } from './nodes/slab';
15
+ import { SpawnNode } from './nodes/spawn';
14
16
  import { StairNode } from './nodes/stair';
15
17
  import { StairSegmentNode } from './nodes/stair-segment';
16
18
  import { WallNode } from './nodes/wall';
@@ -20,6 +22,7 @@ export const AnyNode = z.discriminatedUnion('type', [
20
22
  SiteNode,
21
23
  BuildingNode,
22
24
  LevelNode,
25
+ ColumnNode,
23
26
  WallNode,
24
27
  FenceNode,
25
28
  ItemNode,
@@ -32,6 +35,7 @@ export const AnyNode = z.discriminatedUnion('type', [
32
35
  StairSegmentNode,
33
36
  ScanNode,
34
37
  GuideNode,
38
+ SpawnNode,
35
39
  WindowNode,
36
40
  DoorNode,
37
41
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"node-actions.d.ts","sourceRoot":"","sources":["../../../src/store/actions/node-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAIf,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AA6N9C,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,EAAE,SA4C/C,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,SAAS;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAAE,SA+DrD,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK,SAAS,EAAE,SAsGjB,CAAA"}
1
+ {"version":3,"file":"node-actions.d.ts","sourceRoot":"","sources":["../../../src/store/actions/node-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAIf,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AA6N9C,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,EAAE,SA8C/C,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,SAAS;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAAE,SA+DrD,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK,SAAS,EAAE,SAsGjB,CAAA"}
@@ -153,25 +153,26 @@ export const createNodesAction = (set, get, ops) => {
153
153
  const nextNodes = { ...state.nodes };
154
154
  const nextRootIds = [...state.rootNodeIds];
155
155
  for (const { node, parentId } of ops) {
156
+ const effectiveParentId = parentId ?? node.parentId ?? null;
156
157
  // 1. Assign parentId to the child (Safe because BaseNode has parentId)
157
158
  const newNode = {
158
159
  ...node,
159
- parentId: parentId ?? null,
160
+ parentId: effectiveParentId,
160
161
  };
161
162
  nextNodes[newNode.id] = newNode;
162
163
  // 2. Update the Parent's children list
163
- if (parentId && nextNodes[parentId]) {
164
- const parent = nextNodes[parentId];
164
+ if (effectiveParentId && nextNodes[effectiveParentId]) {
165
+ const parent = nextNodes[effectiveParentId];
165
166
  // Type Guard: Check if the parent node is a container that supports children
166
167
  if ('children' in parent && Array.isArray(parent.children)) {
167
- nextNodes[parentId] = {
168
+ nextNodes[effectiveParentId] = {
168
169
  ...parent,
169
170
  // Use Set to prevent duplicate IDs if createNode is called twice
170
171
  children: Array.from(new Set([...parent.children, newNode.id])), // We don't verify child types here
171
172
  };
172
173
  }
173
174
  }
174
- else if (!parentId) {
175
+ else if (!effectiveParentId) {
175
176
  // 3. Handle Root nodes
176
177
  if (!nextRootIds.includes(newNode.id)) {
177
178
  nextRootIds.push(newNode.id);
@@ -4,14 +4,57 @@ export type ControlValue = boolean | number;
4
4
  export type ItemInteractiveState = {
5
5
  controlValues: ControlValue[];
6
6
  };
7
+ export type DoorInteractiveState = {
8
+ operationState?: number;
9
+ swingAngle?: number;
10
+ };
11
+ export type DoorAnimationState = {
12
+ field: keyof DoorInteractiveState;
13
+ from: number;
14
+ to: number;
15
+ startedAt: number | null;
16
+ durationMs: number;
17
+ persist: boolean;
18
+ };
19
+ export type WindowInteractiveState = {
20
+ operationState?: number;
21
+ };
22
+ export type WindowAnimationState = {
23
+ field: keyof WindowInteractiveState;
24
+ from: number;
25
+ to: number;
26
+ startedAt: number | null;
27
+ durationMs: number;
28
+ persist: boolean;
29
+ };
7
30
  type InteractiveStore = {
8
31
  items: Record<AnyNodeId, ItemInteractiveState>;
32
+ doors: Record<AnyNodeId, DoorInteractiveState>;
33
+ doorAnimations: Record<AnyNodeId, DoorAnimationState>;
34
+ windows: Record<AnyNodeId, WindowInteractiveState>;
35
+ windowAnimations: Record<AnyNodeId, WindowAnimationState>;
9
36
  /** Initialize a node's interactive state from its asset definition (idempotent) */
10
37
  initItem: (itemId: AnyNodeId, interactive: Interactive) => void;
11
38
  /** Set a single control value */
12
39
  setControlValue: (itemId: AnyNodeId, index: number, value: ControlValue) => void;
13
40
  /** Remove a node's state (e.g. on unmount) */
14
41
  removeItem: (itemId: AnyNodeId) => void;
42
+ /** Set transient door open state without committing it to the scene node */
43
+ setDoorOpenState: (doorId: AnyNodeId, value: DoorInteractiveState) => void;
44
+ /** Clear transient door open state */
45
+ removeDoorOpenState: (doorId: AnyNodeId) => void;
46
+ /** Queue a door animation for the viewer frame loop */
47
+ startDoorAnimation: (doorId: AnyNodeId, value: DoorAnimationState) => void;
48
+ /** Cancel a queued door animation */
49
+ cancelDoorAnimation: (doorId: AnyNodeId) => void;
50
+ /** Set transient window open state without committing it to the scene node */
51
+ setWindowOpenState: (windowId: AnyNodeId, value: WindowInteractiveState) => void;
52
+ /** Clear transient window open state */
53
+ removeWindowOpenState: (windowId: AnyNodeId) => void;
54
+ /** Queue a window animation for the viewer frame loop */
55
+ startWindowAnimation: (windowId: AnyNodeId, value: WindowAnimationState) => void;
56
+ /** Cancel a queued window animation */
57
+ cancelWindowAnimation: (windowId: AnyNodeId) => void;
15
58
  };
16
59
  export declare const useInteractive: import("zustand").UseBoundStore<import("zustand").StoreApi<InteractiveStore>>;
17
60
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"use-interactive.d.ts","sourceRoot":"","sources":["../../src/store/use-interactive.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,CAAA;AAE3C,MAAM,MAAM,oBAAoB,GAAG;IAEjC,aAAa,EAAE,YAAY,EAAE,CAAA;CAC9B,CAAA;AAED,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAA;IAE9C,mFAAmF;IACnF,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,KAAK,IAAI,CAAA;IAE/D,iCAAiC;IACjC,eAAe,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IAEhF,8CAA8C;IAC9C,UAAU,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACxC,CAAA;AAeD,eAAO,MAAM,cAAc,+EAoCxB,CAAA"}
1
+ {"version":3,"file":"use-interactive.d.ts","sourceRoot":"","sources":["../../src/store/use-interactive.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,CAAA;AAE3C,MAAM,MAAM,oBAAoB,GAAG;IAEjC,aAAa,EAAE,YAAY,EAAE,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,oBAAoB,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,sBAAsB,CAAA;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAA;IAC9C,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAA;IAC9C,cAAc,EAAE,MAAM,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAA;IACrD,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAA;IAClD,gBAAgB,EAAE,MAAM,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAA;IAEzD,mFAAmF;IACnF,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,KAAK,IAAI,CAAA;IAE/D,iCAAiC;IACjC,eAAe,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IAEhF,8CAA8C;IAC9C,UAAU,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAEvC,4EAA4E;IAC5E,gBAAgB,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAE1E,sCAAsC;IACtC,mBAAmB,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAEhD,uDAAuD;IACvD,kBAAkB,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAE1E,qCAAqC;IACrC,mBAAmB,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAEhD,8EAA8E;IAC9E,kBAAkB,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;IAEhF,wCAAwC;IACxC,qBAAqB,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAA;IAEpD,yDAAyD;IACzD,oBAAoB,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAEhF,uCAAuC;IACvC,qBAAqB,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAA;CACrD,CAAA;AAeD,eAAO,MAAM,cAAc,+EA8GxB,CAAA"}
@@ -15,6 +15,10 @@ const defaultControlValue = (interactive, index) => {
15
15
  };
16
16
  export const useInteractive = create((set, get) => ({
17
17
  items: {},
18
+ doors: {},
19
+ doorAnimations: {},
20
+ windows: {},
21
+ windowAnimations: {},
18
22
  initItem: (itemId, interactive) => {
19
23
  const { controls } = interactive;
20
24
  if (controls.length === 0)
@@ -47,4 +51,66 @@ export const useInteractive = create((set, get) => ({
47
51
  return { items: rest };
48
52
  });
49
53
  },
54
+ setDoorOpenState: (doorId, value) => {
55
+ set((state) => ({
56
+ doors: {
57
+ ...state.doors,
58
+ [doorId]: {
59
+ ...state.doors[doorId],
60
+ ...value,
61
+ },
62
+ },
63
+ }));
64
+ },
65
+ removeDoorOpenState: (doorId) => {
66
+ set((state) => {
67
+ const { [doorId]: _, ...rest } = state.doors;
68
+ return { doors: rest };
69
+ });
70
+ },
71
+ startDoorAnimation: (doorId, value) => {
72
+ set((state) => ({
73
+ doorAnimations: {
74
+ ...state.doorAnimations,
75
+ [doorId]: value,
76
+ },
77
+ }));
78
+ },
79
+ cancelDoorAnimation: (doorId) => {
80
+ set((state) => {
81
+ const { [doorId]: _, ...rest } = state.doorAnimations;
82
+ return { doorAnimations: rest };
83
+ });
84
+ },
85
+ setWindowOpenState: (windowId, value) => {
86
+ set((state) => ({
87
+ windows: {
88
+ ...state.windows,
89
+ [windowId]: {
90
+ ...state.windows[windowId],
91
+ ...value,
92
+ },
93
+ },
94
+ }));
95
+ },
96
+ removeWindowOpenState: (windowId) => {
97
+ set((state) => {
98
+ const { [windowId]: _, ...rest } = state.windows;
99
+ return { windows: rest };
100
+ });
101
+ },
102
+ startWindowAnimation: (windowId, value) => {
103
+ set((state) => ({
104
+ windowAnimations: {
105
+ ...state.windowAnimations,
106
+ [windowId]: value,
107
+ },
108
+ }));
109
+ },
110
+ cancelWindowAnimation: (windowId) => {
111
+ set((state) => {
112
+ const { [windowId]: _, ...rest } = state.windowAnimations;
113
+ return { windowAnimations: rest };
114
+ });
115
+ },
50
116
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAMrE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAiVzD,MAAM,MAAM,UAAU,GAAG;IAEvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAGjC,WAAW,EAAE,SAAS,EAAE,CAAA;IAGxB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAG1B,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAG7C,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAGxC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAE/E,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAEnC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAErE,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAE3E,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAGvC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,YAAY,CAAA;IACvE,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAA;IACnF,eAAe,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAC9D,oBAAoB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACpE,CAAA;AAID,KAAK,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,GAAG;IACzD,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;CAC7F,CAAA;AAED,QAAA,MAAM,QAAQ,EAAE,aA8Nf,CAAA;AAED,eAAe,QAAQ,CAAA;AAOvB,wBAAgB,iBAAiB,SAMhC"}
1
+ {"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAMrE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAgZzD,MAAM,MAAM,UAAU,GAAG;IAEvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAGjC,WAAW,EAAE,SAAS,EAAE,CAAA;IAGxB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAG1B,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAG7C,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAGxC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAE/E,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAEnC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAErE,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAE3E,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAGvC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,YAAY,CAAA;IACvE,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAA;IACnF,eAAe,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAC9D,oBAAoB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACpE,CAAA;AAID,KAAK,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,GAAG;IACzD,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;CAC7F,CAAA;AAED,QAAA,MAAM,QAAQ,EAAE,aAwOf,CAAA;AAED,eAAe,QAAQ,CAAA;AAOvB,wBAAgB,iBAAiB,SAMhC"}
@@ -7,8 +7,8 @@ import { LevelNode } from '../schema/nodes/level';
7
7
  import { SiteNode } from '../schema/nodes/site';
8
8
  import { StairNode as StairNodeSchema } from '../schema/nodes/stair';
9
9
  import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment';
10
- import { resetSceneHistoryPauseDepth } from './history-control';
11
10
  import * as nodeActions from './actions/node-actions';
11
+ import { resetSceneHistoryPauseDepth } from './history-control';
12
12
  function getFiniteNumber(value, fallback) {
13
13
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
14
14
  }
@@ -290,6 +290,54 @@ function migrateNodes(nodes) {
290
290
  }
291
291
  return patchedNodes;
292
292
  }
293
+ function getNodeChildIds(node) {
294
+ if (!('children' in node) || !Array.isArray(node.children)) {
295
+ return [];
296
+ }
297
+ return node.children
298
+ .map((child) => {
299
+ if (typeof child === 'string')
300
+ return child;
301
+ if (child && typeof child === 'object' && 'id' in child && typeof child.id === 'string') {
302
+ return child.id;
303
+ }
304
+ return null;
305
+ })
306
+ .filter((id) => typeof id === 'string');
307
+ }
308
+ function normalizeRootNodeIds(nodes, rootNodeIds) {
309
+ const existingRootIds = rootNodeIds.filter((id) => Boolean(nodes[id]));
310
+ const siteRootIds = existingRootIds.filter((id) => nodes[id]?.type === 'site');
311
+ if (siteRootIds.length > 0) {
312
+ return siteRootIds;
313
+ }
314
+ return existingRootIds.filter((id) => nodes[id]?.parentId === null);
315
+ }
316
+ function collectReachableNodeIds(nodes, rootNodeIds) {
317
+ const reachable = new Set();
318
+ const stack = [...rootNodeIds];
319
+ const childIdsByParentId = new Map();
320
+ for (const node of Object.values(nodes)) {
321
+ if (!node.parentId)
322
+ continue;
323
+ const parentId = node.parentId;
324
+ const children = childIdsByParentId.get(parentId) ?? [];
325
+ children.push(node.id);
326
+ childIdsByParentId.set(parentId, children);
327
+ }
328
+ while (stack.length > 0) {
329
+ const id = stack.pop();
330
+ if (!id || reachable.has(id))
331
+ continue;
332
+ const node = nodes[id];
333
+ if (!node)
334
+ continue;
335
+ reachable.add(id);
336
+ stack.push(...getNodeChildIds(node));
337
+ stack.push(...(childIdsByParentId.get(id) ?? []));
338
+ }
339
+ return reachable;
340
+ }
293
341
  const useScene = create()(temporal((set, get) => ({
294
342
  // 1. Flat dictionary of all nodes
295
343
  nodes: {},
@@ -325,9 +373,19 @@ const useScene = create()(temporal((set, get) => ({
325
373
  delete cleanedNodes[node.id];
326
374
  }
327
375
  }
376
+ const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds);
377
+ const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds);
378
+ if (normalizedRootNodeIds.length > 0) {
379
+ for (const node of Object.values(cleanedNodes)) {
380
+ if (reachableNodeIds.has(node.id))
381
+ continue;
382
+ console.warn('[Scene] Removing unreachable node', node.id);
383
+ delete cleanedNodes[node.id];
384
+ }
385
+ }
328
386
  set({
329
387
  nodes: cleanedNodes,
330
- rootNodeIds,
388
+ rootNodeIds: normalizedRootNodeIds,
331
389
  dirtyNodes: new Set(),
332
390
  collections: {},
333
391
  });
@@ -1 +1 @@
1
- {"version":3,"file":"stair-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAa,QAAQ,EAA+B,MAAM,cAAc,CAAA;AAulBrH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAIvC,SAAS;UAAQ,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IA6F5E"}
1
+ {"version":3,"file":"stair-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,WAAW,EAEX,QAAQ,EAGT,MAAM,cAAc,CAAA;AAitBrB,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAQvC,SAAS;UAAQ,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IAyG5E"}
@@ -1,8 +1,9 @@
1
1
  import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
2
2
  import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint';
3
- const CURVED_STAIR_SLAB_OPENING_RATIO = 0.8;
3
+ const CURVED_STAIR_SLAB_OPENING_RATIO = 0.9;
4
4
  const STRAIGHT_STAIR_TARGET_THRESHOLD_MIN = 0.35;
5
5
  const STAIR_SLAB_OPENING_TIGHTENING = 0;
6
+ const CURVED_STAIR_OPENING_STEP_PADDING = 3;
6
7
  function clamp(value, min, max) {
7
8
  return Math.min(max, Math.max(min, value));
8
9
  }
@@ -29,7 +30,8 @@ function polygonsEqual(left, right) {
29
30
  function metadataEqual(left, right) {
30
31
  if (left.length !== right.length)
31
32
  return false;
32
- return left.every((entry, index) => entry.source === right[index]?.source && (entry.stairId ?? null) === (right[index]?.stairId ?? null));
33
+ return left.every((entry, index) => entry.source === right[index]?.source &&
34
+ (entry.stairId ?? null) === (right[index]?.stairId ?? null));
33
35
  }
34
36
  function normalizeExistingMetadata(holes, metadata) {
35
37
  return holes.map((_, index) => metadata?.[index] ?? { source: 'manual' });
@@ -191,6 +193,35 @@ function polygonArea(points) {
191
193
  }
192
194
  return area / 2;
193
195
  }
196
+ function pointOnSegment(point, a, b, tolerance = 1e-6) {
197
+ const cross = (point[1] - a[1]) * (b[0] - a[0]) - (point[0] - a[0]) * (b[1] - a[1]);
198
+ if (Math.abs(cross) > tolerance)
199
+ return false;
200
+ const dot = (point[0] - a[0]) * (b[0] - a[0]) + (point[1] - a[1]) * (b[1] - a[1]);
201
+ if (dot < -tolerance)
202
+ return false;
203
+ const lenSq = (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2;
204
+ return dot <= lenSq + tolerance;
205
+ }
206
+ function pointInPolygon(point, polygon) {
207
+ if (polygon.length < 3)
208
+ return false;
209
+ let inside = false;
210
+ const [x, z] = point;
211
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
212
+ const a = polygon[i];
213
+ const b = polygon[j];
214
+ if (pointOnSegment(point, a, b))
215
+ return true;
216
+ const intersects = a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0];
217
+ if (intersects)
218
+ inside = !inside;
219
+ }
220
+ return inside;
221
+ }
222
+ function polygonContainsPolygon(outer, inner) {
223
+ return inner.every((point) => pointInPolygon(point, outer));
224
+ }
194
225
  function getAxisAlignedRectFromPolygon(polygon) {
195
226
  if (polygon.length < 4)
196
227
  return null;
@@ -288,30 +319,54 @@ function buildUnionPolygonsFromRects(rects) {
288
319
  }
289
320
  return polygons;
290
321
  }
291
- function getCurvedOpeningPolygon(stair) {
292
- const width = Math.max(stair.width ?? 1, 0.4);
293
- const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9);
294
- const outerRadius = innerRadius + width;
295
- const totalSweep = stair.sweepAngle ?? Math.PI / 2;
296
- const openingSweep = Math.sign(totalSweep || 1) *
297
- Math.max(Math.abs(totalSweep) * CURVED_STAIR_SLAB_OPENING_RATIO, Math.abs(totalSweep) / Math.max(stair.stepCount ?? 1, 1));
298
- const startAngle = totalSweep / 2 - openingSweep;
299
- const endAngle = totalSweep / 2;
300
- const segmentCount = Math.max(10, Math.min(32, Math.ceil(Math.abs(openingSweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5)));
322
+ function getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep) {
323
+ const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
324
+ const stepSweep = Math.abs(totalSweep) / stepCount;
325
+ const midRadius = Math.max((innerRadius + outerRadius) * 0.5, 0.01);
326
+ const treadDepth = Math.max(stepSweep * midRadius, 0.2);
327
+ return Math.min(stepCount, Math.max(1, Math.ceil(1.8 / treadDepth), Math.ceil(stepCount * CURVED_STAIR_SLAB_OPENING_RATIO)));
328
+ }
329
+ function buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle) {
330
+ const sweep = endAngle - startAngle;
331
+ const segmentCount = Math.max(10, Math.min(32, Math.ceil(Math.abs(sweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5)));
301
332
  const outerPoints = [];
302
333
  const innerPoints = [];
303
334
  for (let index = 0; index <= segmentCount; index++) {
304
335
  const t = index / segmentCount;
305
- const angle = startAngle + (endAngle - startAngle) * t;
336
+ const angle = startAngle + sweep * t;
306
337
  outerPoints.push(toWorldPlanPoint(stair, Math.cos(angle) * outerRadius, Math.sin(angle) * outerRadius));
307
338
  }
308
339
  for (let index = segmentCount; index >= 0; index--) {
309
340
  const t = index / segmentCount;
310
- const angle = startAngle + (endAngle - startAngle) * t;
341
+ const angle = startAngle + sweep * t;
311
342
  innerPoints.push(toWorldPlanPoint(stair, Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius));
312
343
  }
313
344
  return [...outerPoints, ...innerPoints];
314
345
  }
346
+ function getCurvedOpeningPolygon(stair, targetElevation) {
347
+ const width = Math.max(stair.width ?? 1, 0.4);
348
+ const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9);
349
+ const outerRadius = innerRadius + width;
350
+ const totalSweep = stair.sweepAngle ?? Math.PI / 2;
351
+ const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
352
+ const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount;
353
+ const stepSweep = totalSweep / stepCount;
354
+ const targetThreshold = Math.max(stepHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN);
355
+ const endAngle = totalSweep / 2;
356
+ const fallbackStartStepIndex = Math.max(0, stepCount - getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep));
357
+ let startStepIndex = fallbackStartStepIndex;
358
+ if (typeof targetElevation === 'number') {
359
+ for (let index = 0; index < stepCount; index += 1) {
360
+ const stepTopElevation = stepHeight * (index + 1);
361
+ if (stepTopElevation >= targetElevation - targetThreshold) {
362
+ startStepIndex = Math.max(0, Math.min(fallbackStartStepIndex, index - CURVED_STAIR_OPENING_STEP_PADDING));
363
+ break;
364
+ }
365
+ }
366
+ }
367
+ const startAngle = -totalSweep / 2 + stepSweep * startStepIndex;
368
+ return buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle);
369
+ }
315
370
  function getSpiralOpeningPolygon(stair) {
316
371
  const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4);
317
372
  const segmentCount = 48;
@@ -384,7 +439,7 @@ function getStairOpeningPolygons(stair, nodes, targetElevation) {
384
439
  return [];
385
440
  }
386
441
  if (stair.stairType === 'curved') {
387
- return [getCurvedOpeningPolygon(stair)];
442
+ return [getCurvedOpeningPolygon(stair, targetElevation)];
388
443
  }
389
444
  if (stair.stairType === 'spiral') {
390
445
  return [getSpiralOpeningPolygon(stair)];
@@ -412,7 +467,9 @@ function getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes
412
467
  if (fromLevel === undefined || ceilingLevel === undefined) {
413
468
  return ceiling.height ?? DEFAULT_WALL_HEIGHT;
414
469
  }
415
- return (ceilingLevel - fromLevel) * DEFAULT_WALL_HEIGHT + (ceiling.height ?? DEFAULT_WALL_HEIGHT) - (stair.position[1] ?? 0);
470
+ return ((ceilingLevel - fromLevel) * DEFAULT_WALL_HEIGHT +
471
+ (ceiling.height ?? DEFAULT_WALL_HEIGHT) -
472
+ (stair.position[1] ?? 0));
416
473
  }
417
474
  function shouldApplyStairToSlab(stair, slabLevelId, nodes) {
418
475
  const { fromLevelId, toLevelId } = getResolvedStairLevelIds(stair, nodes);
@@ -467,10 +524,12 @@ export function syncAutoStairOpenings(nodes) {
467
524
  source: 'stair',
468
525
  stairId: stair.id,
469
526
  },
470
- })));
527
+ })))
528
+ .filter((hole) => polygonContainsPolygon(slab.polygon, hole.polygon));
471
529
  const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
472
530
  const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
473
- if (!polygonsEqual(existingHoles, nextHoles) || !metadataEqual(existingMetadata, nextMetadata)) {
531
+ if (!polygonsEqual(existingHoles, nextHoles) ||
532
+ !metadataEqual(existingMetadata, nextMetadata)) {
474
533
  updates.push({
475
534
  id: slab.id,
476
535
  data: {
@@ -498,10 +557,12 @@ export function syncAutoStairOpenings(nodes) {
498
557
  source: 'stair',
499
558
  stairId: stair.id,
500
559
  },
501
- })));
560
+ })))
561
+ .filter((hole) => polygonContainsPolygon(ceiling.polygon, hole.polygon));
502
562
  const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
503
563
  const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
504
- if (!polygonsEqual(existingHoles, nextHoles) || !metadataEqual(existingMetadata, nextMetadata)) {
564
+ if (!polygonsEqual(existingHoles, nextHoles) ||
565
+ !metadataEqual(existingMetadata, nextMetadata)) {
505
566
  updates.push({
506
567
  id: ceiling.id,
507
568
  data: {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stair-opening-sync.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stair-opening-sync.test.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,65 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; core does not
2
+ // depend on @types/bun so the import type is unresolved at compile time.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { BuildingNode, LevelNode, SlabNode, StairNode, StairSegmentNode } from '../../schema';
5
+ import { syncAutoStairOpenings } from './stair-opening-sync';
6
+ describe('syncAutoStairOpenings', () => {
7
+ test('only applies stair holes to destination slabs that contain the opening', () => {
8
+ const building = BuildingNode.parse({ name: 'Building' });
9
+ const ground = LevelNode.parse({ name: 'Ground', level: 0, parentId: building.id });
10
+ const upper = LevelNode.parse({ name: 'Upper', level: 1, parentId: building.id });
11
+ const landingSlab = SlabNode.parse({
12
+ name: 'Landing Slab',
13
+ parentId: upper.id,
14
+ polygon: [
15
+ [0, 0],
16
+ [4, 0],
17
+ [4, 3],
18
+ [0, 3],
19
+ ],
20
+ });
21
+ const bedroomSlab = SlabNode.parse({
22
+ name: 'Bedroom Slab',
23
+ parentId: upper.id,
24
+ polygon: [
25
+ [4, 0],
26
+ [8, 0],
27
+ [8, 3],
28
+ [4, 3],
29
+ ],
30
+ });
31
+ const segment = StairSegmentNode.parse({
32
+ parentId: 'stair_main',
33
+ width: 1,
34
+ length: 2.6,
35
+ height: 2.5,
36
+ stepCount: 12,
37
+ });
38
+ const stair = StairNode.parse({
39
+ id: 'stair_main',
40
+ name: 'Main Stair',
41
+ parentId: ground.id,
42
+ position: [2, 0, 0.2],
43
+ stairType: 'straight',
44
+ fromLevelId: ground.id,
45
+ toLevelId: upper.id,
46
+ slabOpeningMode: 'destination',
47
+ children: [segment.id],
48
+ });
49
+ const nodes = Object.fromEntries([
50
+ building,
51
+ ground,
52
+ upper,
53
+ landingSlab,
54
+ bedroomSlab,
55
+ stair,
56
+ { ...segment, parentId: stair.id },
57
+ ].map((node) => [node.id, node]));
58
+ const updates = syncAutoStairOpenings(nodes);
59
+ const landingUpdate = updates.find((update) => update.id === landingSlab.id);
60
+ const bedroomUpdate = updates.find((update) => update.id === bedroomSlab.id);
61
+ expect(landingUpdate?.data.holes).toHaveLength(1);
62
+ expect(landingUpdate?.data.holeMetadata).toEqual([{ source: 'stair', stairId: stair.id }]);
63
+ expect(bedroomUpdate).toBeUndefined();
64
+ });
65
+ });
@@ -197,7 +197,7 @@ function generateStairSegmentGeometry(segment, absoluteHeight) {
197
197
  matrix.setPosition(width / 2, 0, 0);
198
198
  extrudedGeometry.applyMatrix4(matrix);
199
199
  extrudedGeometry.computeVertexNormals();
200
- const geometry = extrudedGeometry.toNonIndexed() ?? extrudedGeometry;
200
+ const geometry = extrudedGeometry.index ? extrudedGeometry.toNonIndexed() : extrudedGeometry;
201
201
  if (geometry !== extrudedGeometry) {
202
202
  extrudedGeometry.dispose();
203
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Core library for Pascal 3D building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,6 +15,36 @@
15
15
  "types": "./dist/utils/clone-scene-graph.d.ts",
16
16
  "import": "./dist/utils/clone-scene-graph.js",
17
17
  "default": "./dist/utils/clone-scene-graph.js"
18
+ },
19
+ "./schema": {
20
+ "types": "./dist/schema/index.d.ts",
21
+ "import": "./dist/schema/index.js",
22
+ "default": "./dist/schema/index.js"
23
+ },
24
+ "./store": {
25
+ "types": "./dist/store/use-scene.d.ts",
26
+ "import": "./dist/store/use-scene.js",
27
+ "default": "./dist/store/use-scene.js"
28
+ },
29
+ "./material-library": {
30
+ "types": "./dist/material-library.d.ts",
31
+ "import": "./dist/material-library.js",
32
+ "default": "./dist/material-library.js"
33
+ },
34
+ "./spatial-grid": {
35
+ "types": "./dist/hooks/spatial-grid/spatial-grid-manager.d.ts",
36
+ "import": "./dist/hooks/spatial-grid/spatial-grid-manager.js",
37
+ "default": "./dist/hooks/spatial-grid/spatial-grid-manager.js"
38
+ },
39
+ "./wall": {
40
+ "types": "./dist/systems/wall/wall-footprint.d.ts",
41
+ "import": "./dist/systems/wall/wall-footprint.js",
42
+ "default": "./dist/systems/wall/wall-footprint.js"
43
+ },
44
+ "./stair-openings": {
45
+ "types": "./dist/systems/stair/stair-opening-sync.d.ts",
46
+ "import": "./dist/systems/stair/stair-opening-sync.js",
47
+ "default": "./dist/systems/stair/stair-opening-sync.js"
18
48
  }
19
49
  },
20
50
  "files": [
@@ -37,8 +67,6 @@
37
67
  "idb-keyval": "^6.2.2",
38
68
  "mitt": "^3.0.1",
39
69
  "nanoid": "^5.1.6",
40
- "three-bvh-csg": "^0.0.18",
41
- "three-mesh-bvh": "^0.9.8",
42
70
  "zod": "^4.3.5",
43
71
  "zundo": "^2.3.0",
44
72
  "zustand": "^5"