@pascal-app/core 0.1.3

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 (103) hide show
  1. package/dist/events/bus.d.ts +42 -0
  2. package/dist/events/bus.d.ts.map +1 -0
  3. package/dist/events/bus.js +13 -0
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +18 -0
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -0
  6. package/dist/hooks/scene-registry/scene-registry.js +35 -0
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +90 -0
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -0
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +466 -0
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +4 -0
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -0
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +115 -0
  13. package/dist/hooks/spatial-grid/spatial-grid.d.ts +23 -0
  14. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/spatial-grid.js +115 -0
  16. package/dist/hooks/spatial-grid/use-spatial-query.d.ts +16 -0
  17. package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/use-spatial-query.js +14 -0
  19. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +47 -0
  20. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -0
  21. package/dist/hooks/spatial-grid/wall-spatial-grid.js +113 -0
  22. package/dist/index.d.ts +17 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +22 -0
  25. package/dist/lib/asset-storage.d.ts +11 -0
  26. package/dist/lib/asset-storage.d.ts.map +1 -0
  27. package/dist/lib/asset-storage.js +48 -0
  28. package/dist/lib/space-detection.d.ts +34 -0
  29. package/dist/lib/space-detection.d.ts.map +1 -0
  30. package/dist/lib/space-detection.js +499 -0
  31. package/dist/schema/base.d.ts +30 -0
  32. package/dist/schema/base.d.ts.map +1 -0
  33. package/dist/schema/base.js +25 -0
  34. package/dist/schema/camera.d.ts +13 -0
  35. package/dist/schema/camera.d.ts.map +1 -0
  36. package/dist/schema/camera.js +9 -0
  37. package/dist/schema/index.d.ts +17 -0
  38. package/dist/schema/index.d.ts.map +1 -0
  39. package/dist/schema/index.js +18 -0
  40. package/dist/schema/nodes/building.d.ts +25 -0
  41. package/dist/schema/nodes/building.d.ts.map +1 -0
  42. package/dist/schema/nodes/building.js +16 -0
  43. package/dist/schema/nodes/ceiling.d.ts +25 -0
  44. package/dist/schema/nodes/ceiling.d.ts.map +1 -0
  45. package/dist/schema/nodes/ceiling.js +16 -0
  46. package/dist/schema/nodes/guide.d.ts +27 -0
  47. package/dist/schema/nodes/guide.d.ts.map +1 -0
  48. package/dist/schema/nodes/guide.js +11 -0
  49. package/dist/schema/nodes/item.d.ts +65 -0
  50. package/dist/schema/nodes/item.d.ts.map +1 -0
  51. package/dist/schema/nodes/item.js +38 -0
  52. package/dist/schema/nodes/level.d.ts +24 -0
  53. package/dist/schema/nodes/level.d.ts.map +1 -0
  54. package/dist/schema/nodes/level.js +21 -0
  55. package/dist/schema/nodes/roof.d.ts +28 -0
  56. package/dist/schema/nodes/roof.d.ts.map +1 -0
  57. package/dist/schema/nodes/roof.js +28 -0
  58. package/dist/schema/nodes/scan.d.ts +27 -0
  59. package/dist/schema/nodes/scan.d.ts.map +1 -0
  60. package/dist/schema/nodes/scan.js +11 -0
  61. package/dist/schema/nodes/site.d.ts +90 -0
  62. package/dist/schema/nodes/site.d.ts.map +1 -0
  63. package/dist/schema/nodes/site.js +39 -0
  64. package/dist/schema/nodes/slab.d.ts +24 -0
  65. package/dist/schema/nodes/slab.d.ts.map +1 -0
  66. package/dist/schema/nodes/slab.js +15 -0
  67. package/dist/schema/nodes/wall.d.ts +37 -0
  68. package/dist/schema/nodes/wall.d.ts.map +1 -0
  69. package/dist/schema/nodes/wall.js +30 -0
  70. package/dist/schema/nodes/zone.d.ts +24 -0
  71. package/dist/schema/nodes/zone.d.ts.map +1 -0
  72. package/dist/schema/nodes/zone.js +22 -0
  73. package/dist/schema/types.d.ts +339 -0
  74. package/dist/schema/types.d.ts.map +1 -0
  75. package/dist/schema/types.js +25 -0
  76. package/dist/store/actions/node-actions.d.ts +12 -0
  77. package/dist/store/actions/node-actions.d.ts.map +1 -0
  78. package/dist/store/actions/node-actions.js +121 -0
  79. package/dist/store/use-scene.d.ts +31 -0
  80. package/dist/store/use-scene.d.ts.map +1 -0
  81. package/dist/store/use-scene.js +127 -0
  82. package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
  83. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
  84. package/dist/systems/ceiling/ceiling-system.js +65 -0
  85. package/dist/systems/item/item-system.d.ts +2 -0
  86. package/dist/systems/item/item-system.d.ts.map +1 -0
  87. package/dist/systems/item/item-system.js +43 -0
  88. package/dist/systems/roof/roof-system.d.ts +8 -0
  89. package/dist/systems/roof/roof-system.d.ts.map +1 -0
  90. package/dist/systems/roof/roof-system.js +254 -0
  91. package/dist/systems/slab/slab-system.d.ts +8 -0
  92. package/dist/systems/slab/slab-system.d.ts.map +1 -0
  93. package/dist/systems/slab/slab-system.js +117 -0
  94. package/dist/systems/wall/wall-mitering.d.ts +32 -0
  95. package/dist/systems/wall/wall-mitering.d.ts.map +1 -0
  96. package/dist/systems/wall/wall-mitering.js +214 -0
  97. package/dist/systems/wall/wall-system.d.ts +12 -0
  98. package/dist/systems/wall/wall-system.d.ts.map +1 -0
  99. package/dist/systems/wall/wall-system.js +286 -0
  100. package/dist/utils/types.d.ts +6 -0
  101. package/dist/utils/types.d.ts.map +1 -0
  102. package/dist/utils/types.js +7 -0
  103. package/package.json +58 -0
