@pascal-app/viewer 0.3.3 → 0.5.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.
Files changed (44) hide show
  1. package/dist/components/renderers/building/building-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/building/building-renderer.js +1 -1
  3. package/dist/components/renderers/item/item-renderer.d.ts.map +1 -1
  4. package/dist/components/renderers/item/item-renderer.js +14 -19
  5. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/node-renderer.js +3 -1
  7. package/dist/components/renderers/site/site-renderer.d.ts.map +1 -1
  8. package/dist/components/renderers/site/site-renderer.js +59 -15
  9. package/dist/components/renderers/slab/slab-renderer.js +1 -1
  10. package/dist/components/renderers/stair/stair-renderer.d.ts +5 -0
  11. package/dist/components/renderers/stair/stair-renderer.d.ts.map +1 -0
  12. package/dist/components/renderers/stair/stair-renderer.js +21 -0
  13. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +5 -0
  14. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +1 -0
  15. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +20 -0
  16. package/dist/components/renderers/wall/wall-renderer.js +1 -1
  17. package/dist/components/viewer/index.d.ts.map +1 -1
  18. package/dist/components/viewer/index.js +2 -3
  19. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  20. package/dist/components/viewer/post-processing.js +33 -45
  21. package/dist/components/viewer/walkthrough-controls.d.ts +2 -0
  22. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -0
  23. package/dist/components/viewer/walkthrough-controls.js +131 -0
  24. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  25. package/dist/hooks/use-node-events.d.ts +9 -1
  26. package/dist/hooks/use-node-events.d.ts.map +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/dist/lib/materials.d.ts +1 -0
  31. package/dist/lib/materials.d.ts.map +1 -1
  32. package/dist/lib/materials.js +1 -0
  33. package/dist/lib/merged-outline-node.d.ts +88 -0
  34. package/dist/lib/merged-outline-node.d.ts.map +1 -0
  35. package/dist/lib/merged-outline-node.js +450 -0
  36. package/dist/lib/suppress-three-clock-warning.d.ts +25 -0
  37. package/dist/lib/suppress-three-clock-warning.d.ts.map +1 -0
  38. package/dist/lib/suppress-three-clock-warning.js +51 -0
  39. package/dist/store/use-viewer.d.ts +6 -0
  40. package/dist/store/use-viewer.d.ts.map +1 -1
  41. package/dist/store/use-viewer.js +8 -1
  42. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  43. package/dist/systems/wall/wall-cutout.js +110 -61
  44. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Suppresses the `THREE.Clock: This module has been deprecated` warning
3
+ * emitted by three.js r183+ on every `new THREE.Clock()` call.
4
+ *
5
+ * We don't instantiate `Clock` ourselves — `@react-three/fiber` 9.x
6
+ * (current stable) creates one internally per `<Canvas>` mount. The
7
+ * migration to `THREE.Timer` lands in R3F 10.x (still alpha).
8
+ *
9
+ * Uses three's `setConsoleFunction` hook so we don't touch `console.warn`
10
+ * globally. Only the exact Clock deprecation message is suppressed; all
11
+ * other three.js logs (including TSL stack-trace warnings) pass through
12
+ * untouched.
13
+ *
14
+ * Runs in production as well as dev — the suppressed message is a pure
15
+ * deprecation notice with no user-actionable content.
16
+ *
17
+ * REMOVAL: safe to delete once `@react-three/fiber` no longer constructs
18
+ * `new THREE.Clock()`. To verify after an R3F upgrade:
19
+ * grep -n "new THREE.Clock" node_modules/@react-three/fiber/dist/*.js
20
+ * No hits → delete this file and its import in components/viewer/index.tsx.
21
+ *
22
+ * @see https://github.com/pascalorg/editor/issues/213
23
+ */
24
+ import { setConsoleFunction } from 'three';
25
+ // three's warn() prepends 'THREE.' to its first argument, and the Clock
26
+ // constructor passes 'THREE.Clock: ...', producing a double-prefixed
27
+ // string. Exact equality keeps the suppression surgical — if three ever
28
+ // rewords this, the filter stops matching and the message resurfaces.
29
+ const CLOCK_DEPRECATION_MESSAGE = 'THREE.THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.';
30
+ // HMR-idempotent install guard. setConsoleFunction is a single-slot global
31
+ // in three, so re-evaluating this module on HMR would reinstall the hook.
32
+ const INSTALLED = Symbol.for('@pascal-app/viewer/suppress-three-clock-warning');
33
+ if (!globalThis[INSTALLED]) {
34
+ ;
35
+ globalThis[INSTALLED] = true;
36
+ setConsoleFunction((method, message, ...params) => {
37
+ if (method === 'warn' && message === CLOCK_DEPRECATION_MESSAGE) {
38
+ return;
39
+ }
40
+ // Mirror three's default stack-trace handling so TSL warnings/errors
41
+ // keep their clickable stack frames.
42
+ if (method !== 'log') {
43
+ const first = params[0];
44
+ if (first?.isStackTrace && typeof first.getError === 'function') {
45
+ console[method](first.getError(message));
46
+ return;
47
+ }
48
+ }
49
+ console[method](message, ...params);
50
+ });
51
+ }
@@ -12,6 +12,10 @@ type Outliner = {
12
12
  };
