@pascal-app/viewer 0.2.0 → 0.3.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.
@@ -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], ...]]
@@ -0,0 +1,12 @@
1
+ import type { ThreeEvent } from "@react-three/fiber";
2
+ export declare function useGridEvents(): {
3
+ onPointerDown: (e: ThreeEvent<PointerEvent>) => void;
4
+ onPointerUp: (e: ThreeEvent<PointerEvent>) => void;
5
+ onClick: (e: ThreeEvent<PointerEvent>) => void;
6
+ onPointerEnter: (e: ThreeEvent<PointerEvent>) => void;
7
+ onPointerLeave: (e: ThreeEvent<PointerEvent>) => void;
8
+ onPointerMove: (e: ThreeEvent<PointerEvent>) => void;
9
+ onDoubleClick: (e: ThreeEvent<PointerEvent>) => void;
10
+ onContextMenu: (e: ThreeEvent<PointerEvent>) => void;
11
+ };
12
+ //# sourceMappingURL=use-grid-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-grid-events.d.ts","sourceRoot":"","sources":["../../src/hooks/use-grid-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,wBAAgB,aAAa;uBAYN,UAAU,CAAC,YAAY,CAAC;qBAI1B,UAAU,CAAC,YAAY,CAAC;iBAI5B,UAAU,CAAC,YAAY,CAAC;wBAIjB,UAAU,CAAC,YAAY,CAAC;wBACxB,UAAU,CAAC,YAAY,CAAC;uBACzB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;EAE9C"}
@@ -0,0 +1,33 @@
1
+ import { emitter } from "@pascal-app/core";
2
+ export function useGridEvents() {
3
+ const emit = (suffix, e) => {
4
+ const eventKey = `grid:${suffix}`;
5
+ const payload = {
6
+ position: [e.point.x, e.point.y, e.point.z],
7
+ nativeEvent: e,
8
+ };
9
+ emitter.emit(eventKey, payload);
10
+ };
11
+ return {
12
+ onPointerDown: (e) => {
13
+ if (e.button !== 0)
14
+ return;
15
+ emit("pointerdown", e);
16
+ },
17
+ onPointerUp: (e) => {
18
+ if (e.button !== 0)
19
+ return;
20
+ emit("pointerup", e);
21
+ },
22
+ onClick: (e) => {
23
+ if (e.button !== 0)
24
+ return;
25
+ emit("click", e);
26
+ },
27
+ onPointerEnter: (e) => emit("enter", e),
28
+ onPointerLeave: (e) => emit("leave", e),
29
+ onPointerMove: (e) => emit("move", e),
30
+ onDoubleClick: (e) => emit("double-click", e),
31
+ onContextMenu: (e) => emit("context-menu", e),
32
+ };
33
+ }
@@ -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';
@@ -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":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC5F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAKrC,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACrC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAA;CAC9B,CAAA;AAED,KAAK,QAAQ,GAAG;IACd,eAAe,EAAE,QAAQ,EAAE,CAAA;IAC3B,cAAc,EAAE,QAAQ,EAAE,CAAA;CAC3B,CAAA;AAED,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,aAAa,CAAA;IACxB,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChD,YAAY,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAEjE,UAAU,EAAE,aAAa,GAAG,cAAc,CAAA;IAC1C,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,cAAc,KAAK,IAAI,CAAA;IAE7D,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAA;IAE3C,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;IACrD,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;IAExE,QAAQ,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,WAAW,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,KAAK,IAAI,CAAA;IAEtD,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAErC,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAEtC,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAEpC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACzC,kBAAkB,EAAE,MAAM,CACxB,MAAM,EACN;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAClE,CAAA;IAGD,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,IAAI,CAAA;IACvD,cAAc,EAAE,MAAM,IAAI,CAAA;IAE1B,QAAQ,EAAE,QAAQ,CAAA;IAGlB,WAAW,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;IACzC,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAE1D,WAAW,EAAE,OAAO,CAAA;IACpB,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAE1C,cAAc,EAAE,OAAO,CAAA;IACvB,iBAAiB,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;CAC/C,CAAA;AAED,QAAA,MAAM,SAAS;;;;;;;;;;4BApBG,OAAO;6BAAe,OAAO;2BAAa,OAAO;;;;;;;;;;;;;;4BAAjD,OAAO;6BAAe,OAAO;2BAAa,OAAO;;;;EAiJlE,CAAA;AAED,eAAe,SAAS,CAAA"}
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.0",
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",