@pascal-app/viewer 0.9.2 → 1.0.0-beta.1

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 (77) hide show
  1. package/README.md +20 -5
  2. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  3. package/dist/components/viewer/frame-limiter.js +25 -5
  4. package/dist/components/viewer/glb-scene.js +2 -2
  5. package/dist/components/viewer/glb-walkthrough-controller.d.ts +10 -0
  6. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  7. package/dist/components/viewer/glb-walkthrough-controller.js +96 -8
  8. package/dist/components/viewer/index.d.ts.map +1 -1
  9. package/dist/components/viewer/index.js +9 -0
  10. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  11. package/dist/components/viewer/post-processing.js +1 -0
  12. package/dist/components/viewer/selection-manager.js +5 -2
  13. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  14. package/dist/hooks/use-gltf-ktx2.js +2 -5
  15. package/dist/index.d.ts +8 -5
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +10 -8
  18. package/dist/lib/hero-pose.d.ts +47 -0
  19. package/dist/lib/hero-pose.d.ts.map +1 -0
  20. package/dist/lib/hero-pose.js +227 -0
  21. package/dist/lib/ktx2-loader.d.ts +3 -0
  22. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  23. package/dist/lib/ktx2-loader.js +6 -0
  24. package/dist/lib/materials.d.ts +2 -0
  25. package/dist/lib/materials.d.ts.map +1 -1
  26. package/dist/lib/materials.js +56 -9
  27. package/dist/lib/materials.test.d.ts +2 -0
  28. package/dist/lib/materials.test.d.ts.map +1 -0
  29. package/dist/lib/materials.test.js +25 -0
  30. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  31. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  32. package/dist/lib/merged-outline-node.test.js +22 -0
  33. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  34. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  35. package/dist/lib/snapshot-pipeline.js +254 -0
  36. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  37. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  38. package/dist/lib/terrain-perimeter-fill.js +75 -0
  39. package/dist/lib/texture-node-guard.d.ts +13 -0
  40. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  41. package/dist/lib/texture-node-guard.js +39 -0
  42. package/dist/lib/texture-reference.d.ts +31 -0
  43. package/dist/lib/texture-reference.d.ts.map +1 -0
  44. package/dist/lib/texture-reference.js +139 -0
  45. package/dist/lib/texture-reference.test.d.ts +2 -0
  46. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  47. package/dist/lib/texture-reference.test.js +86 -0
  48. package/dist/store/use-viewer.d.ts +13 -0
  49. package/dist/store/use-viewer.d.ts.map +1 -1
  50. package/dist/store/use-viewer.js +16 -1
  51. package/dist/store/use-viewer.test.js +19 -0
  52. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  53. package/dist/systems/ceiling/ceiling-system.js +8 -4
  54. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  55. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  56. package/dist/systems/level/level-system.d.ts.map +1 -1
  57. package/dist/systems/level/level-system.js +8 -7
  58. package/dist/systems/level/level-utils.d.ts.map +1 -1
  59. package/dist/systems/level/level-utils.js +7 -6
  60. package/dist/systems/slab/slab-system.d.ts +5 -4
  61. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  62. package/dist/systems/slab/slab-system.js +24 -94
  63. package/dist/systems/slab/slab-system.test.js +51 -12
  64. package/dist/systems/stair/stair-system.js +1 -1
  65. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  66. package/dist/systems/wall/wall-cutout.js +5 -2
  67. package/dist/systems/wall/wall-support-extension.test.js +69 -1
  68. package/dist/systems/wall/wall-system.d.ts +3 -7
  69. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  70. package/dist/systems/wall/wall-system.js +87 -22
  71. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  72. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  73. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  74. package/package.json +2 -2
  75. package/dist/components/viewer/walkthrough-controls.d.ts +0 -3
  76. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  77. package/dist/components/viewer/walkthrough-controls.js +0 -151