13
13
  type ViewerState = {
14
14
  selection: SelectionPath;
15
+ previewSelectedIds: BaseNode['id'][];
16
+ setPreviewSelectedIds: (ids: BaseNode['id'][]) => void;
17
+ hoverHighlightMode: 'default' | 'delete';
18
+ setHoverHighlightMode: (mode: 'default' | 'delete') => void;
15
19
  hoveredId: AnyNode['id'] | ZoneNode['id'] | null;
16
20
  setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void;
17
21
  cameraMode: 'perspective' | 'orthographic';
@@ -44,6 +48,8 @@ type ViewerState = {
44
48
  setExportScene: (fn: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null) => void;
45
49
  debugColors: boolean;
46
50
  setDebugColors: (enabled: boolean) => void;
51
+ walkthroughMode: boolean;
52
+ setWalkthroughMode: (mode: boolean) => void;
47
53
  cameraDragging: boolean;
48
54
  setCameraDragging: (dragging: boolean) => void;
49
55
  };
@@ -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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"}
@@ -3,6 +3,10 @@ import { create } from 'zustand';
3
3
  import { persist } from 'zustand/middleware';
4
4
  const useViewer = create()(persist((set) => ({
5
5
  selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
6
+ previewSelectedIds: [],
7
+ setPreviewSelectedIds: (ids) => set({ previewSelectedIds: ids }),
8
+ hoverHighlightMode: 'default',
9
+ setHoverHighlightMode: (mode) => set({ hoverHighlightMode: mode }),
6
10
  hoveredId: null,
7
11
  setHoveredId: (id) => set({ hoveredId: id }),
8
12
  cameraMode: 'perspective',
@@ -82,7 +86,7 @@ const useViewer = create()(persist((set) => ({
82
86
  if (updates.selectedIds === undefined)
83
87
  newSelection.selectedIds = [];
84
88
  }
85
- return { selection: newSelection };
89
+ return { selection: newSelection, previewSelectedIds: [] };
86
90
  }),
87
91
  resetSelection: () => set({
88
92
  selection: {
@@ -91,12 +95,15 @@ const useViewer = create()(persist((set) => ({
91
95
  zoneId: null,
92
96
  selectedIds: [],
93
97
  },
98
+ previewSelectedIds: [],
94
99
  }),
95
100
  outliner: { selectedObjects: [], hoveredObjects: [] },
96
101
  exportScene: null,
97
102
  setExportScene: (fn) => set({ exportScene: fn }),
98
103
  debugColors: false,
99
104
  setDebugColors: (enabled) => set({ debugColors: enabled }),
105
+ walkthroughMode: false,
106
+ setWalkthroughMode: (mode) => set({ walkthroughMode: mode }),
100
107
  cameraDragging: false,
101
108
  setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
102
109
  }), {
@@ -1 +1 @@
1
- {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAmIA,eAAO,MAAM,UAAU,YAiFtB,CAAA"}
1
+ {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAoMA,eAAO,MAAM,UAAU,YAgFtB,CAAA"}
@@ -1,12 +1,26 @@
1
- import { sceneRegistry, useScene } from '@pascal-app/core';
1
+ import { baseMaterial, sceneRegistry, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { useRef } from 'react';
4
+ import { Color } from 'three';
4
5
  import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl';
5
6
  import { MeshStandardNodeMaterial, Vector3 } from 'three/webgpu';
6
7
  import useViewer from '../../store/use-viewer';
7
8
  const tmpVec = new Vector3();
8
9
  const u = new Vector3();
9
10
  const v = new Vector3();
11
+ const DEFAULT_WALL_COLOR = '#f2f0ed';
12
+ const WALL_HIGHLIGHT_PROFILES = {
13
+ delete: {
14
+ color: new Color('#dc2626'),
15
+ blend: 0.78,
16
+ emissiveIntensity: 0.46,
17
+ },
18
+ selection: {
19
+ color: new Color('#818cf8'),
20
+ blend: 0.32,
21
+ emissiveIntensity: 0.42,
22
+ },
23
+ };
10
24
  const dotPattern = Fn(() => {
11
25
  const scale = float(0.1);
12
26
  const dotSize = float(0.3);
@@ -19,6 +33,17 @@ const dotPattern = Fn(() => {
19
33
  return dots.mul(yFade);
20
34
  });
21
35
  const wallMaterialCache = new Map();
36
+ const presetColors = {
37
+ white: '#ffffff',
38
+ brick: '#8b4513',
39
+ concrete: '#808080',
40
+ wood: '#deb887',
41
+ glass: '#87ceeb',
42
+ metal: '#c0c0c0',
43
+ plaster: '#f5f5dc',
44
+ tile: '#dcdcdc',
45
+ marble: '#f5f5f5',
46
+ };
22
47
  function getMaterialHash(wallNode) {
23
48
  if (!wallNode.material)
24
49
  return 'none';
@@ -31,20 +56,22 @@ function getMaterialHash(wallNode) {
31
56
  }
32
57
  return 'default';
33
58
  }
34
- const presetColors = {
35
- white: '#ffffff',
36
- brick: '#8b4513',
37
- concrete: '#808080',
38
- wood: '#deb887',
39
- glass: '#87ceeb',
40
- metal: '#c0c0c0',
41
- plaster: '#f5f5dc',
42
- tile: '#dcdcdc',
43
- marble: '#f5f5f5',
44
- };
45
59
  function getPresetColor(preset) {
46
60
  return presetColors[preset] ?? '#ffffff';
47
61
  }
62
+ function getHighlightedColor(color, kind) {
63
+ const profile = WALL_HIGHLIGHT_PROFILES[kind];
64
+ return new Color(color).lerp(profile.color, profile.blend);
65
+ }
66
+ function createHighlightedWallMaterial(material, baseColor, kind) {
67
+ const highlightedMaterial = material.clone();
68
+ const highlightedColor = getHighlightedColor(baseColor, kind);
69
+ const profile = WALL_HIGHLIGHT_PROFILES[kind];
70
+ highlightedMaterial.color = highlightedColor;
71
+ highlightedMaterial.emissive = highlightedColor.clone();
72
+ highlightedMaterial.emissiveIntensity = profile.emissiveIntensity;
73
+ return highlightedMaterial;
74
+ }
48
75
  function getMaterialsForWall(wallNode) {
49
76
  const cacheKey = wallNode.id;
50
77
  const materialHash = getMaterialHash(wallNode);
@@ -55,19 +82,25 @@ function getMaterialsForWall(wallNode) {
55
82
  if (existing) {
56
83
  existing.visible.dispose();
57
84
  existing.invisible.dispose();
85
+ existing.deleteVisible.dispose();
86
+ existing.deleteInvisible.dispose();
87
+ existing.highlightedVisible.dispose();
88
+ existing.highlightedInvisible.dispose();
58
89
  }
59
- let userColor = '#ffffff';
90
+ let userColor = DEFAULT_WALL_COLOR;
60
91
  if (wallNode.material?.properties?.color) {
61
92
  userColor = wallNode.material.properties.color;
62
93
  }
63
94
  else if (wallNode.material?.preset && wallNode.material.preset !== 'custom') {
64
95
  userColor = getPresetColor(wallNode.material.preset);
65
96
  }
66
- const visibleMat = new MeshStandardNodeMaterial({
67
- color: userColor,
68
- roughness: 1,
69
- metalness: 0,
70
- });
97
+ const visibleMat = wallNode.material
98
+ ? new MeshStandardNodeMaterial({
99
+ color: userColor,
100
+ roughness: 1,
101
+ metalness: 0,
102
+ })
103
+ : baseMaterial.clone();
71
104
  const invisibleMat = new MeshStandardNodeMaterial({
72
105
  transparent: true,
73
106
  opacityNode: mix(float(0.0), float(0.24), dotPattern()),
@@ -75,7 +108,19 @@ function getMaterialsForWall(wallNode) {
75
108
  depthWrite: false,
76
109
  emissive: userColor,
77
110
  });
78
- const result = { visible: visibleMat, invisible: invisibleMat, materialHash };
111
+ const highlightedVisible = createHighlightedWallMaterial(visibleMat, userColor, 'selection');
112
+ const highlightedInvisible = createHighlightedWallMaterial(invisibleMat, userColor, 'selection');
113
+ const deleteVisible = createHighlightedWallMaterial(visibleMat, userColor, 'delete');
114
+ const deleteInvisible = createHighlightedWallMaterial(invisibleMat, userColor, 'delete');
115
+ const result = {
116
+ visible: visibleMat,
117
+ invisible: invisibleMat,
118
+ deleteVisible,
119
+ deleteInvisible,
120
+ highlightedVisible,
121
+ highlightedInvisible,
122
+ materialHash,
123
+ };
79
124
  wallMaterialCache.set(cacheKey, result);
80
125
  return result;
81
126
  }
@@ -106,65 +151,69 @@ export const WallCutout = () => {
106
151
  const lastUpdateTime = useRef(0);
107
152
  const lastWallMode = useRef(useViewer.getState().wallMode);
108
153
  const lastNumberOfWalls = useRef(0);
109
- const lastWallMaterials = useRef(new Map());
154
+ const lastHighlightKey = useRef('');
110
155
  useFrame(({ camera, clock }) => {
111
156
  const wallMode = useViewer.getState().wallMode;
157
+ const selectedIds = useViewer.getState().selection.selectedIds;
158
+ const previewSelectedIds = useViewer.getState().previewSelectedIds;
159
+ const hoveredId = useViewer.getState().hoveredId;
160
+ const hoverHighlightMode = useViewer.getState().hoverHighlightMode;
112
161
  const currentTime = clock.elapsedTime;
113
162
  const currentCameraPosition = camera.position;
114
163
  camera.getWorldDirection(tmpVec);
115
164
  tmpVec.add(currentCameraPosition);
165
+ const highlightedWallIds = new Set([...selectedIds, ...previewSelectedIds].filter((id) => useScene.getState().nodes[id]?.type === 'wall'));
166
+ const deleteHoveredWallId = hoverHighlightMode === 'delete' &&
167
+ hoveredId &&
168
+ useScene.getState().nodes[hoveredId]?.type === 'wall'
169
+ ? hoveredId
170
+ : null;
171
+ const highlightKey = `${Array.from(highlightedWallIds).sort().join('|')}::${deleteHoveredWallId ?? ''}`;
116
172
  const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current);
117
173
  const directionChanged = tmpVec.distanceTo(lastCameraTarget.current);
118
174
  const timeSinceUpdate = currentTime - lastUpdateTime.current;
119
- const shouldUpdate = ((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
175
+ if (((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
120
176
  lastWallMode.current !== wallMode ||
121
- sceneRegistry.byType.wall.size !== lastNumberOfWalls.current;
122
- const walls = sceneRegistry.byType.wall;
123
- const currentWallIds = new Set();
124
- walls.forEach((wallId) => {
125
- const wallMesh = sceneRegistry.nodes.get(wallId);
126
- if (!wallMesh)
127
- return;
128
- const wallNode = useScene.getState().nodes[wallId];
129
- if (!wallNode || wallNode.type !== 'wall')
130
- return;
131
- currentWallIds.add(wallId);
132
- const hideWall = getWallHideState(wallNode, wallMesh, wallMode, u);
133
- if (shouldUpdate) {
134
- const materials = getMaterialsForWall(wallNode);
135
- wallMesh.material = hideWall ? materials.invisible : materials.visible;
136
- }
137
- else {
138
- const currentMaterial = wallMesh.material;
139
- const materials = wallMaterialCache.get(wallId);
140
- if (!materials ||
141
- currentMaterial !== (hideWall ? materials.invisible : materials.visible)) {
142
- const newMaterials = getMaterialsForWall(wallNode);
143
- wallMesh.material = hideWall ? newMaterials.invisible : newMaterials.visible;
144
- }
145
- }
146
- });
147
- if (shouldUpdate) {
177
+ sceneRegistry.byType.wall.size !== lastNumberOfWalls.current ||
178
+ lastHighlightKey.current !== highlightKey) {
148
179
  lastCameraPosition.current.copy(currentCameraPosition);
149
180
  lastCameraTarget.current.copy(tmpVec);
150
181
  lastUpdateTime.current = currentTime;
151
182
  camera.getWorldDirection(u);
152
- if (lastWallMode.current !== wallMode) {
153
- wallMaterialCache.clear();
154
- }
155
- for (const [wallId, mats] of lastWallMaterials.current) {
156
- if (!currentWallIds.has(wallId)) {
157
- mats.visible.dispose();
158
- mats.invisible.dispose();
159
- wallMaterialCache.delete(wallId);
183
+ const walls = sceneRegistry.byType.wall;
184
+ walls.forEach((wallId) => {
185
+ const wallMesh = sceneRegistry.nodes.get(wallId);
186
+ if (!wallMesh)
187
+ return;
188
+ const wallNode = useScene.getState().nodes[wallId];
189
+ if (!wallNode || wallNode.type !== 'wall')
190
+ return;
191
+ const hideWall = getWallHideState(wallNode, wallMesh, wallMode, u);
192
+ const isDeleteHighlighted = deleteHoveredWallId === wallId;
193
+ const isSelectionHighlighted = !isDeleteHighlighted && highlightedWallIds.has(wallId);
194
+ const materials = getMaterialsForWall(wallNode);
195
+ if (hideWall) {
196
+ ;
197
+ wallMesh.material = isDeleteHighlighted
198
+ ? materials.deleteInvisible
199
+ : isSelectionHighlighted
200
+ ? materials.highlightedInvisible
201
+ : materials.invisible;
160
202
  }
161
- }
162
- lastWallMaterials.current.clear();
163
- for (const [wallId, mats] of wallMaterialCache) {
164
- lastWallMaterials.current.set(wallId, mats);
165
- }
203
+ else {
204
+ ;
205
+ wallMesh.material = isDeleteHighlighted
206
+ ? materials.deleteVisible
207
+ : isSelectionHighlighted
208
+ ? materials.highlightedVisible
209
+ : wallNode.material
210
+ ? materials.visible
211
+ : baseMaterial;
212
+ }
213
+ });
166
214
  lastWallMode.current = wallMode;
167
215
  lastNumberOfWalls.current = sceneRegistry.byType.wall.size;
216
+ lastHighlightKey.current = highlightKey;
168
217
  }
169
218
  });
170
219
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",