@pascal-app/core 1.0.0-beta.3 → 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 (72) 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 +86 -172
  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/heal-scene-graph.d.ts +5 -0
  64. package/dist/utils/heal-scene-graph.d.ts.map +1 -1
  65. package/dist/utils/heal-scene-graph.js +46 -1
  66. package/dist/utils/scene-migrations.d.ts +4 -0
  67. package/dist/utils/scene-migrations.d.ts.map +1 -0
  68. package/dist/utils/scene-migrations.js +7 -0
  69. package/dist/utils/vertical-scene-migration.d.ts +13 -0
  70. package/dist/utils/vertical-scene-migration.d.ts.map +1 -0
  71. package/dist/utils/vertical-scene-migration.js +179 -0
  72. package/package.json +4 -4
@@ -13,6 +13,11 @@
13
13
  // duplicate reference renders the child twice (duplicate React keys in the
14
14
  // 2D plan, doubled hosted geometry in 3D). Same-array duplicates are
15
15
  // collapsed too.
16
+ // 4. A node with a null `parentId` that exactly one parent still claims via
17
+ // `children` (the legacy site-child flatten and the old default-scene
18
+ // assembler both linked children without writing `parentId`). The editor
19
+ // renders the chain through `children`, but the hosted scene authority
20
+ // validates parent/child symmetry and rejects the whole scene.
16
21
  //
17
22
  // All are also prevented at the source now; this is the load-time safety net
18
23
  // for already-saved scenes.
@@ -102,5 +107,45 @@ export function healSceneNodes(input) {
102
107
  }
103
108
  nodes[id] = node;
104
109
  }
105
- return { nodes, droppedWallIds, strippedChildRefs, strippedStaleChildRefs };
110
+ // Pass 3: repair null parent links. A node claimed as a child by exactly one
111
+ // parent must point back at it; legacy writers linked `children` without
112
+ // writing `parentId`. Embedded legacy site children claim by their `id` so
113
+ // the flattened flat-map node is repaired too.
114
+ const claimantsByChildId = new Map();
115
+ for (const [id, node] of Object.entries(nodes)) {
116
+ const children = node?.children;
117
+ if (!Array.isArray(children))
118
+ continue;
119
+ for (const child of children) {
120
+ const childId = typeof child === 'string'
121
+ ? child
122
+ : child && typeof child === 'object' && typeof child.id === 'string'
123
+ ? child.id
124
+ : null;
125
+ if (!childId || !(childId in nodes))
126
+ continue;
127
+ const claimants = claimantsByChildId.get(childId) ?? [];
128
+ claimants.push(id);
129
+ claimantsByChildId.set(childId, claimants);
130
+ }
131
+ }
132
+ const repairedParentLinkNodeIds = [];
133
+ for (const [id, node] of Object.entries(nodes)) {
134
+ if (!node || typeof node !== 'object')
135
+ continue;
136
+ if (node.parentId != null)
137
+ continue;
138
+ const claimants = claimantsByChildId.get(id);
139
+ if (claimants?.length !== 1 || claimants[0] === id)
140
+ continue;
141
+ nodes[id] = { ...node, parentId: claimants[0] };
142
+ repairedParentLinkNodeIds.push(id);
143
+ }
144
+ return {
145
+ nodes,
146
+ droppedWallIds,
147
+ strippedChildRefs,
148
+ strippedStaleChildRefs,
149
+ repairedParentLinkNodeIds,
150
+ };
106
151
  }
@@ -0,0 +1,4 @@
1
+ export { type HealSceneResult, healSceneNodes } from './heal-scene-graph';
2
+ export { type RetiredSceneNodeMigration, removeRetiredDrawingSheetNodes, } from './retired-scene-nodes';
3
+ export { migrateVerticalSceneNodes, type VerticalSceneMigration, } from './vertical-scene-migration';
4
+ //# sourceMappingURL=scene-migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-migrations.d.ts","sourceRoot":"","sources":["../../src/utils/scene-migrations.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EACL,KAAK,yBAAyB,EAC9B,8BAA8B,GAC/B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,yBAAyB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,4BAA4B,CAAA"}
@@ -0,0 +1,7 @@
1
+ // Server-safe scene migrations shared by the client loader and the hosted
2
+ // scene authority. Everything exported here must stay pure data logic with no
3
+ // store, React, or Three.js imports so it can run in Server Components and
4
+ // API routes.
5
+ export { healSceneNodes } from './heal-scene-graph';
6
+ export { removeRetiredDrawingSheetNodes, } from './retired-scene-nodes';
7
+ export { migrateVerticalSceneNodes, } from './vertical-scene-migration';
@@ -0,0 +1,13 @@
1
+ export type VerticalSceneMigration = {
2
+ changed: boolean;
3
+ nodes: Record<string, unknown>;
4
+ };
5
+ /**
6
+ * Applies the vertical-model load migration to serialized scene nodes.
7
+ *
8
+ * This must remain pure, idempotent, and server-safe: the editor loader and
9
+ * hosted scene authority both call it so they compare and persist the same
10
+ * canonical fields during collaboration.
11
+ */
12
+ export declare function migrateVerticalSceneNodes(sourceNodes: Record<string, unknown>): VerticalSceneMigration;
13
+ //# sourceMappingURL=vertical-scene-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vertical-scene-migration.d.ts","sourceRoot":"","sources":["../../src/utils/vertical-scene-migration.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B,CAAA;AAuBD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,sBAAsB,CA+KxB"}
@@ -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.3",
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",
@@ -17,9 +17,9 @@
17
17
  "default": "./dist/utils/clone-scene-graph.js"
18
18
  },
19
19
  "./scene-migrations": {
20
- "types": "./dist/utils/retired-scene-nodes.d.ts",
21
- "import": "./dist/utils/retired-scene-nodes.js",
22
- "default": "./dist/utils/retired-scene-nodes.js"
20
+ "types": "./dist/utils/scene-migrations.d.ts",
21
+ "import": "./dist/utils/scene-migrations.js",
22
+ "default": "./dist/utils/scene-migrations.js"
23
23
  },
24
24
  "./registry": {
25
25
  "types": "./dist/registry/index.d.ts",