@@ -0,0 +1,86 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer 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 * as THREE from 'three';
5
+ import { getPascalTextureRef, stampPascalTextureRef } from './texture-reference';
6
+ // The module reads the storage origin lazily on first use, so setting the env
7
+ // here (before any stamp call) pins it for the whole test file.
8
+ process.env.NEXT_PUBLIC_SUPABASE_URL ??= 'https://test-storage.supabase.co';
9
+ const STORAGE_ORIGIN = new URL(process.env.NEXT_PUBLIC_SUPABASE_URL).origin;
10
+ describe('Pascal texture references', () => {
11
+ test("resolves 'material' input to library-material for storage-bucket URLs", () => {
12
+ const texture = new THREE.Texture();
13
+ texture.colorSpace = THREE.SRGBColorSpace;
14
+ const src = `${STORAGE_ORIGIN}/storage/v1/object/public/materials/user/mtl_1/oak_basecolor_512.ktx2`;
15
+ const ref = stampPascalTextureRef(texture, { kind: 'material', src, slot: 'map' });
16
+ expect(ref).toEqual({
17
+ v: 1,
18
+ kind: 'library-material',
19
+ src,
20
+ map: 'basecolor',
21
+ colorSpace: 'srgb',
22
+ });
23
+ expect(getPascalTextureRef(texture)).toEqual(ref);
24
+ });
25
+ test("resolves 'material' input to app-material for assets-CDN catalog URLs", () => {
26
+ const cdnOrigin = new URL(process.env.NEXT_PUBLIC_ASSETS_CDN_URL || 'https://editor.pascal.app')
27
+ .origin;
28
+ const texture = new THREE.Texture();
29
+ const src = `${cdnOrigin}/material/concrete/prepared_drywall/prepared_drywall_normal_512.ktx2`;
30
+ const ref = stampPascalTextureRef(texture, { kind: 'material', src, slot: 'normalMap' });
31
+ expect(ref).toEqual({ v: 1, kind: 'app-material', src, map: 'normal', colorSpace: 'linear' });
32
+ expect(getPascalTextureRef(texture)).toEqual(ref);
33
+ const foreign = new THREE.Texture();
34
+ expect(stampPascalTextureRef(foreign, {
35
+ kind: 'material',
36
+ src: 'https://example.com/material/concrete/x/x_basecolor_512.ktx2',
37
+ slot: 'map',
38
+ })).toBeNull();
39
+ });
40
+ test('stamps the exact project-asset payload for Pascal storage URLs', () => {
41
+ const texture = new THREE.Texture();
42
+ texture.colorSpace = THREE.SRGBColorSpace;
43
+ const ref = stampPascalTextureRef(texture, {
44
+ kind: 'project-asset',
45
+ src: `${STORAGE_ORIGIN}/storage/v1/object/public/project-assets/project/asset.png`,
46
+ slot: 'map',
47
+ });
48
+ expect(ref).toEqual({
49
+ v: 1,
50
+ kind: 'project-asset',
51
+ src: `${STORAGE_ORIGIN}/storage/v1/object/public/project-assets/project/asset.png`,
52
+ map: 'basecolor',
53
+ colorSpace: 'srgb',
54
+ });
55
+ expect(getPascalTextureRef(texture)).toEqual(ref);
56
+ });
57
+ test('keeps local and non-Pascal URLs unstamped', () => {
58
+ for (const src of [
59
+ 'asset://project/asset',
60
+ 'blob:https://editor.pascal.app/asset',
61
+ 'data:image/png;base64,AAAA',
62
+ 'https://example.com/storage/v1/object/public/project-assets/project/asset.png',
63
+ ]) {
64
+ const texture = new THREE.Texture();
65
+ expect(stampPascalTextureRef(texture, { kind: 'project-asset', src, slot: 'map' })).toBeNull();
66
+ expect(texture.userData.pascalTextureRef).toBeUndefined();
67
+ }
68
+ });
69
+ test('includes imageIndex only for item GLB references', () => {
70
+ const texture = new THREE.Texture();
71
+ const ref = stampPascalTextureRef(texture, {
72
+ kind: 'item-glb',
73
+ src: `${STORAGE_ORIGIN}/storage/v1/object/public/items/system/chair/model.glb`,
74
+ slot: 'normalMap',
75
+ imageIndex: 2,
76
+ });
77
+ expect(ref).toEqual({
78
+ v: 1,
79
+ kind: 'item-glb',
80
+ src: `${STORAGE_ORIGIN}/storage/v1/object/public/items/system/chair/model.glb`,
81
+ imageIndex: 2,
82
+ map: 'normal',
83
+ colorSpace: 'linear',
84
+ });
85
+ });
86
+ });
@@ -3,6 +3,7 @@ import type { Object3D } from 'three';
3
3
  import type { EdgeMode } from '../lib/edge-style';