@@ -0,0 +1,127 @@
1
+ 'use client';
2
+ import { temporal } from 'zundo';
3
+ import { create } from 'zustand';
4
+ import { persist } from 'zustand/middleware';
5
+ import { BuildingNode } from '../schema';
6
+ import { LevelNode } from '../schema/nodes/level';
7
+ import { isObject } from '../utils/types';
8
+ import * as nodeActions from './actions/node-actions';
9
+ const useScene = create()(persist(temporal((set, get) => ({
10
+ // 1. Flat dictionary of all nodes
11
+ nodes: {},
12
+ // 2. Root node IDs
13
+ rootNodeIds: [],
14
+ // 3. Dirty set
15
+ dirtyNodes: new Set(),
16
+ clearScene: () => {
17
+ set({
18
+ nodes: {},
19
+ rootNodeIds: [],
20
+ dirtyNodes: new Set(),
21
+ });
22
+ get().loadScene(); // Default scene
23
+ },
24
+ setScene: (nodes, rootNodeIds) => {
25
+ set({
26
+ nodes,
27
+ rootNodeIds,
28
+ dirtyNodes: new Set(),
29
+ });
30
+ // Mark all nodes as dirty to trigger re-validation
31
+ Object.values(nodes).forEach((node) => {
32
+ get().markDirty(node.id);
33
+ });
34
+ },
35
+ loadScene: () => {
36
+ if (get().rootNodeIds.length > 0) {
37
+ // Assign all nodes as dirty to force re-validation
38
+ Object.values(get().nodes).forEach((node) => {
39
+ get().markDirty(node.id);
40
+ });
41
+ return; // Scene already loaded
42
+ }
43
+ const building = BuildingNode.parse({
44
+ children: [],
45
+ });
46
+ const level0 = LevelNode.parse({
47
+ level: 0,
48
+ children: [],
49
+ });
50
+ building.children.push(level0.id);
51
+ // Define all nodes flat
52
+ const nodes = {
53
+ [building.id]: building,
54
+ [level0.id]: level0,
55
+ };
56
+ // Root nodes are the levels
57
+ const rootNodeIds = [building.id];
58
+ set({ nodes, rootNodeIds });
59
+ },
60
+ markDirty: (id) => {
61
+ get().dirtyNodes.add(id);
62
+ },
63
+ clearDirty: (id) => {
64
+ get().dirtyNodes.delete(id);
65
+ },
66
+ createNodes: (ops) => nodeActions.createNodesAction(set, get, ops),
67
+ createNode: (node, parentId) => nodeActions.createNodesAction(set, get, [{ node, parentId }]),
68
+ updateNodes: (updates) => nodeActions.updateNodesAction(set, get, updates),
69
+ updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]),
70
+ // --- DELETE ---
71
+ deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids),
72
+ deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]),
73
+ }), {
74
+ partialize: (state) => {
75
+ const { nodes, rootNodeIds } = state; // Only track nodes and rootNodeIds in history
76
+ return { nodes, rootNodeIds };
77
+ },
78
+ limit: 50, // Limit to last 50 actions
79
+ }), {
80
+ name: 'editor-storage',
81
+ partialize: (state) => ({
82
+ nodes: Object.fromEntries(Object.entries(state.nodes).filter(([_, node]) => {
83
+ const meta = node.metadata;
84
+ const isTransient = isObject(meta) && 'isTransient' in meta && meta.isTransient === true;
85
+ return !isTransient;
86
+ })),
87
+ rootNodeIds: state.rootNodeIds,
88
+ }),
89
+ onRehydrateStorage: (state) => {
90
+ console.log('hydrating...');
91
+ // optional
92
+ return (state, error) => {
93
+ if (error) {
94
+ console.log('an error happened during hydration', error);
95
+ }
96
+ else {
97
+ console.log('hydration finished');
98
+ }
99
+ };
100
+ },
101
+ }));
102
+ export default useScene;
103
+ // Track previous temporal state lengths
104
+ let prevPastLength = 0;
105
+ let prevFutureLength = 0;
106
+ // Subscribe to the temporal store (Undo/Redo events)
107
+ useScene.temporal.subscribe((state) => {
108
+ const currentPastLength = state.pastStates.length;
109
+ const currentFutureLength = state.futureStates.length;
110
+ // Undo: futureStates increases (state moved from past to future)
111
+ // Redo: pastStates increases while futureStates decreases (state moved from future to past)
112
+ const didUndo = currentFutureLength > prevFutureLength;
113
+ const didRedo = currentPastLength > prevPastLength && currentFutureLength < prevFutureLength;
114
+ if (didUndo || didRedo) {
115
+ // Use RAF to ensure all middleware and store updates are complete
116
+ requestAnimationFrame(() => {
117
+ const currentNodes = useScene.getState().nodes;
118
+ // Trigger a full scene re-validation after undo/redo
119
+ Object.values(currentNodes).forEach((node) => {
120
+ useScene.getState().markDirty(node.id);
121
+ });
122
+ });
123
+ }
124
+ // Update tracked lengths
125
+ prevPastLength = currentPastLength;
126
+ prevFutureLength = currentFutureLength;
127
+ });
@@ -0,0 +1,8 @@
1
+ import * as THREE from 'three';
2
+ import type { CeilingNode } from '../../schema';
3
+ export declare const CeilingSystem: () => null;
4
+ /**
5
+ * Generates flat ceiling geometry from polygon (no extrusion)
6
+ */
7
+ export declare function generateCeilingGeometry(ceilingNode: CeilingNode): THREE.BufferGeometry;
8
+ //# sourceMappingURL=ceiling-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,cAAc,CAAA;AAO1D,eAAO,MAAM,aAAa,YAuBzB,CAAA;AAeD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,GAAG,KAAK,CAAC,cAAc,CA6BtF"}
@@ -0,0 +1,65 @@
1
+ import { useFrame } from '@react-three/fiber';
2
+ import * as THREE from 'three';
3
+ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
4
+ import useScene from '../../store/use-scene';
5
+ // ============================================================================
6
+ // CEILING SYSTEM
7
+ // ============================================================================
8
+ export const CeilingSystem = () => {
9
+ const dirtyNodes = useScene((state) => state.dirtyNodes);
10
+ const clearDirty = useScene((state) => state.clearDirty);
11
+ useFrame(() => {
12
+ if (dirtyNodes.size === 0)
13
+ return;
14
+ const nodes = useScene.getState().nodes;
15
+ // Process dirty ceilings
16
+ dirtyNodes.forEach((id) => {
17
+ const node = nodes[id];
18
+ if (!node || node.type !== 'ceiling')
19
+ return;
20
+ const mesh = sceneRegistry.nodes.get(id);
21
+ if (mesh) {
22
+ updateCeilingGeometry(node, mesh);
23
+ clearDirty(id);
24
+ }
25
+ // If mesh not found, keep it dirty for next frame
26
+ });
27
+ });
28
+ return null;
29
+ };
30
+ /**
31
+ * Updates the geometry for a single ceiling
32
+ */
33
+ function updateCeilingGeometry(node, mesh) {
34
+ const newGeo = generateCeilingGeometry(node);
35
+ mesh.geometry.dispose();
36
+ mesh.geometry = newGeo;
37
+ // Position at the ceiling height
38
+ mesh.position.y = (node.height ?? 2.5) - 0.01; // Slight offset to avoid z-fighting with upper-level slabs
39
+ }
40
+ /**
41
+ * Generates flat ceiling geometry from polygon (no extrusion)
42
+ */
43
+ export function generateCeilingGeometry(ceilingNode) {
44
+ const polygon = ceilingNode.polygon;
45
+ if (polygon.length < 3) {
46
+ return new THREE.BufferGeometry();
47
+ }
48
+ // Create shape from polygon
49
+ // Shape is in X-Y plane, we'll rotate to X-Z plane
50
+ const shape = new THREE.Shape();
51
+ const firstPt = polygon[0];
52
+ // Negate Y (which becomes Z) to get correct orientation after rotation
53
+ shape.moveTo(firstPt[0], -firstPt[1]);
54
+ for (let i = 1; i < polygon.length; i++) {
55
+ const pt = polygon[i];
56
+ shape.lineTo(pt[0], -pt[1]);
57
+ }
58
+ shape.closePath();
59
+ // Create flat shape geometry (no extrusion)
60
+ const geometry = new THREE.ShapeGeometry(shape);
61
+ // Rotate so the shape lies flat in X-Z plane
62
+ geometry.rotateX(-Math.PI / 2);
63
+ geometry.computeVertexNormals();
64
+ return geometry;
65
+ }
@@ -0,0 +1,2 @@
1
+ export declare const ItemSystem: () => null;
2
+ //# sourceMappingURL=item-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item/item-system.tsx"],"names":[],"mappings":"AAYA,eAAO,MAAM,UAAU,YAyCtB,CAAA"}
@@ -0,0 +1,43 @@
1
+ import { useFrame } from '@react-three/fiber';
2
+ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
3
+ import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
4
+ import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
5
+ import useScene from '../../store/use-scene';
6
+ // ============================================================================
7
+ // ITEM SYSTEM
8
+ // ============================================================================
9
+ export const ItemSystem = () => {
10
+ const dirtyNodes = useScene((state) => state.dirtyNodes);
11
+ const clearDirty = useScene((state) => state.clearDirty);
12
+ useFrame(() => {
13
+ if (dirtyNodes.size === 0)
14
+ return;
15
+ const nodes = useScene.getState().nodes;
16
+ dirtyNodes.forEach((id) => {
17
+ const node = nodes[id];
18
+ if (!node || node.type !== 'item')
19
+ return;
20
+ const item = node;
21
+ const mesh = sceneRegistry.nodes.get(id);
22
+ if (!mesh)
23
+ return;
24
+ if (item.asset.attachTo === 'wall-side') {
25
+ // Wall-attached item: offset Z by half the parent wall's thickness
26
+ const parentWall = item.parentId ? nodes[item.parentId] : undefined;
27
+ if (parentWall && parentWall.type === 'wall') {
28
+ const wallThickness = parentWall.thickness ?? 0.1;
29
+ const side = item.side === 'front' ? 1 : -1;
30
+ mesh.position.z = (wallThickness / 2) * side;
31
+ }
32
+ }
33
+ else if (!item.asset.attachTo) {
34
+ // Floor item: elevate by slab height (using full footprint overlap)
35
+ const levelId = resolveLevelId(item, nodes);
36
+ const slabElevation = spatialGridManager.getSlabElevationForItem(levelId, item.position, item.asset.dimensions, item.rotation);
37
+ mesh.position.y = slabElevation;
38
+ }
39
+ clearDirty(id);
40
+ });
41
+ });
42
+ return null;
43
+ };
@@ -0,0 +1,8 @@
1
+ import * as THREE from 'three';
2
+ import type { RoofNode } from '../../schema';
3
+ export declare const RoofSystem: () => null;
4
+ /**
5
+ * Generates detailed gable roof geometry with layers, walls, and overhangs
6
+ */
7
+ export declare function generateRoofGeometry(roofNode: RoofNode): THREE.BufferGeometry;
8
+ //# sourceMappingURL=roof-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAmBvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAqJD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAsH7E"}
@@ -0,0 +1,254 @@
1
+ import { useFrame } from '@react-three/fiber';
2
+ import * as THREE from 'three';
3
+ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
4
+ import useScene from '../../store/use-scene';
5
+ // ============================================================================
6
+ // ROOF GEOMETRY CONSTANTS
7
+ // ============================================================================
8
+ const THICKNESS_A = 0.05; // Roof cover thickness (5cm)
9
+ const THICKNESS_B = 0.1; // Structure thickness (10cm)
10
+ const ROOF_COVER_OVERHANG = 0.05; // Extension of cover past structure (5cm)
11
+ const EAVE_OVERHANG = 0.4; // Horizontal eave overhang (40cm)
12
+ const RAKE_OVERHANG = 0.3; // Overhang at gable ends (30cm)
13
+ const WALL_THICKNESS = 0.2; // Gable wall thickness (20cm)
14
+ const BASE_HEIGHT = 0.5; // Base height / knee wall / truss heel (50cm)
15
+ // ============================================================================
16
+ // ROOF SYSTEM
17
+ // ============================================================================
18
+ export const RoofSystem = () => {
19
+ const dirtyNodes = useScene((state) => state.dirtyNodes);
20
+ const clearDirty = useScene((state) => state.clearDirty);
21
+ useFrame(() => {
22
+ if (dirtyNodes.size === 0)
23
+ return;
24
+ const nodes = useScene.getState().nodes;
25
+ // Process dirty roofs
26
+ dirtyNodes.forEach((id) => {
27
+ const node = nodes[id];
28
+ if (!node || node.type !== 'roof')
29
+ return;
30
+ const mesh = sceneRegistry.nodes.get(id);
31
+ if (mesh) {
32
+ updateRoofGeometry(node, mesh);
33
+ clearDirty(id);
34
+ }
35
+ // If mesh not found, keep it dirty for next frame
36
+ });
37
+ });
38
+ return null;
39
+ };
40
+ /**
41
+ * Updates the geometry and transform for a single roof
42
+ */
43
+ function updateRoofGeometry(node, mesh) {
44
+ const newGeo = generateRoofGeometry(node);
45
+ mesh.geometry.dispose();
46
+ mesh.geometry = newGeo;
47
+ // Update position and rotation
48
+ mesh.position.set(node.position[0], node.position[1], node.position[2]);
49
+ mesh.rotation.y = node.rotation;
50
+ }
51
+ /**
52
+ * Helper to solve pitch angle analytically given rise, run and thicknesses
53
+ * Solves: run * tan(a) + (ThickA + ThickB)/cos(a) = rise
54
+ */
55
+ function solvePitch(rise, run, thickA, thickB) {
56
+ const T = thickA + thickB;
57
+ if (run < 0.01)
58
+ return 0;
59
+ const R = Math.sqrt(run * run + rise * rise);
60
+ if (R <= T) {
61
+ return Math.atan2(rise, run) * 0.5; // Fallback
62
+ }
63
+ const phi = Math.atan2(rise, run);
64
+ const shift = Math.asin(T / R);
65
+ return phi - shift;
66
+ }
67
+ /**
68
+ * Helper to create a Three.js Shape from polygon points
69
+ */
70
+ function createShape(points) {
71
+ const shape = new THREE.Shape();
72
+ if (points.length === 0)
73
+ return shape;
74
+ const firstPoint = points[0];
75
+ if (!firstPoint)
76
+ return shape;
77
+ shape.moveTo(firstPoint.x, firstPoint.y);
78
+ for (let i = 1; i < points.length; i++) {
79
+ const point = points[i];
80
+ if (point) {
81
+ shape.lineTo(point.x, point.y);
82
+ }
83
+ }
84
+ shape.closePath();
85
+ return shape;
86
+ }
87
+ /**
88
+ * Generate profile for one side of the roof (left or right)
89
+ */
90
+ function getSideProfile(dir, width, roofHeight) {
91
+ const halfWall = WALL_THICKNESS / 2;
92
+ const rise = Math.max(0, roofHeight - BASE_HEIGHT);
93
+ const run = width - halfWall;
94
+ const angle = solvePitch(rise, run, THICKNESS_A, THICKNESS_B);
95
+ const tanA = Math.tan(angle);
96
+ const cosA = Math.cos(angle);
97
+ const sinA = Math.sin(angle);
98
+ const ridgeUnderY = BASE_HEIGHT + run * tanA;
99
+ const ridgeInterfaceY = ridgeUnderY + THICKNESS_B / cosA;
100
+ const ridgeTopY = ridgeInterfaceY + THICKNESS_A / cosA;
101
+ const wallOuterTopY = BASE_HEIGHT - WALL_THICKNESS * tanA;
102
+ const overhangDx = EAVE_OVERHANG * cosA;
103
+ const eaveTopZ = width + halfWall + overhangDx;
104
+ const eaveTopY = ridgeTopY - eaveTopZ * tanA;
105
+ const coverExtDx = ROOF_COVER_OVERHANG * cosA;
106
+ const coverExtDy = ROOF_COVER_OVERHANG * sinA;
107
+ const eaveTopExtZ = eaveTopZ + coverExtDx;
108
+ const eaveTopExtY = eaveTopY - coverExtDy;
109
+ const eaveInterfaceExtZ = eaveTopExtZ - THICKNESS_A * sinA;
110
+ const eaveInterfaceExtY = eaveTopExtY - THICKNESS_A * cosA;
111
+ const eaveInterfaceZ = eaveTopZ;
112
+ const eaveBottomZ = eaveTopZ;
113
+ const eaveBottomY = ridgeUnderY - eaveTopZ * tanA;
114
+ // Layer A (Cover)
115
+ const pointsA = [
116
+ { x: 0, y: ridgeTopY },
117
+ { x: dir * eaveTopExtZ, y: eaveTopExtY },
118
+ { x: dir * eaveInterfaceExtZ, y: eaveInterfaceExtY },
119
+ { x: 0, y: ridgeInterfaceY },
120
+ ];
121
+ // Layer B (Structure)
122
+ const pointsB = [
123
+ { x: 0, y: ridgeInterfaceY },
124
+ { x: dir * eaveInterfaceZ, y: ridgeInterfaceY - eaveTopZ * tanA },
125
+ { x: dir * eaveBottomZ, y: eaveBottomY },
126
+ { x: 0, y: ridgeUnderY },
127
+ ];
128
+ // Side Wall
129
+ const zInner = width - halfWall;
130
+ const zOuter = width + halfWall;
131
+ const pointsSide = [
132
+ { x: dir * zInner, y: 0 },
133
+ { x: dir * zOuter, y: 0 },
134
+ { x: dir * zOuter, y: Math.max(0, wallOuterTopY) },
135
+ { x: dir * zInner, y: BASE_HEIGHT },
136
+ ];
137
+ // Gable Top (C1)
138
+ const pointsC1 = [
139
+ { x: 0, y: BASE_HEIGHT },
140
+ { x: dir * zInner, y: BASE_HEIGHT },
141
+ { x: dir * zInner, y: BASE_HEIGHT },
142
+ { x: 0, y: ridgeUnderY },
143
+ ];
144
+ // Gable Base (C2)
145
+ const pointsC2 = [
146
+ { x: 0, y: 0 },
147
+ { x: dir * zInner, y: 0 },
148
+ { x: dir * zInner, y: BASE_HEIGHT },
149
+ { x: 0, y: BASE_HEIGHT },
150
+ ];
151
+ return { pointsA, pointsB, pointsSide, pointsC1, pointsC2 };
152
+ }
153
+ /**
154
+ * Generates detailed gable roof geometry with layers, walls, and overhangs
155
+ */
156
+ export function generateRoofGeometry(roofNode) {
157
+ const { length, height, leftWidth, rightWidth } = roofNode;
158
+ const ridgeLength = length;
159
+ // Get profiles for both sides
160
+ const leftP = getSideProfile(1, leftWidth, height);
161
+ const rightP = getSideProfile(-1, rightWidth, height);
162
+ // Create shapes from profiles
163
+ const shapes = {
164
+ ALeft: createShape(leftP.pointsA),
165
+ ARight: createShape(rightP.pointsA),
166
+ BLeft: createShape(leftP.pointsB),
167
+ BRight: createShape(rightP.pointsB),
168
+ SideLeft: createShape(leftP.pointsSide),
169
+ SideRight: createShape(rightP.pointsSide),
170
+ C1Left: createShape(leftP.pointsC1),
171
+ C1Right: createShape(rightP.pointsC1),
172
+ C2Left: createShape(leftP.pointsC2),
173
+ C2Right: createShape(rightP.pointsC2),
174
+ };
175
+ // Calculate extrusion lengths and offsets
176
+ const lengths = {
177
+ A: ridgeLength + 2 * RAKE_OVERHANG + 2 * ROOF_COVER_OVERHANG + WALL_THICKNESS,
178
+ B: ridgeLength + 2 * RAKE_OVERHANG + WALL_THICKNESS,
179
+ Side: ridgeLength + WALL_THICKNESS,
180
+ Gable: WALL_THICKNESS,
181
+ };
182
+ const offsets = {
183
+ A: -RAKE_OVERHANG - ROOF_COVER_OVERHANG - WALL_THICKNESS / 2,
184
+ B: -RAKE_OVERHANG - WALL_THICKNESS / 2,
185
+ Side: -WALL_THICKNESS / 2,
186
+ GableFront: -WALL_THICKNESS / 2,
187
+ GableBack: ridgeLength - WALL_THICKNESS / 2,
188
+ };
189
+ // Helper to create and position extruded geometry
190
+ const createPart = (shape, depth, xOffset) => {
191
+ const geo = new THREE.ExtrudeGeometry(shape, { depth, bevelEnabled: false });
192
+ // Rotate to align: extrusion goes along X axis
193
+ geo.rotateY(Math.PI / 2);
194
+ geo.translate(xOffset, 0, 0);
195
+ return geo;
196
+ };
197
+ // Create all parts
198
+ const geometries = [];
199
+ // Layer A (Cover) - both sides
200
+ geometries.push(createPart(shapes.ALeft, lengths.A, offsets.A));
201
+ geometries.push(createPart(shapes.ARight, lengths.A, offsets.A));
202
+ // Layer B (Structure) - both sides
203
+ geometries.push(createPart(shapes.BLeft, lengths.B, offsets.B));
204
+ geometries.push(createPart(shapes.BRight, lengths.B, offsets.B));
205
+ // Side Walls - both sides
206
+ geometries.push(createPart(shapes.SideLeft, lengths.Side, offsets.Side));
207
+ geometries.push(createPart(shapes.SideRight, lengths.Side, offsets.Side));
208
+ // Gable Walls (Front)
209
+ geometries.push(createPart(shapes.C1Left, lengths.Gable, offsets.GableFront));
210
+ geometries.push(createPart(shapes.C1Right, lengths.Gable, offsets.GableFront));
211
+ geometries.push(createPart(shapes.C2Left, lengths.Gable, offsets.GableFront));
212
+ geometries.push(createPart(shapes.C2Right, lengths.Gable, offsets.GableFront));
213
+ // Gable Walls (Back)
214
+ geometries.push(createPart(shapes.C1Left, lengths.Gable, offsets.GableBack));
215
+ geometries.push(createPart(shapes.C1Right, lengths.Gable, offsets.GableBack));
216
+ geometries.push(createPart(shapes.C2Left, lengths.Gable, offsets.GableBack));
217
+ geometries.push(createPart(shapes.C2Right, lengths.Gable, offsets.GableBack));
218
+ // Merge all geometries
219
+ const mergedGeometry = new THREE.BufferGeometry();
220
+ const positions = [];
221
+ const normals = [];
222
+ const uvs = [];
223
+ for (const geo of geometries) {
224
+ const posAttr = geo.getAttribute('position');
225
+ const normAttr = geo.getAttribute('normal');
226
+ const uvAttr = geo.getAttribute('uv');
227
+ if (posAttr) {
228
+ for (let i = 0; i < posAttr.count; i++) {
229
+ positions.push(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i));
230
+ }
231
+ }
232
+ if (normAttr) {
233
+ for (let i = 0; i < normAttr.count; i++) {
234
+ normals.push(normAttr.getX(i), normAttr.getY(i), normAttr.getZ(i));
235
+ }
236
+ }
237
+ if (uvAttr) {
238
+ for (let i = 0; i < uvAttr.count; i++) {
239
+ uvs.push(uvAttr.getX(i), uvAttr.getY(i));
240
+ }
241
+ }
242
+ geo.dispose();
243
+ }
244
+ mergedGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
245
+ mergedGeometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
246
+ if (uvs.length > 0) {
247
+ mergedGeometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
248
+ }
249
+ mergedGeometry.computeVertexNormals();
250
+ // Center the geometry at X=0 (translate by -ridgeLength/2)
251
+ // This matches the old geometry centering behavior
252
+ mergedGeometry.translate(-ridgeLength / 2, 0, 0);
253
+ return mergedGeometry;
254
+ }
@@ -0,0 +1,8 @@
1
+ import * as THREE from 'three';
2
+ import type { SlabNode } from '../../schema';
3
+ export declare const SlabSystem: () => null;
4
+ /**
5
+ * Generates extruded slab geometry from polygon
6
+ */
7
+ export declare function generateSlabGeometry(slabNode: SlabNode): THREE.BufferGeometry;
8
+ //# sourceMappingURL=slab-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAOvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAkED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAiC7E"}
@@ -0,0 +1,117 @@
1
+ import { useFrame } from '@react-three/fiber';
2
+ import * as THREE from 'three';
3
+ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
4
+ import useScene from '../../store/use-scene';
5
+ // ============================================================================
6
+ // SLAB SYSTEM
7
+ // ============================================================================
8
+ export const SlabSystem = () => {
9
+ const dirtyNodes = useScene((state) => state.dirtyNodes);
10
+ const clearDirty = useScene((state) => state.clearDirty);
11
+ useFrame(() => {
12
+ if (dirtyNodes.size === 0)
13
+ return;
14
+ const nodes = useScene.getState().nodes;
15
+ // Process dirty slabs
16
+ dirtyNodes.forEach((id) => {
17
+ const node = nodes[id];
18
+ if (!node || node.type !== 'slab')
19
+ return;
20
+ const mesh = sceneRegistry.nodes.get(id);
21
+ if (mesh) {
22
+ updateSlabGeometry(node, mesh);
23
+ clearDirty(id);
24
+ }
25
+ // If mesh not found, keep it dirty for next frame
26
+ });
27
+ });
28
+ return null;
29
+ };
30
+ /**
31
+ * Updates the geometry for a single slab
32
+ */
33
+ function updateSlabGeometry(node, mesh) {
34
+ const newGeo = generateSlabGeometry(node);
35
+ mesh.geometry.dispose();
36
+ mesh.geometry = newGeo;
37
+ }
38
+ /** Half of default wall thickness — used to extend slab geometry under walls */
39
+ const SLAB_OUTSET = 0.05;
40
+ /**
41
+ * Expand a polygon outward by a uniform distance.
42
+ * Offsets each edge outward then intersects consecutive offset edges.
43
+ */
44
+ function outsetPolygon(polygon, amount) {
45
+ const n = polygon.length;
46
+ if (n < 3)
47
+ return polygon;
48
+ // Determine winding via signed area
49
+ let area2 = 0;
50
+ for (let i = 0; i < n; i++) {
51
+ const j = (i + 1) % n;
52
+ area2 += polygon[i][0] * polygon[j][1] - polygon[j][0] * polygon[i][1];
53
+ }
54
+ const s = area2 >= 0 ? 1 : -1;
55
+ // Offset each edge outward by amount
56
+ const offEdges = [];
57
+ for (let i = 0; i < n; i++) {
58
+ const j = (i + 1) % n;
59
+ const dx = polygon[j][0] - polygon[i][0];
60
+ const dz = polygon[j][1] - polygon[i][1];
61
+ const len = Math.sqrt(dx * dx + dz * dz);
62
+ if (len < 1e-9) {
63
+ offEdges.push([polygon[i][0], polygon[i][1], dx, dz]);
64
+ continue;
65
+ }
66
+ const nx = (s * dz / len) * amount;
67
+ const nz = (s * -dx / len) * amount;
68
+ offEdges.push([polygon[i][0] + nx, polygon[i][1] + nz, dx, dz]);
69
+ }
70
+ // Intersect consecutive offset edges to get new vertices
71
+ const result = [];
72
+ for (let i = 0; i < n; i++) {
73
+ const j = (i + 1) % n;
74
+ const [ax, az, adx, adz] = offEdges[i];
75
+ const [bx, bz, bdx, bdz] = offEdges[j];
76
+ const denom = adx * bdz - adz * bdx;
77
+ if (Math.abs(denom) < 1e-9) {
78
+ // Parallel edges — use offset endpoint
79
+ result.push([ax + adx, az + adz]);
80
+ }
81
+ else {
82
+ const t = ((bx - ax) * bdz - (bz - az) * bdx) / denom;
83
+ result.push([ax + t * adx, az + t * adz]);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ /**
89
+ * Generates extruded slab geometry from polygon
90
+ */
91
+ export function generateSlabGeometry(slabNode) {
92
+ const polygon = outsetPolygon(slabNode.polygon, SLAB_OUTSET);
93
+ const elevation = slabNode.elevation ?? 0.05;
94
+ if (polygon.length < 3) {
95
+ return new THREE.BufferGeometry();
96
+ }
97
+ // Create shape from polygon
98
+ // Shape is in X-Y plane, we'll rotate to X-Z plane after extrusion
99
+ const shape = new THREE.Shape();
100
+ const firstPt = polygon[0];
101
+ // Negate Y (which becomes Z) to get correct orientation after rotation
102
+ shape.moveTo(firstPt[0], -firstPt[1]);
103
+ for (let i = 1; i < polygon.length; i++) {
104
+ const pt = polygon[i];
105
+ shape.lineTo(pt[0], -pt[1]);
106
+ }
107
+ shape.closePath();
108
+ // Extrude the shape by elevation
109
+ const geometry = new THREE.ExtrudeGeometry(shape, {
110
+ depth: elevation,
111
+ bevelEnabled: false,
112
+ });
113
+ // Rotate so extrusion direction (Z) becomes height direction (Y)
114
+ geometry.rotateX(-Math.PI / 2);
115
+ geometry.computeVertexNormals();
116
+ return geometry;
117
+ }