@pascal-app/viewer 0.3.1 → 0.3.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ceiling-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/ceiling/ceiling-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,kBAAkB,CAAA;AA+ChE,eAAO,MAAM,eAAe,GAAI,UAAU;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,4CAwB9D,CAAA"}
1
+ {"version":3,"file":"ceiling-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/ceiling/ceiling-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,kBAAkB,CAAA;AAmChE,eAAO,MAAM,eAAe,GAAI,UAAU;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,4CAoC9D,CAAA"}
@@ -1,45 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
3
+ import { useMemo, useRef } from 'react';
4
4
  import { float, mix, positionWorld, smoothstep } from 'three/tsl';
5
5
  import { BackSide, FrontSide, MeshBasicNodeMaterial } from 'three/webgpu';
6
6
  import { useNodeEvents } from '../../../hooks/use-node-events';
7
+ import { DEFAULT_CEILING_MATERIAL } from '../../../lib/materials';
7
8
  import { NodeRenderer } from '../node-renderer';
8
- // TSL material that renders differently based on face direction:
9
- // - Back face (looking up at ceiling from below): solid
10
- // - Front face (looking down at ceiling from above): 30% opacity
11
- const ceilingTopMaterial = new MeshBasicNodeMaterial({
12
- color: 0xb5_a7_8d,
13
- transparent: true,
14
- depthWrite: false,
15
- side: FrontSide,
16
- // Disabled as we only show ceiling grid when needed
17
- // alphaTestNode: float(0.4), // Discard pixels with alpha below 0.4 to create grid lines and not affect depth buffer
18
- });
19
- const ceilingBottomMaterial = new MeshBasicNodeMaterial({
20
- color: 0x99_99_99,
21
- transparent: true,
22
- side: BackSide,
23
- });
24
- // Create grid pattern based on local position
25
- const gridScale = 5; // Grid cells per meter (1 = 1m grid)
9
+ const gridScale = 5;
26
10
  const gridX = positionWorld.x.mul(gridScale).fract();
27
11
  const gridY = positionWorld.z.mul(gridScale).fract();
28
- // Create grid lines - they are at 0 and 1
29
- const lineWidth = 0.05; // Width of grid lines (0-1 range within cell)
30
- // Create visible lines at edges (near 0 and near 1)
12
+ const lineWidth = 0.05;
31
13
  const lineX = smoothstep(lineWidth, 0, gridX).add(smoothstep(1.0 - lineWidth, 1.0, gridX));
32
14
  const lineY = smoothstep(lineWidth, 0, gridY).add(smoothstep(1.0 - lineWidth, 1.0, gridY));
33
- // Combine: if either X or Y is a line, show the line
34
15
  const gridPattern = lineX.max(lineY);
35
- // Grid lines at 0.6 opacity, spaces at 0.2 opacity
36
16
  const gridOpacity = mix(float(0.2), float(0.6), gridPattern);
