@pascal-app/viewer 0.2.0 → 0.3.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.
@@ -0,0 +1,18 @@
1
+ import type { ErrorInfo, ReactNode } from 'react';
2
+ import { Component } from 'react';
3
+ export declare class ErrorBoundary extends Component<{
4
+ children: ReactNode;
5
+ fallback: ReactNode;
6
+ }, {
7
+ hasError: boolean;
8
+ }> {
9
+ state: {
10
+ hasError: boolean;
11
+ };
12
+ static getDerivedStateFromError(): {
13
+ hasError: boolean;
14
+ };
15
+ componentDidCatch(_e: Error, _i: ErrorInfo): void;
16
+ render(): ReactNode;
17
+ }
18
+ //# sourceMappingURL=error-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../../src/components/error-boundary.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,qBAAa,aAAc,SAAQ,SAAS,CAC1C;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,EAC5C;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CACtB;IACC,KAAK;;MAAsB;IAC3B,MAAM,CAAC,wBAAwB;;;IAG/B,iBAAiB,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS;IAC1C,MAAM;CAGP"}
@@ -0,0 +1,11 @@
1
+ import { Component } from 'react';
2
+ export class ErrorBoundary extends Component {
3
+ state = { hasError: false };
4
+ static getDerivedStateFromError() {
5
+ return { hasError: true };
6
+ }
7
+ componentDidCatch(_e, _i) { }
8
+ render() {
9
+ return this.state.hasError ? this.props.fallback : this.props.children;
10
+ }
11
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"item-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/item/item-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,QAAQ,EAKd,MAAM,kBAAkB,CAAA;AAwCzB,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CAexD,CAAA"}
1
+ {"version":3,"file":"item-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/item/item-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,QAAQ,EAKd,MAAM,kBAAkB,CAAA;AAoDzB,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CAiBxD,CAAA"}
@@ -11,6 +11,7 @@ import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu';
11
11
  import { useNodeEvents } from '../../../hooks/use-node-events';
12
12
  import { resolveCdnUrl } from '../../../lib/asset-url';
13
13
  import { useItemLightPool } from '../../../store/use-item-light-pool';
14
+ import { ErrorBoundary } from '../../error-boundary';
14
15
  import { NodeRenderer } from '../node-renderer';
15
16
  // Shared materials to avoid creating new instances for every mesh
