@pascal-app/core 1.0.0-beta.4 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +12 -0
  2. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  3. package/dist/hooks/spatial-grid/spatial-grid-manager.js +39 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -1
  7. package/dist/lib/room-topology-index.d.ts +50 -0
  8. package/dist/lib/room-topology-index.d.ts.map +1 -0
  9. package/dist/lib/room-topology-index.js +237 -0
  10. package/dist/lib/space-detection.d.ts +29 -3
  11. package/dist/lib/space-detection.d.ts.map +1 -1
  12. package/dist/lib/space-detection.js +858 -242
  13. package/dist/registry/index.d.ts +3 -2
  14. package/dist/registry/index.d.ts.map +1 -1
  15. package/dist/registry/index.js +2 -1
  16. package/dist/registry/registry.d.ts +17 -1
  17. package/dist/registry/registry.d.ts.map +1 -1
  18. package/dist/registry/registry.js +74 -0
  19. package/dist/registry/types.d.ts +35 -0
  20. package/dist/registry/types.d.ts.map +1 -1
  21. package/dist/registry/use-registry-version.d.ts +11 -0
  22. package/dist/registry/use-registry-version.d.ts.map +1 -0
  23. package/dist/registry/use-registry-version.js +15 -0
  24. package/dist/schema/index.d.ts +1 -0
  25. package/dist/schema/index.d.ts.map +1 -1
  26. package/dist/schema/index.js +2 -0
  27. package/dist/schema/nodes/level.d.ts +3 -0
  28. package/dist/schema/nodes/level.d.ts.map +1 -1
  29. package/dist/schema/nodes/level.js +9 -0
  30. package/dist/schema/types.d.ts +1 -0
  31. package/dist/schema/types.d.ts.map +1 -1
  32. package/dist/services/index.d.ts +1 -1
  33. package/dist/services/index.d.ts.map +1 -1
  34. package/dist/services/index.js +1 -1
  35. package/dist/services/storey.d.ts +3 -10
  36. package/dist/services/storey.d.ts.map +1 -1
  37. package/dist/services/storey.js +45 -15
  38. package/dist/store/actions/node-actions.d.ts +8 -4
  39. package/dist/store/actions/node-actions.d.ts.map +1 -1
  40. package/dist/store/actions/node-actions.js +31 -4
  41. package/dist/store/history-control.d.ts +5 -0
  42. package/dist/store/history-control.d.ts.map +1 -1
  43. package/dist/store/history-control.js +68 -6
  44. package/dist/store/use-scene.d.ts.map +1 -1
  45. package/dist/store/use-scene.js +75 -163
  46. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  47. package/dist/systems/elevator/elevator-service.js +16 -11
  48. package/dist/systems/slab/slab-support.d.ts +0 -31
  49. package/dist/systems/slab/slab-support.d.ts.map +1 -1
  50. package/dist/systems/slab/slab-support.js +26 -4
  51. package/dist/systems/stair/stair-opening-preview.d.ts +1 -0
  52. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  53. package/dist/systems/stair/stair-rise.d.ts.map +1 -1
  54. package/dist/systems/stair/stair-rise.js +4 -2
  55. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  56. package/dist/systems/wall/wall-mitering.js +88 -9
  57. package/dist/systems/wall/wall-topology.d.ts +53 -0
  58. package/dist/systems/wall/wall-topology.d.ts.map +1 -0
  59. package/dist/systems/wall/wall-topology.js +414 -0
  60. package/dist/utils/clone-scene-graph.d.ts +2 -0
  61. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  62. package/dist/utils/clone-scene-graph.js +13 -2
  63. package/dist/utils/scene-migrations.d.ts +1 -0
  64. package/dist/utils/scene-migrations.d.ts.map +1 -1
  65. package/dist/utils/scene-migrations.js +1 -0
  66. package/dist/utils/vertical-scene-migration.d.ts +13 -0
  67. package/dist/utils/vertical-scene-migration.d.ts.map +1 -0
  68. package/dist/utils/vertical-scene-migration.js +179 -0
  69. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import { GROUND_SUPPORT_ID } from '../hooks/spatial-grid/support-host-id';