37
- // faceDirection is 1.0 for front face, -1.0 for back face
38
- // Front face (top, looking down): grid pattern, Back face (bottom, looking up): solid
39
- ceilingTopMaterial.opacityNode = gridOpacity;
17
+ function createCeilingMaterials(color = '#999999') {
18
+ const topMaterial = new MeshBasicNodeMaterial({
19
+ color,
20
+ transparent: true,
21
+ depthWrite: false,
22
+ side: FrontSide,
23
+ });
24
+ topMaterial.opacityNode = gridOpacity;
25
+ const bottomMaterial = new MeshBasicNodeMaterial({
26
+ color,
27
+ transparent: true,
28
+ side: BackSide,
29
+ });
30
+ return { topMaterial, bottomMaterial };
31
+ }
40
32
  export const CeilingRenderer = ({ node }) => {
41
33
  const ref = useRef(null);
42
34
  useRegistry(node.id, 'ceiling', ref);
43
35
  const handlers = useNodeEvents(node, 'ceiling');
44
- return (_jsxs("mesh", { material: ceilingBottomMaterial, ref: ref, children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("mesh", { material: ceilingTopMaterial, name: "ceiling-grid", ...handlers, scale: 0, visible: false, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] }));
36
+ const materials = useMemo(() => {
37
+ const mat = node.material;
38
+ if (mat) {
39
+ const props = mat.properties;
40
+ const color = props?.color || '#999999';
41
+ return createCeilingMaterials(color);
42
+ }
43
+ return {
44
+ topMaterial: createCeilingMaterials().topMaterial,
45
+ bottomMaterial: DEFAULT_CEILING_MATERIAL,
46
+ };
47
+ }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
48
+ return (_jsxs("mesh", { material: materials.bottomMaterial, ref: ref, children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("mesh", { material: materials.topMaterial, name: "ceiling-grid", ...handlers, scale: 0, visible: false, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] }));
45
49
  };
@@ -1 +1 @@
1
- {"version":3,"file":"door-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/door/door-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAe,MAAM,kBAAkB,CAAA;AAK7D,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CAsBxD,CAAA"}
1
+ {"version":3,"file":"door-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/door/door-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,4CA2BxD,CAAA"}
@@ -1,11 +1,18 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
3
+ import { useMemo, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
+ import { createMaterial, DEFAULT_DOOR_MATERIAL } from '../../../lib/materials';
5
6
  export const DoorRenderer = ({ node }) => {
6
7
  const ref = useRef(null);
7
8
  useRegistry(node.id, 'door', ref);
8
9
  const handlers = useNodeEvents(node, 'door');
9
10
  const isTransient = !!node.metadata?.isTransient;
10
- return (_jsxs("mesh", { castShadow: true, position: node.position, receiveShadow: true, ref: ref, rotation: node.rotation, visible: node.visible, ...(isTransient ? {} : handlers), children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("meshStandardMaterial", { color: "#d1d5db" })] }));
11
+ const material = useMemo(() => {
12
+ const mat = node.material;
13
+ if (!mat)
14
+ return DEFAULT_DOOR_MATERIAL;
15
+ return createMaterial(mat);
16
+ }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
17
+ return (_jsx("mesh", { castShadow: true, material: material, position: node.position, receiveShadow: true, ref: ref, rotation: node.rotation, visible: node.visible, ...(isTransient ? {} : handlers), children: _jsx("boxGeometry", { args: [0, 0, 0] }) }));
11
18
  };
@@ -1 +1 @@
1
- {"version":3,"file":"roof-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/roof/roof-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAe,MAAM,kBAAkB,CAAA;AAQ7D,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CA+BxD,CAAA"}
1
+ {"version":3,"file":"roof-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/roof/roof-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAe,MAAM,kBAAkB,CAAA;AAS7D,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CAkCxD,CAAA"}
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
3
+ import { useMemo, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
+ import { createMaterial } from '../../../lib/materials';
5
6
  import useViewer from '../../../store/use-viewer';
6
7
  import { NodeRenderer } from '../node-renderer';
7
8
  import { roofDebugMaterials, roofMaterials } from './roof-materials';
@@ -10,5 +11,12 @@ export const RoofRenderer = ({ node }) => {
10
11
  useRegistry(node.id, 'roof', ref);
11
12
  const handlers = useNodeEvents(node, 'roof');
12
13
  const debugColors = useViewer((s) => s.debugColors);
13
- return (_jsxs("group", { position: node.position, ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: [_jsx("mesh", { castShadow: true, material: debugColors ? roofDebugMaterials : roofMaterials, name: "merged-roof", receiveShadow: true, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), _jsx("group", { name: "segments-wrapper", visible: false, children: (node.children ?? []).map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) })] }));
14
+ const customMaterial = useMemo(() => {
15
+ const mat = node.material;
16
+ if (!mat)
17
+ return null;
18
+ return createMaterial(mat);
19
+ }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
20
+ const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials;
21
+ return (_jsxs("group", { position: node.position, ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: [_jsx("mesh", { castShadow: true, material: material, name: "merged-roof", receiveShadow: true, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), _jsx("group", { name: "segments-wrapper", visible: false, children: (node.children ?? []).map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) })] }));
14
22
  };
@@ -1 +1 @@
1
- {"version":3,"file":"roof-segment-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/roof-segment/roof-segment-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAe,MAAM,kBAAkB,CAAA;AAOpE,eAAO,MAAM,mBAAmB,GAAI,UAAU;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,4CAqBtE,CAAA"}
1
+ {"version":3,"file":"roof-segment-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/roof-segment/roof-segment-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAe,MAAM,kBAAkB,CAAA;AAQpE,eAAO,MAAM,mBAAmB,GAAI,UAAU;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,4CA6BtE,CAAA"}
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
3
+ import { useMemo, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
+ import { createMaterial } from '../../../lib/materials';
5
6
  import useViewer from '../../../store/use-viewer';
6
7
  import { roofDebugMaterials, roofMaterials } from '../roof/roof-materials';
7
8
  export const RoofSegmentRenderer = ({ node }) => {
@@ -9,5 +10,12 @@ export const RoofSegmentRenderer = ({ node }) => {
9
10
  useRegistry(node.id, 'roof-segment', ref);
10
11
  const handlers = useNodeEvents(node, 'roof-segment');
11
12
  const debugColors = useViewer((s) => s.debugColors);
12
- return (_jsx("mesh", { material: debugColors ? roofDebugMaterials : roofMaterials, position: node.position, ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }));
13
+ const customMaterial = useMemo(() => {
14
+ const mat = node.material;
15
+ if (!mat)
16
+ return null;
17
+ return createMaterial(mat);
18
+ }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
19
+ const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials;
20
+ return (_jsx("mesh", { material: material, position: node.position, ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }));
13
21
  };
@@ -1 +1 @@
1
- {"version":3,"file":"slab-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/slab/slab-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAe,MAAM,kBAAkB,CAAA;AAK7D,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CAcxD,CAAA"}
1
+ {"version":3,"file":"slab-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/slab/slab-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,4CAyBxD,CAAA"}
@@ -1,10 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
3
+ import { useMemo, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
+ import { createMaterial, DEFAULT_SLAB_MATERIAL } from '../../../lib/materials';
5
6
  export const SlabRenderer = ({ node }) => {
6
7
  const ref = useRef(null);
7
8
  useRegistry(node.id, 'slab', ref);
8
9
  const handlers = useNodeEvents(node, 'slab');
9
- return (_jsxs("mesh", { castShadow: true, receiveShadow: true, ref: ref, ...handlers, visible: node.visible, children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("meshStandardMaterial", { color: "#e5e5e5" })] }));
10
+ const material = useMemo(() => {
11
+ const mat = node.material;
12
+ if (!mat)
13
+ return DEFAULT_SLAB_MATERIAL;
14
+ return createMaterial(mat);
15
+ }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
16
+ return (_jsx("mesh", { castShadow: true, receiveShadow: true, ref: ref, ...handlers, visible: node.visible, material: material, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }));
10
17
  };
@@ -1 +1 @@
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
+ {"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;AAOvE,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CA6BxD,CAAA"}
@@ -1,15 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRegistry, useScene } from '@pascal-app/core';
3
- import { useLayoutEffect, useRef } from 'react';
3
+ import { useLayoutEffect, useMemo, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
+ import { createMaterial, DEFAULT_WALL_MATERIAL } from '../../../lib/materials';
5
6
  import { NodeRenderer } from '../node-renderer';
6
7
  export const WallRenderer = ({ node }) => {
7
8
  const ref = useRef(null);
8
9
  useRegistry(node.id, 'wall', ref);
9
- // Mark dirty on mount so WallSystem rebuilds geometry when wall (re)appears
10
10
  useLayoutEffect(() => {
11
11
  useScene.getState().markDirty(node.id);
12
12
  }, [node.id]);
13
13
  const handlers = useNodeEvents(node, 'wall');
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)))] }));
14
+ const material = useMemo(() => {
15
+ const mat = node.material;
16
+ if (!mat)
17
+ return DEFAULT_WALL_MATERIAL;
18
+ return createMaterial(mat);
19
+ }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
20
+ return (_jsxs("mesh", { castShadow: true, receiveShadow: true, ref: ref, visible: node.visible, material: material, 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)))] }));
15
21
  };