16
17
  const defaultMaterial = new MeshStandardNodeMaterial({
@@ -34,10 +35,15 @@ const getMaterialForOriginal = (original) => {
34
35
  }
35
36
  return defaultMaterial;
36
37
  };
38
+ const BrokenItemFallback = ({ node }) => {
39
+ const handlers = useNodeEvents(node, 'item');
40
+ const [w, h, d] = node.asset.dimensions;
41
+ return (_jsxs("mesh", { "position-y": h / 2, ...handlers, children: [_jsx("boxGeometry", { args: [w, h, d] }), _jsx("meshStandardMaterial", { color: "#ef4444", opacity: 0.6, transparent: true, wireframe: true })] }));
42
+ };
37
43
  export const ItemRenderer = ({ node }) => {
38
44
  const ref = useRef(null);
39
45
  useRegistry(node.id, node.type, ref);
40
- return (_jsxs("group", { position: node.position, ref: ref, rotation: node.rotation, visible: node.visible, children: [_jsx(Suspense, { fallback: _jsx(PreviewModel, { node: node }), children: _jsx(ModelRenderer, { node: node }) }), node.children?.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] }));
46
+ return (_jsxs("group", { position: node.position, ref: ref, rotation: node.rotation, visible: node.visible, children: [_jsx(ErrorBoundary, { fallback: _jsx(BrokenItemFallback, { node: node }), children: _jsx(Suspense, { fallback: _jsx(PreviewModel, { node: node }), children: _jsx(ModelRenderer, { node: node }) }) }), node.children?.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] }));
41
47
  };
42
48
  const previewMaterial = new MeshStandardNodeMaterial({
43
49
  color: '#cccccc',
@@ -1 +1 @@
1
- {"version":3,"file":"wall-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/wall/wall-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAM7D,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CAqBxD,CAAA"}
1
+ {"version":3,"file":"wall-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/wall/wall-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAMvE,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CA0BxD,CAAA"}
@@ -1,11 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
2
+ import { useRegistry, useScene } from '@pascal-app/core';
3
+ import { useLayoutEffect, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
5
  import { NodeRenderer } from '../node-renderer';
6
6
  export const WallRenderer = ({ node }) => {
7
7
  const ref = useRef(null);
8
8
  useRegistry(node.id, 'wall', ref);
9
+ // Mark dirty on mount so WallSystem rebuilds geometry when wall (re)appears
10
+ useLayoutEffect(() => {
11
+ useScene.getState().markDirty(node.id);
12
+ }, [node.id]);
9
13
  const handlers = useNodeEvents(node, 'wall');
10
14
  return (_jsxs("mesh", { castShadow: true, receiveShadow: true, ref: ref, visible: node.visible, children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("mesh", { name: "collision-mesh", visible: false, ...handlers, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] }));
11
15
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ground-occluder.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/ground-occluder.tsx"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc,+CAqE1B,CAAA"}
1
+ {"version":3,"file":"ground-occluder.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/ground-occluder.tsx"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc,+CA+F1B,CAAA"}
@@ -17,12 +17,32 @@ export const GroundOccluder = () => {
17
17
  s.lineTo(size, size);
18
18
  s.lineTo(-size, size);
19
19
  s.closePath();
20
- // Collect all polygons for slabs and zones
20
+ const levelIndexById = new Map();
21
+ let lowestLevelIndex = Number.POSITIVE_INFINITY;
22
+ Object.values(nodes).forEach((node) => {
23
+ if (node.type !== 'level') {
24
+ return;
25
+ }
26
+ levelIndexById.set(node.id, node.level);
27
+ lowestLevelIndex = Math.min(lowestLevelIndex, node.level);
28
+ });
29
+ // Only the lowest level should punch through the ground plane.
30
+ // Upper-level slabs should still cast shadows, but they should not
31
+ // reveal their footprint on the level-zero ground material.
21
32
  const polygons = [];
22
33
  Object.values(nodes).forEach((node) => {
23
- if (node.type === 'slab' && node.polygon && node.polygon.length >= 3) {
24
- polygons.push(node.polygon);
34
+ if (!(node.type === 'slab' && node.visible && node.polygon.length >= 3)) {
35
+ return;
36
+ }
37
+ if (Number.isFinite(lowestLevelIndex)) {
38
+ const parentLevelIndex = node.parentId
39
+ ? levelIndexById.get(node.parentId)
40
+ : undefined;
41
+ if (parentLevelIndex !== lowestLevelIndex) {
42
+ return;
43
+ }
25
44
  }
45
+ polygons.push(node.polygon);
26
46
  });
27
47
  if (polygons.length > 0) {
28
48
  // Format for polygon-clipping: [[[x, y], [x, y], ...]]
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAkB,KAAK,kBAAkB,EAAsB,MAAM,oBAAoB,CAAA;AAEhG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AA2CrC,OAAO,QAAQ,oBAAoB,CAAC;IAClC,UAAU,aAAc,SAAQ,kBAAkB,CAAC,OAAO,KAAK,CAAC;KAAG;CACpE;AA+BD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,QAAA,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CA0DjC,CAAA;AASD,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAkB,KAAK,kBAAkB,EAAsB,MAAM,oBAAoB,CAAA;AAEhG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AA0CrC,OAAO,QAAQ,oBAAoB,CAAC;IAClC,UAAU,aAAc,SAAQ,kBAAkB,CAAC,OAAO,KAAK,CAAC;KAAG;CACpE;AA+BD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,QAAA,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CA6DjC,CAAA;AASD,eAAe,MAAM,CAAA"}
@@ -13,7 +13,6 @@ import { ScanSystem } from '../../systems/scan/scan-system';
13
13
  import { WallCutout } from '../../systems/wall/wall-cutout';
14
14
  import { ZoneSystem } from '../../systems/zone/zone-system';
15
15
  import { SceneRenderer } from '../renderers/scene-renderer';
16
- import { GroundOccluder } from './ground-occluder';
17
16
  import { Lights } from './lights';
18
17
  import { PerfMonitor } from './perf-monitor';
19
18
  import PostProcessing from './post-processing';
@@ -68,11 +67,14 @@ const Viewer = ({ children, selectionManager = 'default', perf = false, }) => {
68
67
  const renderer = new THREE.WebGPURenderer(props);
69
68
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
70
69
  renderer.toneMappingExposure = 0.9;
70
+ // renderer.init() // Only use when using <DebugRenderer />
71
71
  return renderer;
72
+ }, resize: {
73
+ debounce: 100,
72
74
  }, shadows: {
73
75
  type: THREE.PCFShadowMap,
74
76
  enabled: true,
75
- }, children: [_jsx(GroundOccluder, {}), _jsx(ViewerCamera, {}), _jsx(Lights, {}), _jsx(Bvh, { children: _jsx(SceneRenderer, {}) }), _jsx(LevelSystem, {}), _jsx(GuideSystem, {}), _jsx(ScanSystem, {}), _jsx(WallCutout, {}), _jsx(CeilingSystem, {}), _jsx(DoorSystem, {}), _jsx(ItemSystem, {}), _jsx(RoofSystem, {}), _jsx(SlabSystem, {}), _jsx(WallSystem, {}), _jsx(WindowSystem, {}), _jsx(ZoneSystem, {}), _jsx(PostProcessing, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ItemLightSystem, {}), selectionManager === 'default' && _jsx(SelectionManager, {}), perf && _jsx(PerfMonitor, {}), children] }));
77
+ }, children: [_jsx(ViewerCamera, {}), _jsx(Lights, {}), _jsx(Bvh, { children: _jsx(SceneRenderer, {}) }), _jsx(LevelSystem, {}), _jsx(GuideSystem, {}), _jsx(ScanSystem, {}), _jsx(WallCutout, {}), _jsx(CeilingSystem, {}), _jsx(DoorSystem, {}), _jsx(ItemSystem, {}), _jsx(RoofSystem, {}), _jsx(SlabSystem, {}), _jsx(WallSystem, {}), _jsx(WindowSystem, {}), _jsx(ZoneSystem, {}), _jsx(PostProcessing, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ItemLightSystem, {}), selectionManager === 'default' && _jsx(SelectionManager, {}), perf && _jsx(PerfMonitor, {}), children] }));
76
78
  };
77
79
  const DebugRenderer = () => {
78
80
  useFrame(({ gl, scene, camera }) => {
@@ -1 +1 @@
1
- {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA8BA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAQD,QAAA,MAAM,oBAAoB,YA4RzB,CAAA;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAQD,QAAA,MAAM,oBAAoB,YAkRzB,CAAA;AAED,eAAe,oBAAoB,CAAA"}
@@ -3,9 +3,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Color, Layers, UnsignedByteType } from 'three';
4
4
  import { outline } from 'three/addons/tsl/display/OutlineNode.js';
5
5
  import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
6
- import { traa } from 'three/addons/tsl/display/TRAANode.js';
7
6
  import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js';
8
- import { add, colorToDirection, diffuseColor, directionToColor, float, mix, mrt, normalView, oscSine, output, pass, sample, time, uniform, vec4, velocity, } from 'three/tsl';
7
+ import { add, colorToDirection, diffuseColor, directionToColor, float, mix, mrt, normalView, oscSine, output, pass, sample, time, uniform, vec4, } from 'three/tsl';
9
8
  import { RenderPipeline } from 'three/webgpu';
10
9
  import { SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
11
10
  import useViewer from '../../store/use-viewer';
@@ -93,64 +92,58 @@ const PostProcessingPasses = () => {
93
92
  outliner.selectedObjects.length = 0;
94
93
  outliner.hoveredObjects.length = 0;
95
94
  try {
96
- // Scene pass with MRT for SSGI
97
95
  const scenePass = pass(scene, camera);
98
- scenePass.setMRT(mrt({
99
- output,
100
- diffuseColor,
101
- normal: directionToColor(normalView),
102
- velocity,
103
- }));
104
- // Get texture outputs
105
- const scenePassColor = scenePass.getTextureNode('output');
106
- const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
107
- const scenePassDepth = scenePass.getTextureNode('depth');
108
- const scenePassNormal = scenePass.getTextureNode('normal');
109
- const scenePassVelocity = scenePass.getTextureNode('velocity');
110
- // Optimize texture bandwidth
111
- const diffuseTexture = scenePass.getTexture('diffuseColor');
112
- diffuseTexture.type = UnsignedByteType;
113
- const normalTexture = scenePass.getTexture('normal');
114
- normalTexture.type = UnsignedByteType;
115
- // Extract normal from color-encoded texture
116
- const sceneNormal = sample((uv) => {
117
- return colorToDirection(scenePassNormal.sample(uv));
118
- });
119
96
  const zonePass = pass(scene, camera);
120
97
  zonePass.setLayers(zoneLayers);
121
- // SSGI Pass (cast to PerspectiveCamera for SSGI)
122
- const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera);
123
- giPass.sliceCount.value = SSGI_PARAMS.sliceCount;
124
- giPass.stepCount.value = SSGI_PARAMS.stepCount;
125
- giPass.radius.value = SSGI_PARAMS.radius;
126
- giPass.expFactor.value = SSGI_PARAMS.expFactor;
127
- giPass.thickness.value = SSGI_PARAMS.thickness;
128
- giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting;
129
- giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity;
130
- giPass.giIntensity.value = SSGI_PARAMS.giIntensity;
131
- giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
132
- giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
133
- giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
134
- const giTexture = giPass.getTextureNode();
135
- // DenoiseNode only denoises RGB — alpha is passed through unchanged.
136
- // SSGI packs AO into alpha, so we remap it into RGB before denoising.
137
- // convertToTexture() inside denoise() will call rtt() on this vec4 node automatically.
138
- const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1));
139
- const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
140
- denoisePass.index.value = 0;
141
- denoisePass.radius.value = 4;
142
- const gi = giPass.rgb;
143
- const ao = denoisePass.r;
144
- // const gi = giPass.rgb;
145
- // const ao = giPass.a;
98
+ const scenePassColor = scenePass.getTextureNode('output');
146
99
  // Background detection via alpha: renderer clears with alpha=0 (setClearAlpha(0) in useFrame),
147
100
  // so background pixels have scenePassColor.a=0 while geometry pixels have output.a=1.
148
101
  // WebGPU only applies clearColorValue to MRT attachment 0 (output), so scenePassColor.a
149
102
  // is the reliable geometry mask — no normals, no flicker.
150
103
  const hasGeometry = scenePassColor.a;
151
104
  const contentAlpha = hasGeometry.max(zonePass.a);
152
- // Composite: scene * AO + diffuse * GI
153
- const compositePass = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
105
+ let sceneColor = scenePassColor;
106
+ if (SSGI_PARAMS.enabled) {
107
+ // MRT only needed for SSGI (diffuse for GI, normal for SSGI sampling)
108
+ scenePass.setMRT(mrt({
109
+ output,
110
+ diffuseColor,
111
+ normal: directionToColor(normalView),
112
+ }));
113
+ const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
114
+ const scenePassDepth = scenePass.getTextureNode('depth');
115
+ const scenePassNormal = scenePass.getTextureNode('normal');
116
+ // Optimize texture bandwidth
117
+ const diffuseTexture = scenePass.getTexture('diffuseColor');
118
+ diffuseTexture.type = UnsignedByteType;
119
+ const normalTexture = scenePass.getTexture('normal');
120
+ normalTexture.type = UnsignedByteType;
121
+ // Extract normal from color-encoded texture
122
+ const sceneNormal = sample((uv) => colorToDirection(scenePassNormal.sample(uv)));
123
+ const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera);
124
+ giPass.sliceCount.value = SSGI_PARAMS.sliceCount;
125
+ giPass.stepCount.value = SSGI_PARAMS.stepCount;
126
+ giPass.radius.value = SSGI_PARAMS.radius;
127
+ giPass.expFactor.value = SSGI_PARAMS.expFactor;
128
+ giPass.thickness.value = SSGI_PARAMS.thickness;
129
+ giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting;
130
+ giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity;
131
+ giPass.giIntensity.value = SSGI_PARAMS.giIntensity;
132
+ giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
133
+ giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
134
+ giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
135
+ const giTexture = giPass.getTextureNode();
136
+ // DenoiseNode only denoises RGB — alpha is passed through unchanged.
137
+ // SSGI packs AO into alpha, so we remap it into RGB before denoising.
138
+ const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1));
139
+ const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
140
+ denoisePass.index.value = 0;
141
+ denoisePass.radius.value = 4;
142
+ const gi = giPass.rgb;
143
+ const ao = denoisePass.r;
144
+ // Composite: scene * AO + diffuse * GI
145
+ sceneColor = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
146
+ }
154
147
  function generateSelectedOutlinePass() {
155
148
  const edgeStrength = uniform(3);
156
149
  const edgeGlow = uniform(0);
@@ -193,18 +186,8 @@ const PostProcessingPasses = () => {
193
186
  }
194
187
  const selectedOutlinePass = generateSelectedOutlinePass();
195
188
  const hoverOutlinePass = generateHoverOutlinePass();
196
- // Combine composite with outlines BEFORE applying TRAA
197
- const compositeWithOutlines = SSGI_PARAMS.enabled
198
- ? vec4(add(compositePass.rgb, selectedOutlinePass.add(hoverOutlinePass)), compositePass.a)
199
- : vec4(add(scenePassColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), scenePassColor.a);
200
- // TRAA (Temporal Reprojection Anti-Aliasing) - applied AFTER combining everything
201
- const traaOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera);
202
- // For zone-over-background pixels, scenePassDepth=1.0 (no scene geometry) causes TRAA
203
- // to output black. Use hasGeometry to blend: geometry pixels use traaRgb, all others
204
- // (zones over background, pure background) use compositePass.rgb directly.
205
- const traaRgb = traaOutput.rgb;
206
- const colorSource = mix(compositePass.rgb, traaRgb, hasGeometry);
207
- const finalOutput = vec4(mix(bgUniform.current, colorSource, contentAlpha), float(1));
189
+ const compositeWithOutlines = vec4(add(sceneColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), sceneColor.a);
190
+ const finalOutput = vec4(mix(bgUniform.current, compositeWithOutlines.rgb, contentAlpha), float(1));
208
191
  const renderPipeline = new RenderPipeline(renderer);
209
192
  renderPipeline.outputNode = finalOutput;
210
193
  renderPipelineRef.current = renderPipeline;
@@ -1,4 +1,4 @@
1
- export declare const ASSETS_CDN_URL: any;
1
+ export declare const ASSETS_CDN_URL: string;
2
2
  /**
3
3
  * Resolves an asset URL to the appropriate format:
4
4
  * - If URL starts with http:// or https://, return as-is (external URL)
@@ -1 +1 @@
1
- {"version":3,"file":"asset-url.d.ts","sourceRoot":"","sources":["../../src/lib/asset-url.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,KAAwE,CAAA;AAEnG;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB5F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAiB3E"}
1
+ {"version":3,"file":"asset-url.d.ts","sourceRoot":"","sources":["../../src/lib/asset-url.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,QAAwE,CAAA;AAEnG;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB5F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAiB3E"}
@@ -1,5 +1,4 @@
1
1
  import { loadAssetUrl } from '@pascal-app/core';
2
- // @ts-expect-error
3
2
  export const ASSETS_CDN_URL = process.env.NEXT_PUBLIC_ASSETS_CDN_URL || 'https://editor.pascal.app';
4
3
  /**
5
4
  * Resolves an asset URL to the appropriate format:
@@ -18,6 +18,8 @@ type ViewerState = {
18
18
  setCameraMode: (mode: 'perspective' | 'orthographic') => void;
19
19
  theme: 'light' | 'dark';
20
20
  setTheme: (theme: 'light' | 'dark') => void;
21
+ unit: 'metric' | 'imperial';
22
+ setUnit: (unit: 'metric' | 'imperial') => void;
21
23
  levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
22
24
  setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
23
25
  wallMode: 'up' | 'cutaway' | 'down';
@@ -38,8 +40,8 @@ type ViewerState = {
38
40
  setSelection: (updates: Partial<SelectionPath>) => void;
39
41
  resetSelection: () => void;
40
42
  outliner: Outliner;
41
- exportScene: (() => Promise<void>) | null;
42
- setExportScene: (fn: (() => Promise<void>) | null) => void;
43
+ exportScene: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null;
44
+ setExportScene: (fn: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null) => void;
43
45
  debugColors: boolean;
44
46
  setDebugColors: (enabled: boolean) => void;
45
47
  cameraDragging: boolean;
@@ -52,6 +54,7 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
52
54
  setOptions: (options: Partial<import("zustand/middleware").PersistOptions<ViewerState, {
53
55
  cameraMode: "perspective" | "orthographic";
54
56
  theme: "light" | "dark";
57
+ unit: "metric" | "imperial";
55
58
  levelMode: "stacked" | "exploded" | "solo" | "manual";
56
59
  wallMode: "up" | "cutaway" | "down";
57
60
  projectPreferences: Record<string, {
@@ -68,6 +71,7 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
68
71
  getOptions: () => Partial<import("zustand/middleware").PersistOptions<ViewerState, {
69
72
  cameraMode: "perspective" | "orthographic";
70
73
  theme: "light" | "dark";
74
+ unit: "metric" | "imperial";
71
75
  levelMode: "stacked" | "exploded" | "solo" | "manual";
72
76
  wallMode: "up" | "cutaway" | "down";
73
77
  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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"}
@@ -9,6 +9,8 @@ const useViewer = create()(persist((set) => ({
9
9
  setCameraMode: (mode) => set({ cameraMode: mode }),
10
10
  theme: 'light',
11
11
  setTheme: (theme) => set({ theme }),
12
+ unit: 'metric',
13
+ setUnit: (unit) => set({ unit }),
12
14
  levelMode: 'stacked',
13
15
  setLevelMode: (mode) => set({ levelMode: mode }),
14
16
  wallMode: 'up',
@@ -102,6 +104,7 @@ const useViewer = create()(persist((set) => ({
102
104
  partialize: (state) => ({
103
105
  cameraMode: state.cameraMode,
104
106
  theme: state.theme,
107
+ unit: state.unit,
105
108
  levelMode: state.levelMode,
106
109
  wallMode: state.wallMode,
107
110
  projectPreferences: state.projectPreferences,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,9 +34,10 @@
34
34
  },
35
35
  "devDependencies": {
36
36
  "@pascal/typescript-config": "*",
37
+ "@types/node": "^25.5.0",
37
38
  "@types/react": "^19.2.2",
38
- "typescript": "5.9.3",
39
- "@types/three": "^0.183.0"
39
+ "@types/three": "^0.183.0",
40
+ "typescript": "5.9.3"
40
41
  },
41
42
  "keywords": [
42
43
  "3d",