@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.
- package/dist/events/bus.d.ts +38 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +2 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +164 -6
- package/dist/index.d.ts +8 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -14
- package/dist/lib/door-operation.d.ts +7 -0
- package/dist/lib/door-operation.d.ts.map +1 -0
- package/dist/lib/door-operation.js +25 -0
- package/dist/lib/slab-polygon.d.ts +3 -0
- package/dist/lib/slab-polygon.d.ts.map +1 -0
- package/dist/lib/slab-polygon.js +58 -0
- package/dist/material-library.d.ts +5 -3
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +26 -49
- package/dist/schema/asset-url.d.ts +34 -0
- package/dist/schema/asset-url.d.ts.map +1 -0
- package/dist/schema/asset-url.js +79 -0
- package/dist/schema/asset-url.test.d.ts +2 -0
- package/dist/schema/asset-url.test.d.ts.map +1 -0
- package/dist/schema/asset-url.test.js +138 -0
- package/dist/schema/index.d.ts +7 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +5 -3
- package/dist/schema/material.d.ts +3 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +13 -11
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/column.d.ts +520 -0
- package/dist/schema/nodes/column.d.ts.map +1 -0
- package/dist/schema/nodes/column.js +385 -0
- package/dist/schema/nodes/door.d.ts +73 -1
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +39 -2
- package/dist/schema/nodes/fence.d.ts +1 -1
- package/dist/schema/nodes/guide.d.ts +17 -0
- package/dist/schema/nodes/guide.d.ts.map +1 -1
- package/dist/schema/nodes/guide.js +11 -1
- package/dist/schema/nodes/item.d.ts +8 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +18 -1
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +6 -0
- package/dist/schema/nodes/roof-segment.d.ts +1 -1
- package/dist/schema/nodes/roof.d.ts +4 -4
- package/dist/schema/nodes/scan.d.ts.map +1 -1
- package/dist/schema/nodes/scan.js +2 -1
- package/dist/schema/nodes/site.d.ts +1 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -1
- package/dist/schema/nodes/spawn.d.ts +24 -0
- package/dist/schema/nodes/spawn.d.ts.map +1 -0
- package/dist/schema/nodes/spawn.js +8 -0
- package/dist/schema/nodes/stair-segment.d.ts +1 -1
- package/dist/schema/nodes/stair.d.ts +8 -8
- package/dist/schema/nodes/wall.d.ts +3 -3
- package/dist/schema/nodes/window.d.ts +56 -1
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +29 -0
- package/dist/schema/types.d.ts +324 -21
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +6 -5
- package/dist/store/use-interactive.d.ts +43 -0
- package/dist/store/use-interactive.d.ts.map +1 -1
- package/dist/store/use-interactive.js +66 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +60 -2
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +81 -20
- package/dist/systems/stair/stair-opening-sync.test.d.ts +2 -0
- package/dist/systems/stair/stair-opening-sync.test.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-sync.test.js +65 -0
- package/dist/systems/stair/stair-system.js +1 -1
- package/package.json +31 -3
package/dist/schema/types.js
CHANGED
|
@@ -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,
|
|
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:
|
|
160
|
+
parentId: effectiveParentId,
|
|
160
161
|
};
|
|
161
162
|
nextNodes[newNode.id] = newNode;
|
|
162
163
|
// 2. Update the Parent's children list
|
|
163
|
-
if (
|
|
164
|
-
const parent = nextNodes[
|
|
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[
|
|
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 (!
|
|
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;
|
|
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;
|
|
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"}
|
package/dist/store/use-scene.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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 &&
|
|
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
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
const segmentCount = Math.max(10, Math.min(32, Math.ceil(Math.abs(
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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) ||
|
|
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) ||
|
|
564
|
+
if (!polygonsEqual(existingHoles, nextHoles) ||
|
|
565
|
+
!metadataEqual(existingMetadata, nextMetadata)) {
|
|
505
566
|
updates.push({
|
|
506
567
|
id: ceiling.id,
|
|
507
568
|
data: {
|
|
@@ -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()
|
|
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.
|
|
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"
|