@@ -1 +1 @@
1
- {"version":3,"file":"window-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/window/window-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAK/D,eAAO,MAAM,cAAc,GAAI,UAAU;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,4CAsB5D,CAAA"}
1
+ {"version":3,"file":"window-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/window/window-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAM/D,eAAO,MAAM,cAAc,GAAI,UAAU;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,4CA2B5D,CAAA"}
@@ -1,11 +1,18 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
3
+ import { useMemo, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
+ import { createMaterial, DEFAULT_WINDOW_MATERIAL } from '../../../lib/materials';
5
6
  export const WindowRenderer = ({ node }) => {
6
7
  const ref = useRef(null);
7
8
  useRegistry(node.id, 'window', ref);
8
9
  const handlers = useNodeEvents(node, 'window');
9
10
  const isTransient = !!node.metadata?.isTransient;
10
- return (_jsxs("mesh", { castShadow: true, position: node.position, receiveShadow: true, ref: ref, rotation: node.rotation, visible: node.visible, ...(isTransient ? {} : handlers), children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("meshStandardMaterial", { color: "#d1d5db" })] }));
11
+ const material = useMemo(() => {
12
+ const mat = node.material;
13
+ if (!mat)
14
+ return DEFAULT_WINDOW_MATERIAL;
15
+ return createMaterial(mat);
16
+ }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
17
+ return (_jsx("mesh", { castShadow: true, material: material, position: node.position, receiveShadow: true, ref: ref, rotation: node.rotation, visible: node.visible, ...(isTransient ? {} : handlers), children: _jsx("boxGeometry", { args: [0, 0, 0] }) }));
11
18
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-gltf-ktx2.d.ts","sourceRoot":"","sources":["../../src/hooks/use-gltf-ktx2.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAU3C,QAAA,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,UAAU,CAAC,OAAO,OAAO,CA2B5D,CAAA;AACD,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"use-gltf-ktx2.d.ts","sourceRoot":"","sources":["../../src/hooks/use-gltf-ktx2.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAU3C,QAAA,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,UAAU,CAAC,OAAO,OAAO,CA2B5D,CAAA;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { default as Viewer } from './components/viewer';
2
2
  export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url';
3
3
  export { SCENE_LAYER, ZONE_LAYER } from './lib/layers';
4
+ export { clearMaterialCache, createDefaultMaterial, createMaterial, DEFAULT_CEILING_MATERIAL, DEFAULT_DOOR_MATERIAL, DEFAULT_ROOF_MATERIAL, DEFAULT_SLAB_MATERIAL, DEFAULT_WALL_MATERIAL, DEFAULT_WINDOW_MATERIAL, disposeMaterial, } from './lib/materials';
4
5
  export { default as useViewer } from './store/use-viewer';
5
6
  export { InteractiveSystem } from './systems/interactive/interactive-system';
6
7
  export { snapLevelsToTruePositions } from './systems/level/level-utils';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,wBAAwB,EACxB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,eAAe,GAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { default as Viewer } from './components/viewer';
2
2
  export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url';
3
3
  export { SCENE_LAYER, ZONE_LAYER } from './lib/layers';
4
+ export { clearMaterialCache, createDefaultMaterial, createMaterial, DEFAULT_CEILING_MATERIAL, DEFAULT_DOOR_MATERIAL, DEFAULT_ROOF_MATERIAL, DEFAULT_SLAB_MATERIAL, DEFAULT_WALL_MATERIAL, DEFAULT_WINDOW_MATERIAL, disposeMaterial, } from './lib/materials';
4
5
  export { default as useViewer } from './store/use-viewer';
5
6
  export { InteractiveSystem } from './systems/interactive/interactive-system';
6
7
  export { snapLevelsToTruePositions } from './systems/level/level-utils';
@@ -0,0 +1,13 @@
1
+ import { type MaterialSchema } from '@pascal-app/core';
2
+ import * as THREE from 'three';
3
+ export declare function createMaterial(material?: MaterialSchema): THREE.MeshStandardMaterial;
4
+ export declare function createDefaultMaterial(color?: string, roughness?: number): THREE.MeshStandardMaterial;
5
+ export declare const DEFAULT_WALL_MATERIAL: THREE.MeshStandardMaterial;
6
+ export declare const DEFAULT_SLAB_MATERIAL: THREE.MeshStandardMaterial;
7
+ export declare const DEFAULT_DOOR_MATERIAL: THREE.MeshStandardMaterial;
8
+ export declare const DEFAULT_WINDOW_MATERIAL: THREE.MeshStandardMaterial;
9
+ export declare const DEFAULT_CEILING_MATERIAL: THREE.MeshStandardMaterial;
10
+ export declare const DEFAULT_ROOF_MATERIAL: THREE.MeshStandardMaterial;
11
+ export declare function disposeMaterial(material: THREE.Material): void;
12
+ export declare function clearMaterialCache(): void;
13
+ //# sourceMappingURL=materials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,cAAc,EAAmB,MAAM,kBAAkB,CAAA;AAChG,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,wBAAgB,cAAc,CAAC,QAAQ,CAAC,EAAE,cAAc,GAAG,KAAK,CAAC,oBAAoB,CAmBpF;AAED,wBAAgB,qBAAqB,CACnC,KAAK,GAAE,MAAkB,EACzB,SAAS,GAAE,MAAY,GACtB,KAAK,CAAC,oBAAoB,CAO5B;AAED,eAAO,MAAM,qBAAqB,4BAAwC,CAAA;AAC1E,eAAO,MAAM,qBAAqB,4BAAwC,CAAA;AAC1E,eAAO,MAAM,qBAAqB,4BAAwC,CAAA;AAC1E,eAAO,MAAM,uBAAuB,4BAOlC,CAAA;AACF,eAAO,MAAM,wBAAwB,4BAAyC,CAAA;AAC9E,eAAO,MAAM,qBAAqB,4BAAyC,CAAA;AAE3E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,GAAG,IAAI,CAE9D;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC"}
@@ -0,0 +1,58 @@
1
+ import { resolveMaterial } from '@pascal-app/core';
2
+ import * as THREE from 'three';
3
+ const sideMap = {
4
+ front: THREE.FrontSide,
5
+ back: THREE.BackSide,
6
+ double: THREE.DoubleSide,
7
+ };
8
+ const materialCache = new Map();
9
+ function getCacheKey(props) {
10
+ return `${props.color}-${props.roughness}-${props.metalness}-${props.opacity}-${props.transparent}-${props.side}`;
11
+ }
12
+ export function createMaterial(material) {
13
+ const props = resolveMaterial(material);
14
+ const cacheKey = getCacheKey(props);
15
+ if (materialCache.has(cacheKey)) {
16
+ return materialCache.get(cacheKey);
17
+ }
18
+ const threeMaterial = new THREE.MeshStandardMaterial({
19
+ color: props.color,
20
+ roughness: props.roughness,
21
+ metalness: props.metalness,
22
+ opacity: props.opacity,
23
+ transparent: props.transparent,
24
+ side: sideMap[props.side],
25
+ });
26
+ materialCache.set(cacheKey, threeMaterial);
27
+ return threeMaterial;
28
+ }
29
+ export function createDefaultMaterial(color = '#ffffff', roughness = 0.9) {
30
+ return new THREE.MeshStandardMaterial({
31
+ color,
32
+ roughness,
33
+ metalness: 0,
34
+ side: THREE.FrontSide,
35
+ });
36
+ }
37
+ export const DEFAULT_WALL_MATERIAL = createDefaultMaterial('#ffffff', 0.9);
38
+ export const DEFAULT_SLAB_MATERIAL = createDefaultMaterial('#e5e5e5', 0.8);
39
+ export const DEFAULT_DOOR_MATERIAL = createDefaultMaterial('#8b4513', 0.7);
40
+ export const DEFAULT_WINDOW_MATERIAL = new THREE.MeshStandardMaterial({
41
+ color: '#87ceeb',
42
+ roughness: 0.1,
43
+ metalness: 0.1,
44
+ opacity: 0.3,
45
+ transparent: true,
46
+ side: THREE.DoubleSide,
47
+ });
48
+ export const DEFAULT_CEILING_MATERIAL = createDefaultMaterial('#f5f5dc', 0.95);
49
+ export const DEFAULT_ROOF_MATERIAL = createDefaultMaterial('#808080', 0.85);
50
+ export function disposeMaterial(material) {
51
+ material.dispose();
52
+ }
53
+ export function clearMaterialCache() {
54
+ for (const material of materialCache.values()) {
55
+ material.dispose();
56
+ }
57
+ materialCache.clear();
58
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAgDA,eAAO,MAAM,UAAU,YAgEtB,CAAA"}
1
+ {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAmIA,eAAO,MAAM,UAAU,YAiFtB,CAAA"}
@@ -7,92 +7,162 @@ import useViewer from '../../store/use-viewer';
7
7
  const tmpVec = new Vector3();
8
8
  const u = new Vector3();
9
9
  const v = new Vector3();
10
- // Dot pattern shader
11
10
  const dotPattern = Fn(() => {
12
- // Create a repeating grid pattern based on world position
13
- const scale = float(0.1); // Dot grid spacing (10cm)
14
- const dotSize = float(0.3); // Size of dots relative to grid
15
- // Use XY coordinates for pattern on wall face
11
+ const scale = float(0.1);
12
+ const dotSize = float(0.3);
16
13
  const uv = vec2(positionLocal.x, positionLocal.y).div(scale);
17
14
  const gridUV = fract(uv);
18
- // Distance from center of grid cell (creates circular dots)
19
15
  const dist = length(gridUV.sub(0.5));
20
- // Create dots: 1 where we want dots, 0 elsewhere
21
16
  const dots = step(dist, dotSize.mul(0.5));
22
- // Vertical fade: fade out as Y increases (from bottom to top)
23
- const fadeHeight = float(2.5); // Fade over 2.5 meters
17
+ const fadeHeight = float(2.5);
24
18
  const yFade = float(1).sub(smoothstep(float(0), fadeHeight, positionLocal.y));
25
19
  return dots.mul(yFade);
26
20
  });
27
- const invsibleWallMaterial = new MeshStandardNodeMaterial({
28
- transparent: true,
29
- opacityNode: mix(float(0.0), float(0.24), dotPattern()),
30
- color: 'white',
31
- depthWrite: false,
32
- emissive: 'white',
33
- });
34
- const wallMaterial = new MeshStandardNodeMaterial({
35
- color: 'white',
36
- roughness: 1,
37
- metalness: 0,
38
- });
21
+ const wallMaterialCache = new Map();
22
+ function getMaterialHash(wallNode) {
23
+ if (!wallNode.material)
24
+ return 'none';
25
+ const mat = wallNode.material;
26
+ if (mat.preset && mat.preset !== 'custom') {
27
+ return `preset-${mat.preset}`;
28
+ }
29
+ if (mat.properties) {
30
+ return `props-${mat.properties.color}-${mat.properties.roughness}-${mat.properties.metalness}`;
31
+ }
32
+ return 'default';
33
+ }
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
+ function getPresetColor(preset) {
46
+ return presetColors[preset] ?? '#ffffff';
47
+ }
48
+ function getMaterialsForWall(wallNode) {
49
+ const cacheKey = wallNode.id;
50
+ const materialHash = getMaterialHash(wallNode);
51
+ const existing = wallMaterialCache.get(cacheKey);
52
+ if (existing && existing.materialHash === materialHash) {
53
+ return existing;
54
+ }
55
+ if (existing) {
56
+ existing.visible.dispose();
57
+ existing.invisible.dispose();
58
+ }
59
+ let userColor = '#ffffff';
60
+ if (wallNode.material?.properties?.color) {
61
+ userColor = wallNode.material.properties.color;
62
+ }
63
+ else if (wallNode.material?.preset && wallNode.material.preset !== 'custom') {
64
+ userColor = getPresetColor(wallNode.material.preset);
65
+ }
66
+ const visibleMat = new MeshStandardNodeMaterial({
67
+ color: userColor,
68
+ roughness: 1,
69
+ metalness: 0,
70
+ });
71
+ const invisibleMat = new MeshStandardNodeMaterial({
72
+ transparent: true,
73
+ opacityNode: mix(float(0.0), float(0.24), dotPattern()),
74
+ color: userColor,
75
+ depthWrite: false,
76
+ emissive: userColor,
77
+ });
78
+ const result = { visible: visibleMat, invisible: invisibleMat, materialHash };
79
+ wallMaterialCache.set(cacheKey, result);
80
+ return result;
81
+ }
82
+ function getWallHideState(wallNode, wallMesh, wallMode, cameraDir) {
83
+ let hideWall = wallNode.frontSide === 'interior' && wallNode.backSide === 'interior';
84
+ if (wallMode === 'up') {
85
+ hideWall = false;
86
+ }
87
+ else if (wallMode === 'down') {
88
+ hideWall = true;
89
+ }
90
+ else {
91
+ wallMesh.getWorldDirection(v);
92
+ if (v.dot(cameraDir) < 0) {
93
+ if (wallNode.frontSide === 'exterior' && wallNode.backSide !== 'exterior') {
94
+ hideWall = true;
95
+ }
96
+ }
97
+ else if (wallNode.backSide === 'exterior' && wallNode.frontSide !== 'exterior') {
98
+ hideWall = true;
99
+ }
100
+ }
101
+ return hideWall;
102
+ }
39
103
  export const WallCutout = () => {
40
104
  const lastCameraPosition = useRef(new Vector3());
41
105
  const lastCameraTarget = useRef(new Vector3());
42
106
  const lastUpdateTime = useRef(0);
43
107
  const lastWallMode = useRef(useViewer.getState().wallMode);
44
108
  const lastNumberOfWalls = useRef(0);
109
+ const lastWallMaterials = useRef(new Map());
45
110
  useFrame(({ camera, clock }) => {
46
111
  const wallMode = useViewer.getState().wallMode;
47
112
  const currentTime = clock.elapsedTime;
48
113
  const currentCameraPosition = camera.position;
49
114
  camera.getWorldDirection(tmpVec);
50
115
  tmpVec.add(currentCameraPosition);
51
- // Throttle: only update if camera moved significantly AND enough time passed
52
116
  const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current);
53
117
  const directionChanged = tmpVec.distanceTo(lastCameraTarget.current);
54
118
  const timeSinceUpdate = currentTime - lastUpdateTime.current;
55
- // Update if moved > 0.5m OR direction changed > 0.3 AND at least 100ms passed
56
- if (((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
119
+ const shouldUpdate = ((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
57
120
  lastWallMode.current !== wallMode ||
58
- sceneRegistry.byType.wall.size !== lastNumberOfWalls.current) {
59
- // Camera has moved, update cutout logic here
60
- // Update last known positions and time
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) {
61
148
  lastCameraPosition.current.copy(currentCameraPosition);
62
149
  lastCameraTarget.current.copy(tmpVec);
63
150
  lastUpdateTime.current = currentTime;
64
151
  camera.getWorldDirection(u);
65
- const walls = sceneRegistry.byType.wall;
66
- walls.forEach((wallId) => {
67
- const wallMesh = sceneRegistry.nodes.get(wallId);
68
- if (!wallMesh)
69
- return;
70
- const wallNode = useScene.getState().nodes[wallId];
71
- if (!wallNode || wallNode.type !== 'wall')
72
- return;
73
- let hideWall = wallNode.frontSide === 'interior' && wallNode.backSide === 'interior';
74
- if (wallMode === 'up') {
75
- hideWall = false;
76
- }
77
- else if (wallMode === 'down') {
78
- hideWall = true;
79
- }
80
- else {
81
- wallMesh.getWorldDirection(v);
82
- if (v.dot(u) < 0) {
83
- // Front side
84
- if (wallNode.frontSide === 'exterior' && wallNode.backSide !== 'exterior') {
85
- hideWall = true;
86
- }
87
- }
88
- else if (wallNode.backSide === 'exterior' && wallNode.frontSide !== 'exterior') {
89
- // Back side
90
- hideWall = true;
91
- }
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);
92
160
  }
93
- ;
94
- wallMesh.material = hideWall ? invsibleWallMaterial : wallMaterial;
95
- });
161
+ }
162
+ lastWallMaterials.current.clear();
163
+ for (const [wallId, mats] of wallMaterialCache) {
164
+ lastWallMaterials.current.set(wallId, mats);
165
+ }
96
166
  lastWallMode.current = wallMode;
97
167
  lastNumberOfWalls.current = sceneRegistry.byType.wall.size;
98
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",