@pascal-app/viewer 0.1.2 → 0.1.5
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.
- package/dist/components/renderers/building/building-renderer.d.ts +5 -0
- package/dist/components/renderers/building/building-renderer.d.ts.map +1 -0
- package/dist/components/renderers/building/building-renderer.js +11 -0
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +5 -0
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -0
- package/dist/components/renderers/ceiling/ceiling-renderer.js +24 -0
- package/dist/components/renderers/guide/guide-renderer.d.ts +5 -0
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +1 -0
- package/dist/components/renderers/guide/guide-renderer.js +36 -0
- package/dist/components/renderers/item/item-renderer.d.ts +5 -0
- package/dist/components/renderers/item/item-renderer.d.ts.map +1 -0
- package/dist/components/renderers/item/item-renderer.js +71 -0
- package/dist/components/renderers/level/level-renderer.d.ts +5 -0
- package/dist/components/renderers/level/level-renderer.d.ts.map +1 -0
- package/dist/components/renderers/level/level-renderer.js +11 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -0
- package/dist/components/renderers/node-renderer.d.ts.map +1 -0
- package/dist/components/renderers/node-renderer.js +19 -0
- package/dist/components/renderers/roof/roof-renderer.d.ts +5 -0
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -0
- package/dist/components/renderers/roof/roof-renderer.js +10 -0
- package/dist/components/renderers/scan/scan-renderer.d.ts +5 -0
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +1 -0
- package/dist/components/renderers/scan/scan-renderer.js +50 -0
- package/dist/components/renderers/scene-renderer.d.ts +2 -0
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -0
- package/dist/components/renderers/scene-renderer.js +8 -0
- package/dist/components/renderers/slab/slab-renderer.d.ts +5 -0
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -0
- package/dist/components/renderers/slab/slab-renderer.js +10 -0
- package/dist/components/renderers/wall/wall-renderer.d.ts +5 -0
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -0
- package/dist/components/renderers/wall/wall-renderer.js +11 -0
- package/dist/components/renderers/zone/zone-renderer.d.ts +5 -0
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +1 -0
- package/dist/components/renderers/zone/zone-renderer.js +154 -0
- package/dist/components/viewer/index.d.ts +13 -0
- package/dist/components/viewer/index.d.ts.map +1 -0
- package/dist/components/viewer/index.js +29 -0
- package/dist/components/viewer/lights.d.ts +2 -0
- package/dist/components/viewer/lights.d.ts.map +1 -0
- package/dist/components/viewer/lights.js +10 -0
- package/dist/components/viewer/post-processing.d.ts +17 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -0
- package/dist/components/viewer/post-processing.js +139 -0
- package/dist/components/viewer/selection-manager.d.ts +2 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -0
- package/dist/components/viewer/selection-manager.js +279 -0
- package/dist/components/viewer/viewer-camera.d.ts +2 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -0
- package/dist/components/viewer/viewer-camera.js +7 -0
- package/dist/hooks/use-asset-url.d.ts +6 -0
- package/dist/hooks/use-asset-url.d.ts.map +1 -0
- package/dist/hooks/use-asset-url.js +21 -0
- package/dist/hooks/use-gltf-ktx2.d.ts +4 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -0
- package/dist/hooks/use-gltf-ktx2.js +16 -0
- package/dist/hooks/use-grid-events.d.ts +12 -0
- package/dist/hooks/use-grid-events.d.ts.map +1 -0
- package/dist/hooks/use-grid-events.js +33 -0
- package/dist/hooks/use-node-events.d.ts +49 -0
- package/dist/hooks/use-node-events.d.ts.map +1 -0
- package/dist/hooks/use-node-events.js +38 -0
- package/dist/index.d.ts +5 -81
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -46669
- package/dist/lib/asset-url.d.ts +15 -0
- package/dist/lib/asset-url.d.ts.map +1 -0
- package/dist/lib/asset-url.js +44 -0
- package/dist/store/use-viewer.d.ts +35 -0
- package/dist/store/use-viewer.d.ts.map +1 -0
- package/dist/store/use-viewer.js +46 -0
- package/dist/systems/guide/guide-system.d.ts +2 -0
- package/dist/systems/guide/guide-system.d.ts.map +1 -0
- package/dist/systems/guide/guide-system.js +16 -0
- package/dist/systems/level/level-system.d.ts +2 -0
- package/dist/systems/level/level-system.d.ts.map +1 -0
- package/dist/systems/level/level-system.js +23 -0
- package/dist/systems/scan/scan-system.d.ts +2 -0
- package/dist/systems/scan/scan-system.d.ts.map +1 -0
- package/dist/systems/scan/scan-system.js +16 -0
- package/dist/systems/wall/wall-cutout.d.ts +2 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -0
- package/dist/systems/wall/wall-cutout.js +103 -0
- package/package.json +49 -34
- package/dist/index.js.map +0 -1
- package/types.d.ts +0 -81
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const ASSETS_CDN_URL = "https://pascal-cdn.wawasensei.dev";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves an asset URL to the appropriate format:
|
|
4
|
+
* - If URL starts with http:// or https://, return as-is (external URL)
|
|
5
|
+
* - If URL starts with asset://, resolve from IndexedDB storage
|
|
6
|
+
* - If URL starts with /, prepend CDN URL (absolute path)
|
|
7
|
+
* - Otherwise, prepend CDN URL (relative path)
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveAssetUrl(url: string | undefined | null): Promise<string | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Synchronous version for URLs that don't need IndexedDB resolution
|
|
12
|
+
* Only use this if you're sure the URL is not an asset:// URL
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveCdnUrl(url: string | undefined | null): string | null;
|
|
15
|
+
//# sourceMappingURL=asset-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-url.d.ts","sourceRoot":"","sources":["../../src/lib/asset-url.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,sCAAsC,CAAA;AAEjE;;;;;;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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { loadAssetUrl } from '@pascal-app/core';
|
|
2
|
+
export const ASSETS_CDN_URL = 'https://pascal-cdn.wawasensei.dev';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves an asset URL to the appropriate format:
|
|
5
|
+
* - If URL starts with http:// or https://, return as-is (external URL)
|
|
6
|
+
* - If URL starts with asset://, resolve from IndexedDB storage
|
|
7
|
+
* - If URL starts with /, prepend CDN URL (absolute path)
|
|
8
|
+
* - Otherwise, prepend CDN URL (relative path)
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveAssetUrl(url) {
|
|
11
|
+
if (!url)
|
|
12
|
+
return null;
|
|
13
|
+
// External URL - use as-is
|
|
14
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
// IndexedDB asset - resolve from storage
|
|
18
|
+
if (url.startsWith('asset://')) {
|
|
19
|
+
return loadAssetUrl(url);
|
|
20
|
+
}
|
|
21
|
+
// Absolute or relative path - prepend CDN URL
|
|
22
|
+
const normalizedPath = url.startsWith('/') ? url : `/${url}`;
|
|
23
|
+
return `${ASSETS_CDN_URL}${normalizedPath}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Synchronous version for URLs that don't need IndexedDB resolution
|
|
27
|
+
* Only use this if you're sure the URL is not an asset:// URL
|
|
28
|
+
*/
|
|
29
|
+
export function resolveCdnUrl(url) {
|
|
30
|
+
if (!url)
|
|
31
|
+
return null;
|
|
32
|
+
// External URL - use as-is
|
|
33
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
// Don't use this for asset:// URLs - use resolveAssetUrl instead
|
|
37
|
+
if (url.startsWith('asset://')) {
|
|
38
|
+
console.warn('Use resolveAssetUrl() for asset:// URLs, not resolveCdnUrl()');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// Absolute or relative path - prepend CDN URL
|
|
42
|
+
const normalizedPath = url.startsWith('/') ? url : `/${url}`;
|
|
43
|
+
return `${ASSETS_CDN_URL}${normalizedPath}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from "@pascal-app/core";
|
|
2
|
+
import type { Object3D } from "three";
|
|
3
|
+
type SelectionPath = {
|
|
4
|
+
buildingId: BuildingNode["id"] | null;
|
|
5
|
+
levelId: LevelNode["id"] | null;
|
|
6
|
+
zoneId: ZoneNode["id"] | null;
|
|
7
|
+
selectedIds: BaseNode["id"][];
|
|
8
|
+
};
|
|
9
|
+
type Outliner = {
|
|
10
|
+
selectedObjects: Object3D[];
|
|
11
|
+
hoveredObjects: Object3D[];
|
|
12
|
+
};
|
|
13
|
+
type ViewerState = {
|
|
14
|
+
selection: SelectionPath;
|
|
15
|
+
hoveredId: AnyNode['id'] | ZoneNode['id'] | null;
|
|
16
|
+
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void;
|
|
17
|
+
cameraMode: 'perspective' | 'orthographic';
|
|
18
|
+
setCameraMode: (mode: 'perspective' | 'orthographic') => void;
|
|
19
|
+
levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
|
|
20
|
+
setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
|
|
21
|
+
wallMode: 'up' | 'cutaway' | 'down';
|
|
22
|
+
setWallMode: (mode: 'up' | 'cutaway' | 'down') => void;
|
|
23
|
+
showScans: boolean;
|
|
24
|
+
setShowScans: (show: boolean) => void;
|
|
25
|
+
showGuides: boolean;
|
|
26
|
+
setShowGuides: (show: boolean) => void;
|
|
27
|
+
setSelection: (updates: Partial<SelectionPath>) => void;
|
|
28
|
+
resetSelection: () => void;
|
|
29
|
+
outliner: Outliner;
|
|
30
|
+
exportScene: (() => Promise<void>) | null;
|
|
31
|
+
setExportScene: (fn: (() => Promise<void>) | null) => void;
|
|
32
|
+
};
|
|
33
|
+
declare const useViewer: import("zustand").UseBoundStore<import("zustand").StoreApi<ViewerState>>;
|
|
34
|
+
export default useViewer;
|
|
35
|
+
//# sourceMappingURL=use-viewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;CAC/B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,QAAQ,EAAE,CAAC;CAC5B,CAAC;AAEF,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,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;IAGtC,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;CAC3D,CAAA;AAED,QAAA,MAAM,SAAS,0EAqDZ,CAAC;AAEJ,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { create } from "zustand";
|
|
3
|
+
const useViewer = create()((set, get) => ({
|
|
4
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
5
|
+
hoveredId: null,
|
|
6
|
+
setHoveredId: (id) => set({ hoveredId: id }),
|
|
7
|
+
cameraMode: "perspective",
|
|
8
|
+
setCameraMode: (mode) => set({ cameraMode: mode }),
|
|
9
|
+
levelMode: "stacked",
|
|
10
|
+
setLevelMode: (mode) => set({ levelMode: mode }),
|
|
11
|
+
wallMode: 'cutaway',
|
|
12
|
+
setWallMode: (mode) => set({ wallMode: mode }),
|
|
13
|
+
showScans: true,
|
|
14
|
+
setShowScans: (show) => set({ showScans: show }),
|
|
15
|
+
showGuides: true,
|
|
16
|
+
setShowGuides: (show) => set({ showGuides: show }),
|
|
17
|
+
setSelection: (updates) => set((state) => {
|
|
18
|
+
const newSelection = { ...state.selection, ...updates };
|
|
19
|
+
// Hierarchy Guard: If we change a high-level parent, reset the children
|
|
20
|
+
if (updates.buildingId !== undefined) {
|
|
21
|
+
newSelection.levelId = null;
|
|
22
|
+
newSelection.zoneId = null;
|
|
23
|
+
newSelection.selectedIds = [];
|
|
24
|
+
}
|
|
25
|
+
else if (updates.levelId !== undefined) {
|
|
26
|
+
newSelection.zoneId = null;
|
|
27
|
+
newSelection.selectedIds = [];
|
|
28
|
+
}
|
|
29
|
+
else if (updates.zoneId !== undefined) {
|
|
30
|
+
newSelection.selectedIds = [];
|
|
31
|
+
}
|
|
32
|
+
return { selection: newSelection };
|
|
33
|
+
}),
|
|
34
|
+
resetSelection: () => set({
|
|
35
|
+
selection: {
|
|
36
|
+
buildingId: null,
|
|
37
|
+
levelId: null,
|
|
38
|
+
zoneId: null,
|
|
39
|
+
selectedIds: [],
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
outliner: { selectedObjects: [], hoveredObjects: [] },
|
|
43
|
+
exportScene: null,
|
|
44
|
+
setExportScene: (fn) => set({ exportScene: fn }),
|
|
45
|
+
}));
|
|
46
|
+
export default useViewer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guide-system.d.ts","sourceRoot":"","sources":["../../../src/systems/guide/guide-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,YAavB,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sceneRegistry } from '@pascal-app/core';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import useViewer from '../../store/use-viewer';
|
|
4
|
+
export const GuideSystem = () => {
|
|
5
|
+
const showGuides = useViewer((state) => state.showGuides);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const guides = sceneRegistry.byType.guide || new Set();
|
|
8
|
+
guides.forEach((guideId) => {
|
|
9
|
+
const node = sceneRegistry.nodes.get(guideId);
|
|
10
|
+
if (node) {
|
|
11
|
+
node.visible = showGuides;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}, [showGuides]);
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-system.tsx"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,YAkBvB,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { sceneRegistry, useScene } from '@pascal-app/core';
|
|
2
|
+
import { useFrame } from '@react-three/fiber';
|
|
3
|
+
import { lerp } from 'three/src/math/MathUtils.js';
|
|
4
|
+
import useViewer from '../../store/use-viewer';
|
|
5
|
+
const LEVEL_HEIGHT = 2.5;
|
|
6
|
+
const EXPLODED_GAP = 5;
|
|
7
|
+
export const LevelSystem = () => {
|
|
8
|
+
useFrame((_, delta) => {
|
|
9
|
+
const levelMode = useViewer.getState().levelMode;
|
|
10
|
+
const selectedLevel = useViewer.getState().selection.levelId;
|
|
11
|
+
sceneRegistry.byType.level.forEach((levelId) => {
|
|
12
|
+
const obj = sceneRegistry.nodes.get(levelId);
|
|
13
|
+
if (obj) {
|
|
14
|
+
const level = useScene.getState().nodes[levelId];
|
|
15
|
+
const targetY = (level.level || 0) *
|
|
16
|
+
(LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0));
|
|
17
|
+
obj.position.y = lerp(obj.position.y, targetY, delta * 3);
|
|
18
|
+
obj.visible = levelMode !== 'solo' || level?.id === selectedLevel || !selectedLevel;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan-system.d.ts","sourceRoot":"","sources":["../../../src/systems/scan/scan-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,YAatB,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sceneRegistry } from '@pascal-app/core';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import useViewer from '../../store/use-viewer';
|
|
4
|
+
export const ScanSystem = () => {
|
|
5
|
+
const showScans = useViewer((state) => state.showScans);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const scans = sceneRegistry.byType.scan || new Set();
|
|
8
|
+
scans.forEach((scanId) => {
|
|
9
|
+
const node = sceneRegistry.nodes.get(scanId);
|
|
10
|
+
if (node) {
|
|
11
|
+
node.visible = showScans;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}, [showScans]);
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAgDA,eAAO,MAAM,UAAU,YAkEtB,CAAA"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { sceneRegistry, useScene } from '@pascal-app/core';
|
|
2
|
+
import { useFrame } from '@react-three/fiber';
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl';
|
|
5
|
+
import { MeshStandardNodeMaterial, Vector3 } from 'three/webgpu';
|
|
6
|
+
import useViewer from '../../store/use-viewer';
|
|
7
|
+
const tmpVec = new Vector3();
|
|
8
|
+
const u = new Vector3();
|
|
9
|
+
const v = new Vector3();
|
|
10
|
+
// Dot pattern shader
|
|
11
|
+
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
|
|
16
|
+
const uv = vec2(positionLocal.x, positionLocal.y).div(scale);
|
|
17
|
+
const gridUV = fract(uv);
|
|
18
|
+
// Distance from center of grid cell (creates circular dots)
|
|
19
|
+
const dist = length(gridUV.sub(0.5));
|
|
20
|
+
// Create dots: 1 where we want dots, 0 elsewhere
|
|
21
|
+
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
|
|
24
|
+
const yFade = float(1).sub(smoothstep(float(0), fadeHeight, positionLocal.y));
|
|
25
|
+
return dots.mul(yFade);
|
|
26
|
+
});
|
|
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
|
+
});
|
|
39
|
+
export const WallCutout = () => {
|
|
40
|
+
const lastCameraPosition = useRef(new Vector3());
|
|
41
|
+
const lastCameraTarget = useRef(new Vector3());
|
|
42
|
+
const lastUpdateTime = useRef(0);
|
|
43
|
+
const lastWallMode = useRef(useViewer.getState().wallMode);
|
|
44
|
+
const lastNumberOfWalls = useRef(0);
|
|
45
|
+
useFrame(({ camera, clock }) => {
|
|
46
|
+
const wallMode = useViewer.getState().wallMode;
|
|
47
|
+
const currentTime = clock.elapsedTime;
|
|
48
|
+
const currentCameraPosition = camera.position;
|
|
49
|
+
camera.getWorldDirection(tmpVec);
|
|
50
|
+
tmpVec.add(currentCameraPosition);
|
|
51
|
+
// Throttle: only update if camera moved significantly AND enough time passed
|
|
52
|
+
const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current);
|
|
53
|
+
const directionChanged = tmpVec.distanceTo(lastCameraTarget.current);
|
|
54
|
+
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) ||
|
|
57
|
+
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
|
|
61
|
+
lastCameraPosition.current.copy(currentCameraPosition);
|
|
62
|
+
lastCameraTarget.current.copy(tmpVec);
|
|
63
|
+
lastUpdateTime.current = currentTime;
|
|
64
|
+
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 {
|
|
89
|
+
// Back side
|
|
90
|
+
if (wallNode.backSide === 'exterior' && wallNode.frontSide !== 'exterior') {
|
|
91
|
+
hideWall = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
;
|
|
96
|
+
wallMesh.material = hideWall ? invsibleWallMaterial : wallMaterial;
|
|
97
|
+
});
|
|
98
|
+
lastWallMode.current = wallMode;
|
|
99
|
+
lastNumberOfWalls.current = sceneRegistry.byType.wall.size;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
};
|
package/package.json
CHANGED
|
@@ -1,51 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/viewer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"private": false,
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "3D viewer component for Pascal building editor",
|
|
6
5
|
"type": "module",
|
|
7
|
-
"main": "./
|
|
8
|
-
"
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
10
8
|
"exports": {
|
|
11
9
|
".": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
14
13
|
}
|
|
15
14
|
},
|
|
16
15
|
"files": [
|
|
17
16
|
"dist",
|
|
18
|
-
"
|
|
17
|
+
"README.md"
|
|
19
18
|
],
|
|
20
|
-
"sideEffects": false,
|
|
21
19
|
"scripts": {
|
|
22
|
-
"build": "
|
|
23
|
-
"
|
|
24
|
-
"typecheck": "tsc --noEmit"
|
|
20
|
+
"build": "tsc --declaration --emitDeclarationOnly && tsc",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
25
22
|
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"three-custom-shader-material": "^6.1.0",
|
|
37
|
-
"zod": "^4.2.1",
|
|
38
|
-
"zustand": "^5.0.9"
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
39
33
|
},
|
|
40
34
|
"peerDependencies": {
|
|
41
|
-
"
|
|
42
|
-
"react-
|
|
43
|
-
"three": "
|
|
44
|
-
"
|
|
35
|
+
"@pascal-app/core": "^0.1.4",
|
|
36
|
+
"@react-three/drei": "^10",
|
|
37
|
+
"@react-three/fiber": "^9",
|
|
38
|
+
"react": "^18 || ^19",
|
|
39
|
+
"three": "^0.182"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"zustand": "^5"
|
|
45
43
|
},
|
|
46
44
|
"devDependencies": {
|
|
47
|
-
"@
|
|
48
|
-
"@types/react": "^19.
|
|
49
|
-
"
|
|
50
|
-
|
|
45
|
+
"@repo/typescript-config": "*",
|
|
46
|
+
"@types/react": "^19.2.2",
|
|
47
|
+
"typescript": "5.9.2",
|
|
48
|
+
"@types/three": "^0.182.0"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"3d",
|
|
52
|
+
"building",
|
|
53
|
+
"editor",
|
|
54
|
+
"viewer",
|
|
55
|
+
"architecture",
|
|
56
|
+
"webgpu",
|
|
57
|
+
"three.js",
|
|
58
|
+
"react-three-fiber"
|
|
59
|
+
],
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "https://github.com/your-username/pascal-editor.git",
|
|
63
|
+
"directory": "packages/viewer"
|
|
64
|
+
},
|
|
65
|
+
"license": "MIT"
|
|
51
66
|
}
|