2
+ import { deriveLegacyLevelHeight } from '../services/level-height';
3
+ import { getCeilingClampBound } from '../services/storey';
4
+ import { computeWallSlabSupport } from '../systems/slab/slab-support';
5
+ import { DEFAULT_WALL_HEIGHT } from '../systems/wall/wall-footprint';
6
+ function getFiniteNumber(value, fallback) {
7
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
8
+ }
9
+ function getStringArray(value) {
10
+ return Array.isArray(value)
11
+ ? value.filter((entry) => typeof entry === 'string')
12
+ : [];
13
+ }
14
+ // Walls whose top lands within this of the storey plane become plane-bound;
15
+ // ceilings whose stored height lands within this of their clamp bound become
16
+ // follows-mode. The strict comparison preserves intentional 0.20-short walls.
17
+ const PLANE_BOUND_EPSILON = 0.2;
18
+ // A ground-pinned wall counts as buried only when the elected slab's occupied
19
+ // interval strictly straddles the pinned base. The tolerance absorbs float
20
+ // drift in stamped offsets without capturing a slab that merely touches the
21
+ // base from above.
22
+ const BURIED_PIN_EPSILON = 1e-3;
23
+ /**
24
+ * Applies the vertical-model load migration to serialized scene nodes.
25
+ *
26
+ * This must remain pure, idempotent, and server-safe: the editor loader and
27
+ * hosted scene authority both call it so they compare and persist the same
28
+ * canonical fields during collaboration.
29
+ */
30
+ export function migrateVerticalSceneNodes(sourceNodes) {
31
+ const nodes = { ...sourceNodes };
32
+ let changed = false;
33
+ const replaceNode = (id, node) => {
34
+ nodes[id] = node;
35
+ changed = true;
36
+ };
37
+ // A level without `height` marks a scene saved before the vertical model
38
+ // landed. Compute the gate before mutating anything so stair and ceiling
39
+ // intent on already-migrated scenes is never reclassified.
40
+ const isLegacyScene = Object.values(nodes).some((node) => node?.type === 'level' && !('height' in node));
41
+ // Ordinals are semantic and compact independently within each building.
42
+ const buildingNodes = Object.values(nodes).filter((node) => node?.type === 'building');
43
+ const levelsByBuilding = new Map();
44
+ for (const [id, node] of Object.entries(nodes)) {
45
+ if (node?.type !== 'level')
46
+ continue;
47
+ const buildingId = buildingNodes.find((building) => building.id === node.parentId)?.id ??
48
+ buildingNodes.find((building) => getStringArray(building.children).includes(id))?.id ??
49
+ null;
50
+ const bucket = levelsByBuilding.get(buildingId) ?? [];
51
+ bucket.push({ id, ordinal: getFiniteNumber(node.level, 0) });
52
+ levelsByBuilding.set(buildingId, bucket);
53
+ }
54
+ for (const bucket of levelsByBuilding.values()) {
55
+ const sorted = [...bucket].sort((a, b) => a.ordinal - b.ordinal);
56
+ const negativeCount = sorted.filter((entry) => entry.ordinal < 0).length;
57
+ sorted.forEach((entry, index) => {
58
+ const nextOrdinal = index - negativeCount;
59
+ const current = nodes[entry.id];
60
+ if (current.level !== nextOrdinal) {
61
+ replaceNode(entry.id, { ...current, level: nextOrdinal });
62
+ }
63
+ });
64
+ }
65
+ // Materialize exact legacy storey planes before classifying wall tops.
66
+ const legacyLevelIds = Object.entries(nodes)
67
+ .filter(([, node]) => node?.type === 'level' && !('height' in node))
68
+ .map(([id]) => id);
69
+ const derivedHeights = new Map();
70
+ for (const levelId of legacyLevelIds) {
71
+ derivedHeights.set(levelId, deriveLegacyLevelHeight(levelId, nodes));
72
+ }
73
+ for (const levelId of legacyLevelIds) {
74
+ const plane = derivedHeights.get(levelId);
75
+ const level = nodes[levelId];
76
+ replaceNode(levelId, { ...level, height: plane });
77
+ const children = getStringArray(level.children)
78
+ .map((childId) => nodes[childId])
79
+ .filter((child) => child !== undefined);
80
+ const slabs = children.filter((child) => child.type === 'slab');
81
+ const walls = children.filter((child) => child.type === 'wall');
82
+ for (const wall of walls) {
83
+ const electedBase = computeWallSlabSupport({
84
+ start: wall.start,
85
+ end: wall.end,
86
+ curveOffset: wall.curveOffset,
87
+ thickness: wall.thickness,
88
+ }, slabs, walls).elevation;
89
+ const effectiveHeight = wall.height ?? DEFAULT_WALL_HEIGHT;
90
+ const top = Math.max(0, electedBase) + effectiveHeight;
91
+ if (Math.abs(plane - top) < PLANE_BOUND_EPSILON) {
92
+ if ('height' in wall) {
93
+ const { height: _height, ...planeBound } = wall;
94
+ replaceNode(wall.id, planeBound);
95
+ }
96
+ }
97
+ else if (wall.height !== effectiveHeight) {
98
+ replaceNode(wall.id, { ...wall, height: effectiveHeight });
99
+ }
100
+ }
101
+ }
102
+ if (isLegacyScene) {
103
+ for (const [id, node] of Object.entries(nodes)) {
104
+ if (node?.type !== 'stair' || node.totalRise !== 2.5)
105
+ continue;
106
+ const { totalRise: _totalRise, ...derivedRise } = node;
107
+ replaceNode(id, derivedRise);
108
+ }
109
+ }
110
+ // Preserve the exact occupied interval of legacy slabs.
111
+ for (const [id, node] of Object.entries(nodes)) {
112
+ if (node?.type !== 'slab' || 'thickness' in node)
113
+ continue;
114
+ const elevation = getFiniteNumber(node.elevation, 0.05);
115
+ replaceNode(id, elevation < 0
116
+ ? { ...node, thickness: 0.05, recessed: true }
117
+ : { ...node, thickness: elevation });
118
+ }
119
+ if (isLegacyScene) {
120
+ for (const [id, node] of Object.entries(nodes)) {
121
+ if (node?.type !== 'ceiling' || !('height' in node))
122
+ continue;
123
+ const dropHeight = () => {
124
+ const { height: _height, ...follows } = node;
125
+ replaceNode(id, follows);
126
+ };
127
+ if (node.autoFromWalls === true) {
128
+ dropHeight();
129
+ continue;
130
+ }
131
+ if (typeof node.parentId !== 'string')
132
+ continue;
133
+ const bound = getCeilingClampBound(node.parentId, nodes, Array.isArray(node.polygon) ? node.polygon : []);
134
+ const stored = getFiniteNumber(node.height, Number.NaN);
135
+ if (Number.isFinite(bound) && Math.abs(stored - bound) < PLANE_BOUND_EPSILON) {
136
+ dropHeight();
137
+ }
138
+ }
139
+ }
140
+ // Terrain-sculpt-era drafting stamped `supportSlabId: 'ground'` onto walls
141
+ // drawn in 3D on flat scenes. The pin short-circuits slab election, so a
142
+ // wall whose feet sit inside a floor slab keeps its base at the level floor
143
+ // — buried in the slab, z-fighting its side faces — while the panel shows
144
+ // the base as automatic. Heal the pin only in that buried state: a deck
145
+ // hovering above an intentionally grounded wall must keep its pin (dropping
146
+ // it would lift the wall onto the deck). Scenes with sculpted terrain are
147
+ // skipped wholesale — a ground host is load-bearing there, and the live
148
+ // terrain field isn't visible to this pure pass.
149
+ const hasSculptedTerrain = Object.values(nodes).some((node) => node?.type === 'site' && node.terrain != null);
150
+ if (!hasSculptedTerrain) {
151
+ for (const [id, node] of Object.entries(nodes)) {
152
+ if (node?.type !== 'wall' || node.supportSlabId !== GROUND_SUPPORT_ID)
153
+ continue;
154
+ const levelId = typeof node.parentId === 'string' ? node.parentId : null;
155
+ if (!levelId || nodes[levelId]?.type !== 'level')
156
+ continue;
157
+ const siblings = Object.values(nodes).filter((sibling) => sibling != null && sibling.parentId === levelId);
158
+ const support = computeWallSlabSupport({
159
+ start: node.start,
160
+ end: node.end,
161
+ curveOffset: node.curveOffset,
162
+ thickness: node.thickness,
163
+ }, siblings.filter((sibling) => sibling.type === 'slab'), siblings.filter((sibling) => sibling.type === 'wall'));
164
+ const elected = support.electedSlabId ? nodes[support.electedSlabId] : null;
165
+ if (!elected)
166
+ continue;
167
+ const pinnedBase = getFiniteNumber(node.supportOffset, 0);
168
+ const electedTop = getFiniteNumber(elected.elevation, 0.05);
169
+ const electedBottom = electedTop - getFiniteNumber(elected.thickness, 0.05);
170
+ const buried = electedBottom <= pinnedBase + BURIED_PIN_EPSILON &&
171
+ electedTop > pinnedBase + BURIED_PIN_EPSILON;
172
+ if (!buried)
173
+ continue;
174
+ const { supportSlabId: _host, supportOffset: _offset, ...healed } = node;
175
+ replaceNode(id, healed);
176
+ }
177
+ }
178
+ return changed ? { changed, nodes } : { changed, nodes: sourceNodes };
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/core",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "Core library for Pascal 3D building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",