4
4
  import type { ColorPreset, RenderShading } from '../lib/materials';
5
5
  export type RenderContext = 'editor' | 'viewer';
6
+ export type MetricNotation = 'meters' | 'millimeters';
6
7
  type SelectionPath = {
7
8
  buildingId: BuildingNode['id'] | null;
8
9
  levelId: LevelNode['id'] | null;
@@ -17,6 +18,10 @@ type ViewerState = {
17
18
  selection: SelectionPath;
18
19
  previewSelectedIds: BaseNode['id'][];
19
20
  setPreviewSelectedIds: (ids: BaseNode['id'][]) => void;
21
+ /** Host-owned selection highlights rendered through the viewer's native
22
+ * selection paths without changing the local user's editable selection. */
23
+ externalSelectedIds: BaseNode['id'][];
24
+ setExternalSelectedIds: (ids: BaseNode['id'][]) => void;
20
25
  hoverHighlightMode: string;
21
26
  setHoverHighlightMode: (mode: string) => void;
22
27
  hoveredId: AnyNode['id'] | ZoneNode['id'] | null;
@@ -57,6 +62,8 @@ type ViewerState = {
57
62
  setShadows: (shadows: boolean) => void;
58
63
  unit: 'metric' | 'imperial';
59
64
  setUnit: (unit: 'metric' | 'imperial') => void;
65
+ metricNotation: MetricNotation;
66
+ setMetricNotation: (notation: MetricNotation) => void;
60
67
  /** True once the user explicitly picked a unit. Until then `unit` is a
61
68
  * locale-derived default and is not persisted, so the default can keep
62
69
  * tracking the browser locale across sessions. */
@@ -100,6 +107,10 @@ type ViewerState = {
100
107
  setDebugColors: (enabled: boolean) => void;
101
108
  walkthroughMode: boolean;
102
109
  setWalkthroughMode: (mode: boolean) => void;
110
+ /** Pointer lock temporarily released mid-walkthrough (⌘/PrintScreen — OS
111
+ * screenshot needs a movable cursor); clicking the canvas re-locks. */
112
+ walkthroughSuspended: boolean;
113
+ setWalkthroughSuspended: (suspended: boolean) => void;
103
114
  cameraDragging: boolean;
104
115
  setCameraDragging: (dragging: boolean) => void;
105
116
  /**
@@ -120,6 +131,7 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
120
131
  setState(state: ViewerState | ((state: ViewerState) => ViewerState), replace: true): unknown;
121
132
  persist: {
122
133
  setOptions: (options: Partial<import("zustand/middleware").PersistOptions<ViewerState, {
134
+ metricNotation: MetricNotation;
123
135
  levelMode: "manual" | "stacked" | "exploded" | "solo";
124
136
  wallMode: "up" | "down" | "cutaway" | "translucent";
125
137
  projectPreferences: Record<string, {
@@ -143,6 +155,7 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
143
155
  onHydrate: (fn: (state: ViewerState) => void) => () => void;
144
156
  onFinishHydration: (fn: (state: ViewerState) => void) => () => void;
145
157
  getOptions: () => Partial<import("zustand/middleware").PersistOptions<ViewerState, {
158
+ metricNotation: MetricNotation;
146
159
  levelMode: "manual" | "stacked" | "exploded" | "solo";
147
160
  wallMode: "up" | "down" | "cutaway" | "translucent";
148
161
  projectPreferences: Record<string, {
@@ -1 +1 @@
1
- {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ const RENDER_SHADINGS = ['solid', 'rendered'];
7
7
  const COLOR_PRESETS = ['clay', 'white', 'mono', 'blueprint'];
8
8
  const EDGE_MODES = ['off', 'soft', 'strong'];
9
9
  const UNITS = ['metric', 'imperial'];
10
+ const METRIC_NOTATIONS = ['meters', 'millimeters'];
10
11
  const LEVEL_MODES = ['stacked', 'exploded', 'solo', 'manual'];
11
12
  const WALL_MODES = ['up', 'cutaway', 'down', 'translucent'];
12
13
  // Countries still on imperial/US customary units: United States, Liberia, Myanmar.
@@ -112,6 +113,7 @@ function normalizePersistedViewerState(value) {
112
113
  edges: pickString(state.edges, EDGE_MODES, 'soft'),
113
114
  shadows: typeof state.shadows === 'boolean' ? state.shadows : true,
114
115
  unit: pickString(state.unit, UNITS, detectDefaultUnit()),
116
+ metricNotation: pickString(state.metricNotation, METRIC_NOTATIONS, 'meters'),
115
117
  unitExplicit: typeof state.unit === 'string' && UNITS.includes(state.unit),
116
118
  levelMode: pickString(state.levelMode, LEVEL_MODES, 'stacked'),
117
119
  wallMode: pickString(state.wallMode, WALL_MODES, 'up'),
@@ -122,6 +124,14 @@ const useViewer = create()(persist((set) => ({
122
124
  selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
123
125
  previewSelectedIds: [],
124
126
  setPreviewSelectedIds: (ids) => set({ previewSelectedIds: ids }),
127
+ externalSelectedIds: [],
128
+ setExternalSelectedIds: (ids) => set((state) => {
129
+ if (state.externalSelectedIds.length === ids.length &&
130
+ state.externalSelectedIds.every((id, index) => id === ids[index])) {
131
+ return state;
132
+ }
133
+ return { externalSelectedIds: ids };
134
+ }),
125
135
  hoverHighlightMode: 'default',
126
136
  setHoverHighlightMode: (mode) => set((state) => (state.hoverHighlightMode === mode ? state : { hoverHighlightMode: mode })),
127
137
  hoveredId: null,
@@ -162,8 +172,10 @@ const useViewer = create()(persist((set) => ({
162
172
  shadows: true,
163
173
  setShadows: (shadows) => set({ shadows }),
164
174
  unit: detectDefaultUnit(),
175
+ metricNotation: 'meters',
165
176
  unitExplicit: false,
166
177
  setUnit: (unit) => set({ unit, unitExplicit: true }),
178
+ setMetricNotation: (metricNotation) => set({ unit: 'metric', metricNotation, unitExplicit: true }),
167
179
  levelMode: 'stacked',
168
180
  setLevelMode: (mode) => set({ levelMode: mode }),
169
181
  wallMode: 'up',
@@ -272,7 +284,9 @@ const useViewer = create()(persist((set) => ({
272
284
  debugColors: false,
273
285
  setDebugColors: (enabled) => set({ debugColors: enabled }),
274
286
  walkthroughMode: false,
275
- setWalkthroughMode: (mode) => set({ walkthroughMode: mode }),
287
+ setWalkthroughMode: (mode) => set({ walkthroughMode: mode, walkthroughSuspended: false }),
288
+ walkthroughSuspended: false,
289
+ setWalkthroughSuspended: (suspended) => set({ walkthroughSuspended: suspended }),
276
290
  cameraDragging: false,
277
291
  setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
278
292
  inputDragging: false,
@@ -292,6 +306,7 @@ const useViewer = create()(persist((set) => ({
292
306
  edges: state.edges,
293
307
  shadows: state.shadows,
294
308
  ...(state.unitExplicit ? { unit: state.unit } : {}),
309
+ metricNotation: state.metricNotation,
295
310
  levelMode: state.levelMode,
296
311
  wallMode: state.wallMode,
297
312
  projectPreferences: state.projectPreferences,
@@ -4,10 +4,12 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
4
4
  import useViewer from './use-viewer';
5
5
  const resetMeasurementPreferences = () => {
6
6
  useViewer.setState({
7
+ externalSelectedIds: [],
7
8
  projectId: null,
8
9
  projectPreferences: {},
9
10
  showMeasurements: true,
10
11
  unit: 'metric',
12
+ metricNotation: 'meters',
11
13
  });
12
14
  };
13
15
  beforeEach(resetMeasurementPreferences);
@@ -39,4 +41,21 @@ describe('measurement display preferences', () => {
39
41
  expect(useViewer.getState().projectPreferences).toEqual(preferences);
40
42
  expect(useViewer.getState().showMeasurements).toBe(false);
41
43
  });
44
+ test('selects millimeters as a metric display notation', () => {
45
+ useViewer.getState().setUnit('imperial');
46
+ useViewer.getState().setMetricNotation('millimeters');
47
+ expect(useViewer.getState()).toMatchObject({
48
+ unit: 'metric',
49
+ metricNotation: 'millimeters',
50
+ unitExplicit: true,
51
+ });
52
+ });
53
+ });
54
+ describe('external selection highlights', () => {
55
+ test('tracks host-owned highlights without changing the local selection', () => {
56
+ const localSelection = useViewer.getState().selection;
57
+ useViewer.getState().setExternalSelectedIds(['wall_remote']);
58
+ expect(useViewer.getState().externalSelectedIds).toEqual(['wall_remote']);
59
+ expect(useViewer.getState().selection).toBe(localSelection);
60
+ });
42
61
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAMjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAgB9B,eAAO,MAAM,aAAa,YA4BzB,CAAA;AA2DD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAM,GAC9C,KAAK,CAAC,cAAc,CA6DtB"}
1
+ {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAOjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAgB9B,eAAO,MAAM,aAAa,YA4BzB,CAAA;AAgED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAM,GAC9C,KAAK,CAAC,cAAc,CA6DtB"}
@@ -1,4 +1,4 @@
1
- import { getEffectiveNode, nodeRegistry, sceneRegistry, useLiveTransforms, useScene, } from '@pascal-app/core';
1
+ import { getEffectiveNode, nodeRegistry, resolveCeilingHeight, sceneRegistry, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { mergeSurfaceHolePolygons } from '../surface-hole-geometry';
@@ -30,7 +30,7 @@ export const CeilingSystem = () => {
30
30
  // the final value on commit. Mirrors WallSystem / GeometrySystem.
31
31
  const effective = getEffectiveNode(node);
32
32
  const itemHoles = collectCeilingHoles(effective, nodes);
33
- updateCeilingGeometry(effective, mesh, itemHoles);
33
+ updateCeilingGeometry(effective, mesh, itemHoles, nodes);
34
34
  clearDirty(id);
35
35
  }
36
36
  // If mesh not found, keep it dirty for next frame
@@ -64,7 +64,7 @@ function collectCeilingHoles(ceiling, nodes) {
64
64
  /**
65
65
  * Updates the geometry for a single ceiling
66
66
  */
67
- function updateCeilingGeometry(node, mesh, extraHoles = []) {
67
+ function updateCeilingGeometry(node, mesh, extraHoles = [], nodes = useScene.getState().nodes) {
68
68
  const newGeo = generateCeilingGeometry(node, extraHoles);
69
69
  mesh.geometry.dispose();
70
70
  mesh.geometry = newGeo;
@@ -81,7 +81,11 @@ function updateCeilingGeometry(node, mesh, extraHoles = []) {
81
81
  const liveTransform = useLiveTransforms.getState().get(node.id);
82
82
  mesh.position.x = liveTransform?.position[0] ?? 0;
83
83
  mesh.position.z = liveTransform?.position[2] ?? 0;
84
- mesh.position.y = (node.height ?? 2.5) - 0.01 + (liveTransform?.position[1] ?? 0); // Slight offset to avoid z-fighting with upper-level slabs
84
+ // Resolved height: explicit when stored, else the level-top bound — so a
85
+ // follows-mode ceiling re-parks under the current plane on every rebuild
86
+ // (level-height edits / covering-slab changes dirty-mark ceilings).
87
+ // Slight offset to avoid z-fighting with upper-level slabs.
88
+ mesh.position.y = resolveCeilingHeight(node, nodes) - 0.01 + (liveTransform?.position[1] ?? 0);
85
89
  }
86
90
  /**
87
91
  * Generates flat ceiling geometry from polygon (no extrusion).
@@ -1 +1 @@
1
- {"version":3,"file":"floor-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/floor-elevation/floor-elevation-system.tsx"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,YA6ChC,CAAA"}
1
+ {"version":3,"file":"floor-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/floor-elevation/floor-elevation-system.tsx"],"names":[],"mappings":"AAyCA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,YAoEhC,CAAA"}
@@ -1,7 +1,6 @@
1
- import { getEffectiveNode, getFloorStackedPosition, nodeRegistry, sceneRegistry, useLiveTransforms, useScene, } from '@pascal-app/core';
1
+ import { getEffectiveNode, getFloorStackedPosition, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
- function withLiveTransform(node, id) {
4
- const liveTransform = useLiveTransforms.getState().get(id);
3
+ function withLiveTransform(node, liveTransform) {
5
4
  if (!liveTransform)
6
5
  return node;
7
6
  const currentRotation = node.rotation;
@@ -38,10 +37,18 @@ export const FloorElevationSystem = () => {
38
37
  const dirtyNodes = useScene((s) => s.dirtyNodes);
39
38
  const clearDirty = useScene((s) => s.clearDirty);
40
39
  useFrame(() => {
41
- if (dirtyNodes.size === 0)
40
+ // Nodes with a live preview (override / transform) are reapplied EVERY
41
+ // frame, not only while dirty: the React commit that rebinds the group's
42
+ // base-Y position can land between frames, after the dirty mark was
43
+ // already consumed by the priority-2 systems — without this the lift
44
+ // vanishes until the next pointer tick re-dirties (visible Y blink
45
+ // during group drags over elevated slabs).
46
+ const overrides = useLiveNodeOverrides.getState().overrides;
47
+ const transforms = useLiveTransforms.getState().transforms;
48
+ if (dirtyNodes.size === 0 && overrides.size === 0 && transforms.size === 0)
42
49
  return;
43
50
  const nodes = useScene.getState().nodes;
44
- dirtyNodes.forEach((id) => {
51
+ const applyLift = (id) => {
45
52
  const node = nodes[id];
46
53
  if (!node)
47
54
  return;
@@ -52,7 +59,8 @@ export const FloorElevationSystem = () => {
52
59
  const mesh = sceneRegistry.nodes.get(id);
53
60
  if (!mesh)
54
61
  return;
55
- const effectiveNode = withLiveTransform(getEffectiveNode(node), id);
62
+ const liveTransform = useLiveTransforms.getState().get(id);
63
+ const effectiveNode = withLiveTransform(getEffectiveNode(node), liveTransform);
56
64
  const position = effectiveNode.position;
57
65
  if (!position)
58
66
  return;
@@ -67,11 +75,26 @@ export const FloorElevationSystem = () => {
67
75
  node: effectiveNode,
68
76
  nodes: resolverNodes,
69
77
  position,
78
+ // 3D drags publish the pointer-decided surface cap with their live
79
+ // transform; honoring it here keeps this system's per-frame Y in
80
+ // agreement with the tool's preview (no deck/floor flicker).
81
+ maxElevation: liveTransform?.supportElevationCap,
70
82
  });
71
83
  mesh.position.y = visualPosition[1];
72
- if (!(def.geometry || def.system)) {
84
+ if (!(def.geometry || def.system) && dirtyNodes.has(id)) {
73
85
  clearDirty(id);
74
86
  }
87
+ };
88
+ dirtyNodes.forEach((id) => {
89
+ applyLift(id);
90
+ });
91
+ overrides.forEach((_values, id) => {
92
+ if (!dirtyNodes.has(id))
93
+ applyLift(id);
94
+ });
95
+ transforms.forEach((_transform, id) => {
96
+ if (!dirtyNodes.has(id) && !overrides.has(id))
97
+ applyLift(id);
75
98
  });
76
99
  }, 1);
77
100
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-system.tsx"],"names":[],"mappings":"AAeA,eAAO,MAAM,WAAW,YA6DvB,CAAA"}
1
+ {"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-system.tsx"],"names":[],"mappings":"AAeA,eAAO,MAAM,WAAW,YAuDvB,CAAA"}
@@ -1,4 +1,4 @@
1
- import { getLevelHeight, sceneRegistry, useScene } from '@pascal-app/core';
1
+ import { getLevelElevations, sceneRegistry, useScene } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { lerp } from 'three/src/math/MathUtils.js';
4
4
  import { applyShadowOnly, clearShadowOnly } from '../../lib/shadow-only';
@@ -14,23 +14,25 @@ export const LevelSystem = () => {
14
14
  const nodes = useScene.getState().nodes;
15
15
  const levelMode = useViewer.getState().levelMode;
16
16
  const selectedLevel = useViewer.getState().selection.levelId;
17
+ const levelElevations = getLevelElevations(nodes);
17
18
  const entries = [];
18
19
  sceneRegistry.byType.level.forEach((levelId) => {
19
20
  const obj = sceneRegistry.nodes.get(levelId);
20
21
  const level = nodes[levelId];
21
22
  if (obj && level) {
22
- entries.push({ levelId, index: level.level ?? 0, obj });
23
+ entries.push({
24
+ levelId,
25
+ index: level.level,
26
+ obj,
27
+ });
23
28
  }
24
29
  });
25
- entries.sort((a, b) => a.index - b.index);
26
- // Walk sorted levels, accumulating base Y offsets
27
30
  const selectedIndex = selectedLevel
28
31
  ? entries.find((e) => e.levelId === selectedLevel)?.index
29
32
  : undefined;
30
- let cumulativeY = 0;
31
33
  for (const { levelId, index, obj } of entries) {
32
34
  const level = nodes[levelId];
33
- const baseY = cumulativeY;
35
+ const baseY = levelElevations.get(levelId)?.baseY ?? 0;
34
36
  const explodedExtra = levelMode === 'exploded' ? index * EXPLODED_GAP : 0;
35
37
  const targetY = baseY + explodedExtra;
36
38
  obj.position.y = lerp(obj.position.y, targetY, delta * 12); // Smoothly animate to new Y position
@@ -51,7 +53,6 @@ export const LevelSystem = () => {
51
53
  }
52
54
  obj.visible = !hidden;
53
55
  }
54
- cumulativeY += getLevelHeight(levelId, nodes, (wallId) => sceneRegistry.nodes.get(wallId)?.position.y);
55
56
  }
56
57
  }, 5); // Using a lower priority so it runs after transforms from other systems have settled
57
58
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"level-utils.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,IAAI,CA6CtD"}
1
+ {"version":3,"file":"level-utils.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,IAAI,CAyCtD"}
@@ -1,4 +1,4 @@
1
- import { getLevelHeight, sceneRegistry, useScene } from '@pascal-app/core';
1
+ import { getLevelElevations, sceneRegistry, useScene } from '@pascal-app/core';
2
2
  /**
3
3
  * Instantly snaps all level Objects3D to their true stacked Y positions
4
4
  * (ignores levelMode — always uses stacked, no exploded gap).
@@ -18,18 +18,19 @@ export function snapLevelsToTruePositions() {
18
18
  const obj = sceneRegistry.nodes.get(levelId);
19
19
  const level = nodes[levelId];
20
20
  if (obj && level) {
21
- entries.push({ levelId, index: level.level ?? 0, obj });
21
+ entries.push({
22
+ levelId,
23
+ obj,
24
+ });
22
25
  }
23
26
  });
24
- entries.sort((a, b) => a.index - b.index);
27
+ const levelElevations = getLevelElevations(nodes);
25
28
  // Snapshot current Y and visibility so we can restore them after the render
26
29
  const snapshot = new Map(entries.map(({ levelId, obj }) => [levelId, { y: obj.position.y, visible: obj.visible }]));
27
30
  // Snap to true stacked positions and make all levels visible
28
- let cumulativeY = 0;
29
31
  for (const { levelId, obj } of entries) {
30
- obj.position.y = cumulativeY;
32
+ obj.position.y = levelElevations.get(levelId)?.baseY ?? 0;
31
33
  obj.visible = true;
32
- cumulativeY += getLevelHeight(levelId, nodes, (wallId) => sceneRegistry.nodes.get(wallId)?.position.y);
33
34
  }
34
35
  return () => {
35
36
  for (const { levelId, obj } of entries) {
@@ -1,10 +1,11 @@
1
1
  import { type SlabNode, type SlabPolygonContext } from '@pascal-app/core';
2
2
  import * as THREE from 'three';
3
- export declare const SlabSystem: () => null;
4
3
  /**
5
- * Generates extruded slab geometry from polygon. `context` carries the
6
- * slab's level neighbourhood (walls + sibling slabs) driving the per-edge
7
- * render offsets — see `getRenderableSlabPolygon`.
4
+ * Generates slab geometry from polygon. `context` carries the slab's level
5
+ * neighbourhood (walls + sibling slabs) driving the per-edge render offsets —
6
+ * see `getRenderableSlabPolygon`. Branches on the explicit `recessed` intent:
7
+ * a recessed slab is an open shell (pool), everything else a solid occupying
8
+ * `[elevation − thickness, elevation]`.
8
9
  */
9
10
  export declare function generateSlabGeometry(slabNode: SlabNode, context: SlabPolygonContext): THREE.BufferGeometry;
10
11
  //# sourceMappingURL=slab-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAIxB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAe9B,eAAO,MAAM,UAAU,YA2CtB,CAAA;AAuCD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,kBAAkB,GAC1B,KAAK,CAAC,cAAc,CAKtB"}
1
+ {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACxB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,kBAAkB,GAC1B,KAAK,CAAC,cAAc,CAItB"}