@pascal-app/viewer 0.1.13 → 0.2.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.
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +16 -9
- package/dist/components/renderers/door/door-renderer.d.ts +5 -0
- package/dist/components/renderers/door/door-renderer.d.ts.map +1 -0
- package/dist/components/renderers/door/door-renderer.js +11 -0
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +1 -1
- package/dist/components/renderers/guide/guide-renderer.js +4 -2
- package/dist/components/renderers/item/item-renderer.d.ts.map +1 -1
- package/dist/components/renderers/item/item-renderer.js +92 -7
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +3 -1
- package/dist/components/renderers/roof/roof-materials.d.ts +4 -0
- package/dist/components/renderers/roof/roof-materials.d.ts.map +1 -0
- package/dist/components/renderers/roof/roof-materials.js +16 -0
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
- package/dist/components/renderers/roof/roof-renderer.js +5 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +5 -0
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -0
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +13 -0
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +1 -1
- package/dist/components/renderers/scan/scan-renderer.js +3 -1
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
- package/dist/components/renderers/scene-renderer.js +3 -3
- package/dist/components/renderers/site/site-renderer.d.ts.map +1 -1
- package/dist/components/renderers/site/site-renderer.js +4 -19
- package/dist/components/renderers/slab/slab-renderer.js +1 -1
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -1
- package/dist/components/renderers/wall/wall-renderer.js +1 -1
- package/dist/components/renderers/window/window-renderer.d.ts.map +1 -1
- package/dist/components/renderers/window/window-renderer.js +2 -1
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +1 -1
- package/dist/components/renderers/zone/zone-renderer.js +33 -13
- package/dist/components/viewer/ground-occluder.d.ts +2 -0
- package/dist/components/viewer/ground-occluder.d.ts.map +1 -0
- package/dist/components/viewer/ground-occluder.js +55 -0
- package/dist/components/viewer/index.d.ts +1 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +59 -6
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +69 -5
- package/dist/components/viewer/perf-monitor.d.ts +2 -0
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -0
- package/dist/components/viewer/perf-monitor.js +42 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +230 -107
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +47 -17
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +2 -2
- package/dist/hooks/use-gltf-ktx2.d.ts +1 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +25 -7
- package/dist/hooks/use-node-events.d.ts +9 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +5 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/lib/asset-url.d.ts +1 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +2 -1
- package/dist/lib/layers.d.ts +5 -0
- package/dist/lib/layers.d.ts.map +1 -0
- package/dist/lib/layers.js +4 -0
- package/dist/store/use-item-light-pool.d.ts +18 -0
- package/dist/store/use-item-light-pool.d.ts.map +1 -0
- package/dist/store/use-item-light-pool.js +30 -0
- package/dist/store/use-viewer.d.ts +52 -7
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +79 -17
- package/dist/systems/interactive/interactive-system.d.ts +2 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -0
- package/dist/systems/interactive/interactive-system.js +95 -0
- package/dist/systems/item-light/item-light-system.d.ts +2 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -0
- package/dist/systems/item-light/item-light-system.js +235 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -8
- package/dist/systems/level/level-utils.d.ts +17 -0
- package/dist/systems/level/level-utils.d.ts.map +1 -0
- package/dist/systems/level/level-utils.js +82 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +2 -4
- package/dist/systems/zone/zone-system.d.ts.map +1 -1
- package/dist/systems/zone/zone-system.js +29 -3
- package/package.json +6 -5
package/dist/lib/asset-url.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-url.d.ts","sourceRoot":"","sources":["../../src/lib/asset-url.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/lib/asset-url.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadAssetUrl } from '@pascal-app/core';
|
|
2
|
-
|
|
2
|
+
// @ts-expect-error
|
|
3
|
+
export const ASSETS_CDN_URL = process.env.NEXT_PUBLIC_ASSETS_CDN_URL || 'https://editor.pascal.app';
|
|
3
4
|
/**
|
|
4
5
|
* Resolves an asset URL to the appropriate format:
|
|
5
6
|
* - If URL starts with http:// or https://, return as-is (external URL)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/lib/layers.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B,oEAAoE;AACpE,eAAO,MAAM,UAAU,IAAI,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnyNodeId, Interactive, LightEffect } from '@pascal-app/core';
|
|
2
|
+
export type LightRegistration = {
|
|
3
|
+
nodeId: AnyNodeId;
|
|
4
|
+
effect: LightEffect;
|
|
5
|
+
toggleIndex: number;
|
|
6
|
+
sliderIndex: number;
|
|
7
|
+
sliderMin: number;
|
|
8
|
+
sliderMax: number;
|
|
9
|
+
hasSlider: boolean;
|
|
10
|
+
};
|
|
11
|
+
type ItemLightPoolStore = {
|
|
12
|
+
registrations: Map<string, LightRegistration>;
|
|
13
|
+
register: (key: string, nodeId: AnyNodeId, effect: LightEffect, interactive: Interactive) => void;
|
|
14
|
+
unregister: (key: string) => void;
|
|
15
|
+
};
|
|
16
|
+
export declare const useItemLightPool: import("zustand").UseBoundStore<import("zustand").StoreApi<ItemLightPoolStore>>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=use-item-light-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-item-light-pool.d.ts","sourceRoot":"","sources":["../../src/store/use-item-light-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAiB,MAAM,kBAAkB,CAAA;AAG1F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,SAAS,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAC7C,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,KAAK,IAAI,CAAA;IACjG,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC,CAAA;AAED,eAAO,MAAM,gBAAgB,iFAiC1B,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
export const useItemLightPool = create((set) => ({
|
|
3
|
+
registrations: new Map(),
|
|
4
|
+
register: (key, nodeId, effect, interactive) => {
|
|
5
|
+
const toggleIndex = interactive.controls.findIndex((c) => c.kind === 'toggle');
|
|
6
|
+
const sliderIndex = interactive.controls.findIndex((c) => c.kind === 'slider');
|
|
7
|
+
const sliderControl = sliderIndex >= 0 ? interactive.controls[sliderIndex] : null;
|
|
8
|
+
const registration = {
|
|
9
|
+
nodeId,
|
|
10
|
+
effect,
|
|
11
|
+
toggleIndex,
|
|
12
|
+
sliderIndex,
|
|
13
|
+
hasSlider: sliderControl !== null,
|
|
14
|
+
sliderMin: sliderControl?.min ?? 0,
|
|
15
|
+
sliderMax: sliderControl?.max ?? 1,
|
|
16
|
+
};
|
|
17
|
+
set((s) => {
|
|
18
|
+
const next = new Map(s.registrations);
|
|
19
|
+
next.set(key, registration);
|
|
20
|
+
return { registrations: next };
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
unregister: (key) => {
|
|
24
|
+
set((s) => {
|
|
25
|
+
const next = new Map(s.registrations);
|
|
26
|
+
next.delete(key);
|
|
27
|
+
return { registrations: next };
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from
|
|
2
|
-
import type { Object3D } from
|
|
1
|
+
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core';
|
|
2
|
+
import type { Object3D } from 'three';
|
|
3
3
|
type SelectionPath = {
|
|
4
|
-
buildingId: BuildingNode[
|
|
5
|
-
levelId: LevelNode[
|
|
6
|
-
zoneId: ZoneNode[
|
|
7
|
-
selectedIds: BaseNode[
|
|
4
|
+
buildingId: BuildingNode['id'] | null;
|
|
5
|
+
levelId: LevelNode['id'] | null;
|
|
6
|
+
zoneId: ZoneNode['id'] | null;
|
|
7
|
+
selectedIds: BaseNode['id'][];
|
|
8
8
|
};
|
|
9
9
|
type Outliner = {
|
|
10
10
|
selectedObjects: Object3D[];
|
|
@@ -16,6 +16,8 @@ type ViewerState = {
|
|
|
16
16
|
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void;
|
|
17
17
|
cameraMode: 'perspective' | 'orthographic';
|
|
18
18
|
setCameraMode: (mode: 'perspective' | 'orthographic') => void;
|
|
19
|
+
theme: 'light' | 'dark';
|
|
20
|
+
setTheme: (theme: 'light' | 'dark') => void;
|
|
19
21
|
levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
|
|
20
22
|
setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
|
|
21
23
|
wallMode: 'up' | 'cutaway' | 'down';
|
|
@@ -24,14 +26,57 @@ type ViewerState = {
|
|
|
24
26
|
setShowScans: (show: boolean) => void;
|
|
25
27
|
showGuides: boolean;
|
|
26
28
|
setShowGuides: (show: boolean) => void;
|
|
29
|
+
showGrid: boolean;
|
|
30
|
+
setShowGrid: (show: boolean) => void;
|
|
31
|
+
projectId: string | null;
|
|
32
|
+
setProjectId: (id: string | null) => void;
|
|
33
|
+
projectPreferences: Record<string, {
|
|
34
|
+
showScans?: boolean;
|
|
35
|
+
showGuides?: boolean;
|
|
36
|
+
showGrid?: boolean;
|
|
37
|
+
}>;
|
|
27
38
|
setSelection: (updates: Partial<SelectionPath>) => void;
|
|
28
39
|
resetSelection: () => void;
|
|
29
40
|
outliner: Outliner;
|
|
30
41
|
exportScene: (() => Promise<void>) | null;
|
|
31
42
|
setExportScene: (fn: (() => Promise<void>) | null) => void;
|
|
43
|
+
debugColors: boolean;
|
|
44
|
+
setDebugColors: (enabled: boolean) => void;
|
|
32
45
|
cameraDragging: boolean;
|
|
33
46
|
setCameraDragging: (dragging: boolean) => void;
|
|
34
47
|
};
|
|
35
|
-
declare const useViewer: import("zustand").UseBoundStore<import("zustand").StoreApi<ViewerState
|
|
48
|
+
declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<ViewerState>, "setState" | "persist"> & {
|
|
49
|
+
setState(partial: ViewerState | Partial<ViewerState> | ((state: ViewerState) => ViewerState | Partial<ViewerState>), replace?: false | undefined): unknown;
|
|
50
|
+
setState(state: ViewerState | ((state: ViewerState) => ViewerState), replace: true): unknown;
|
|
51
|
+
persist: {
|
|
52
|
+
setOptions: (options: Partial<import("zustand/middleware").PersistOptions<ViewerState, {
|
|
53
|
+
cameraMode: "perspective" | "orthographic";
|
|
54
|
+
theme: "light" | "dark";
|
|
55
|
+
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
|
56
|
+
wallMode: "up" | "cutaway" | "down";
|
|
57
|
+
projectPreferences: Record<string, {
|
|
58
|
+
showScans?: boolean;
|
|
59
|
+
showGuides?: boolean;
|
|
60
|
+
showGrid?: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
}, unknown>>) => void;
|
|
63
|
+
clearStorage: () => void;
|
|
64
|
+
rehydrate: () => Promise<void> | void;
|
|
65
|
+
hasHydrated: () => boolean;
|
|
66
|
+
onHydrate: (fn: (state: ViewerState) => void) => () => void;
|
|
67
|
+
onFinishHydration: (fn: (state: ViewerState) => void) => () => void;
|
|
68
|
+
getOptions: () => Partial<import("zustand/middleware").PersistOptions<ViewerState, {
|
|
69
|
+
cameraMode: "perspective" | "orthographic";
|
|
70
|
+
theme: "light" | "dark";
|
|
71
|
+
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
|
72
|
+
wallMode: "up" | "cutaway" | "down";
|
|
73
|
+
projectPreferences: Record<string, {
|
|
74
|
+
showScans?: boolean;
|
|
75
|
+
showGuides?: boolean;
|
|
76
|
+
showGrid?: boolean;
|
|
77
|
+
}>;
|
|
78
|
+
}, unknown>>;
|
|
79
|
+
};
|
|
80
|
+
}>;
|
|
36
81
|
export default useViewer;
|
|
37
82
|
//# sourceMappingURL=use-viewer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"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"}
|
package/dist/store/use-viewer.js
CHANGED
|
@@ -1,33 +1,84 @@
|
|
|
1
|
-
|
|
2
|
-
import { create } from
|
|
3
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { persist } from 'zustand/middleware';
|
|
4
|
+
const useViewer = create()(persist((set) => ({
|
|
4
5
|
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
5
6
|
hoveredId: null,
|
|
6
7
|
setHoveredId: (id) => set({ hoveredId: id }),
|
|
7
|
-
cameraMode:
|
|
8
|
+
cameraMode: 'perspective',
|
|
8
9
|
setCameraMode: (mode) => set({ cameraMode: mode }),
|
|
9
|
-
|
|
10
|
+
theme: 'light',
|
|
11
|
+
setTheme: (theme) => set({ theme }),
|
|
12
|
+
levelMode: 'stacked',
|
|
10
13
|
setLevelMode: (mode) => set({ levelMode: mode }),
|
|
11
|
-
wallMode: '
|
|
14
|
+
wallMode: 'up',
|
|
12
15
|
setWallMode: (mode) => set({ wallMode: mode }),
|
|
13
16
|
showScans: true,
|
|
14
|
-
setShowScans: (show) => set(
|
|
17
|
+
setShowScans: (show) => set((state) => {
|
|
18
|
+
const projectPreferences = { ...(state.projectPreferences || {}) };
|
|
19
|
+
if (state.projectId) {
|
|
20
|
+
projectPreferences[state.projectId] = {
|
|
21
|
+
...(projectPreferences[state.projectId] || {}),
|
|
22
|
+
showScans: show,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { showScans: show, projectPreferences };
|
|
26
|
+
}),
|
|
15
27
|
showGuides: true,
|
|
16
|
-
setShowGuides: (show) => set(
|
|
28
|
+
setShowGuides: (show) => set((state) => {
|
|
29
|
+
const projectPreferences = { ...(state.projectPreferences || {}) };
|
|
30
|
+
if (state.projectId) {
|
|
31
|
+
projectPreferences[state.projectId] = {
|
|
32
|
+
...(projectPreferences[state.projectId] || {}),
|
|
33
|
+
showGuides: show,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { showGuides: show, projectPreferences };
|
|
37
|
+
}),
|
|
38
|
+
showGrid: true,
|
|
39
|
+
setShowGrid: (show) => set((state) => {
|
|
40
|
+
const projectPreferences = { ...(state.projectPreferences || {}) };
|
|
41
|
+
if (state.projectId) {
|
|
42
|
+
projectPreferences[state.projectId] = {
|
|
43
|
+
...(projectPreferences[state.projectId] || {}),
|
|
44
|
+
showGrid: show,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { showGrid: show, projectPreferences };
|
|
48
|
+
}),
|
|
49
|
+
projectId: null,
|
|
50
|
+
setProjectId: (id) => set((state) => {
|
|
51
|
+
if (!id)
|
|
52
|
+
return { projectId: id };
|
|
53
|
+
const prefs = state.projectPreferences?.[id] || {};
|
|
54
|
+
return {
|
|
55
|
+
projectId: id,
|
|
56
|
+
showScans: prefs.showScans ?? true,
|
|
57
|
+
showGuides: prefs.showGuides ?? true,
|
|
58
|
+
showGrid: prefs.showGrid ?? true,
|
|
59
|
+
};
|
|
60
|
+
}),
|
|
61
|
+
projectPreferences: {},
|
|
17
62
|
setSelection: (updates) => set((state) => {
|
|
18
63
|
const newSelection = { ...state.selection, ...updates };
|
|
19
|
-
// Hierarchy Guard: If we change a high-level parent, reset the children
|
|
64
|
+
// Hierarchy Guard: If we change a high-level parent, reset the children unless explicitly provided
|
|
20
65
|
if (updates.buildingId !== undefined) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
66
|
+
if (updates.levelId === undefined)
|
|
67
|
+
newSelection.levelId = null;
|
|
68
|
+
if (updates.zoneId === undefined)
|
|
69
|
+
newSelection.zoneId = null;
|
|
70
|
+
if (updates.selectedIds === undefined)
|
|
71
|
+
newSelection.selectedIds = [];
|
|
24
72
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
73
|
+
if (updates.levelId !== undefined) {
|
|
74
|
+
if (updates.zoneId === undefined)
|
|
75
|
+
newSelection.zoneId = null;
|
|
76
|
+
if (updates.selectedIds === undefined)
|
|
77
|
+
newSelection.selectedIds = [];
|
|
28
78
|
}
|
|
29
|
-
|
|
30
|
-
|
|
79
|
+
if (updates.zoneId !== undefined) {
|
|
80
|
+
if (updates.selectedIds === undefined)
|
|
81
|
+
newSelection.selectedIds = [];
|
|
31
82
|
}
|
|
32
83
|
return { selection: newSelection };
|
|
33
84
|
}),
|
|
@@ -42,7 +93,18 @@ const useViewer = create()((set, get) => ({
|
|
|
42
93
|
outliner: { selectedObjects: [], hoveredObjects: [] },
|
|
43
94
|
exportScene: null,
|
|
44
95
|
setExportScene: (fn) => set({ exportScene: fn }),
|
|
96
|
+
debugColors: false,
|
|
97
|
+
setDebugColors: (enabled) => set({ debugColors: enabled }),
|
|
45
98
|
cameraDragging: false,
|
|
46
99
|
setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
|
|
100
|
+
}), {
|
|
101
|
+
name: 'viewer-preferences',
|
|
102
|
+
partialize: (state) => ({
|
|
103
|
+
cameraMode: state.cameraMode,
|
|
104
|
+
theme: state.theme,
|
|
105
|
+
levelMode: state.levelMode,
|
|
106
|
+
wallMode: state.wallMode,
|
|
107
|
+
projectPreferences: state.projectPreferences,
|
|
108
|
+
}),
|
|
47
109
|
}));
|
|
48
110
|
export default useViewer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"AAwBA,eAAO,MAAM,iBAAiB,+CAgB7B,CAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { pointInPolygon, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
|
|
4
|
+
import { Html } from '@react-three/drei';
|
|
5
|
+
import { createPortal, useFrame } from '@react-three/fiber';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { Vector3 } from 'three';
|
|
8
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
9
|
+
import useViewer from '../../store/use-viewer';
|
|
10
|
+
const _tempVec = new Vector3();
|
|
11
|
+
// ---- Parent: one overlay per interactive item ----
|
|
12
|
+
export const InteractiveSystem = () => {
|
|
13
|
+
const interactiveNodeIds = useScene(useShallow((state) => Object.values(state.nodes)
|
|
14
|
+
.filter((n) => n.type === 'item' && n.asset.interactive != null)
|
|
15
|
+
.map((n) => n.id)));
|
|
16
|
+
return (_jsx(_Fragment, { children: interactiveNodeIds.map((id) => (_jsx(ItemControlsOverlay, { nodeId: id }, id))) }));
|
|
17
|
+
};
|
|
18
|
+
// ---- Child: polls sceneRegistry then portals controls into the item group ----
|
|
19
|
+
const ItemControlsOverlay = ({ nodeId }) => {
|
|
20
|
+
const node = useScene((state) => state.nodes[nodeId]);
|
|
21
|
+
const [itemObj, setItemObj] = useState(null);
|
|
22
|
+
useFrame(() => {
|
|
23
|
+
if (itemObj)
|
|
24
|
+
return;
|
|
25
|
+
const obj = sceneRegistry.nodes.get(nodeId);
|
|
26
|
+
if (obj)
|
|
27
|
+
setItemObj(obj);
|
|
28
|
+
});
|
|
29
|
+
const controlValues = useInteractive(useShallow((state) => state.items[nodeId]?.controlValues));
|
|
30
|
+
const setControlValue = useInteractive((state) => state.setControlValue);
|
|
31
|
+
const zoneId = useViewer((s) => s.selection.zoneId);
|
|
32
|
+
const zonePolygon = useScene((s) => {
|
|
33
|
+
if (!zoneId)
|
|
34
|
+
return null;
|
|
35
|
+
const z = s.nodes[zoneId];
|
|
36
|
+
return z?.polygon ?? null;
|
|
37
|
+
});
|
|
38
|
+
if (!(itemObj && controlValues && node?.asset.interactive))
|
|
39
|
+
return null;
|
|
40
|
+
const { controls } = node.asset.interactive;
|
|
41
|
+
const [, height] = node.asset.dimensions;
|
|
42
|
+
let opacity = 0;
|
|
43
|
+
let pointerEvents = 'none';
|
|
44
|
+
if (zoneId && zonePolygon?.length) {
|
|
45
|
+
itemObj.getWorldPosition(_tempVec);
|
|
46
|
+
const inside = pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
|
|
47
|
+
opacity = inside ? 1 : 0.1;
|
|
48
|
+
pointerEvents = inside ? 'auto' : 'none';
|
|
49
|
+
}
|
|
50
|
+
return createPortal(_jsx(Html, { center: true, distanceFactor: 8, occlude: true, position: [0, height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { style: {
|
|
51
|
+
display: 'flex',
|
|
52
|
+
flexDirection: 'column',
|
|
53
|
+
gap: 6,
|
|
54
|
+
background: 'rgba(0,0,0,0.75)',
|
|
55
|
+
backdropFilter: 'blur(8px)',
|
|
56
|
+
borderRadius: 8,
|
|
57
|
+
padding: '8px 12px',
|
|
58
|
+
minWidth: 120,
|
|
59
|
+
pointerEvents,
|
|
60
|
+
userSelect: 'none',
|
|
61
|
+
opacity,
|
|
62
|
+
transition: 'opacity 0.3s ease',
|
|
63
|
+
}, children: controls.map((control, i) => (_jsx(ControlWidget, { control: control, onChange: (v) => setControlValue(nodeId, i, v), value: controlValues[i] ?? false }, i))) }) }), itemObj);
|
|
64
|
+
};
|
|
65
|
+
// ---- Control widgets ----
|
|
66
|
+
const ControlWidget = ({ control, value, onChange, }) => {
|
|
67
|
+
const labelStyle = {
|
|
68
|
+
color: 'white',
|
|
69
|
+
fontSize: 11,
|
|
70
|
+
fontFamily: 'monospace',
|
|
71
|
+
display: 'flex',
|
|
72
|
+
flexDirection: 'column',
|
|
73
|
+
gap: 2,
|
|
74
|
+
};
|
|
75
|
+
if (control.kind === 'toggle') {
|
|
76
|
+
return (_jsx("button", { onClick: () => onChange(!value), style: {
|
|
77
|
+
background: value ? '#4ade80' : '#374151',
|
|
78
|
+
color: 'white',
|
|
79
|
+
border: 'none',
|
|
80
|
+
borderRadius: 4,
|
|
81
|
+
padding: '4px 8px',
|
|
82
|
+
cursor: 'pointer',
|
|
83
|
+
fontSize: 12,
|
|
84
|
+
fontFamily: 'monospace',
|
|
85
|
+
transition: 'background 0.2s',
|
|
86
|
+
}, children: control.label ?? (value ? 'On' : 'Off') }));
|
|
87
|
+
}
|
|
88
|
+
if (control.kind === 'slider') {
|
|
89
|
+
return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, control.unit ? ` ${control.unit}` : ''] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: control.step, type: "range", value: value })] }));
|
|
90
|
+
}
|
|
91
|
+
if (control.kind === 'temperature') {
|
|
92
|
+
return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, "\u00B0", control.unit] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: 1, type: "range", value: value })] }));
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"item-light-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item-light/item-light-system.tsx"],"names":[],"mappings":"AAwFA,wBAAgB,eAAe,4CA+M9B"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { sceneRegistry, useInteractive, useScene } from '@pascal-app/core';
|
|
3
|
+
import { useFrame } from '@react-three/fiber';
|
|
4
|
+
import { useRef } from 'react';
|
|
5
|
+
import { MathUtils, Vector3 } from 'three';
|
|
6
|
+
import { useItemLightPool } from '../../store/use-item-light-pool';
|
|
7
|
+
import useViewer from '../../store/use-viewer';
|
|
8
|
+
const POOL_SIZE = 12;
|
|
9
|
+
// How often (in seconds) to re-evaluate which items have lights assigned (fallback timer)
|
|
10
|
+
const REASSIGN_INTERVAL = 0.2;
|
|
11
|
+
// Hysteresis: a currently-assigned slot keeps its key unless an unassigned
|
|
12
|
+
// candidate beats it by at least this much (prevents flickering at the boundary)
|
|
13
|
+
const HYSTERESIS = 0.15;
|
|
14
|
+
// Camera movement thresholds that trigger an early re-evaluation
|
|
15
|
+
const CAM_MOVE_DIST = 0.5; // units
|
|
16
|
+
const CAM_ROT_DOT = 0.995; // cos(~5.7°)
|
|
17
|
+
// Module-level temp vectors reused every frame (avoids GC pressure)
|
|
18
|
+
const _dir = new Vector3();
|
|
19
|
+
const _camPos = new Vector3();
|
|
20
|
+
const _camFwd = new Vector3();
|
|
21
|
+
const _itemPos = new Vector3();
|
|
22
|
+
function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveState) {
|
|
23
|
+
// Skip lights that are toggled off — they contribute no illumination
|
|
24
|
+
if (reg.toggleIndex >= 0) {
|
|
25
|
+
const values = interactiveState.items[reg.nodeId]?.controlValues;
|
|
26
|
+
const isOn = Boolean(values?.[reg.toggleIndex]);
|
|
27
|
+
if (!isOn)
|
|
28
|
+
return Number.POSITIVE_INFINITY;
|
|
29
|
+
}
|
|
30
|
+
const { nodeId, effect } = reg;
|
|
31
|
+
const obj = sceneRegistry.nodes.get(nodeId);
|
|
32
|
+
if (!obj)
|
|
33
|
+
return Number.POSITIVE_INFINITY;
|
|
34
|
+
obj.getWorldPosition(_itemPos);
|
|
35
|
+
_itemPos.x += effect.offset[0];
|
|
36
|
+
_itemPos.y += effect.offset[1];
|
|
37
|
+
_itemPos.z += effect.offset[2];
|
|
38
|
+
_dir.copy(_itemPos).sub(_camPos).normalize();
|
|
39
|
+
const dot = _camFwd.dot(_dir); // 1 = ahead, -1 = behind
|
|
40
|
+
// Angular component (0 = dead ahead, 2 = directly behind)
|
|
41
|
+
const angular = 1 - dot;
|
|
42
|
+
// Normalised distance component (assumes scenes < 200 units)
|
|
43
|
+
const dist = _camPos.distanceTo(_itemPos) / 200;
|
|
44
|
+
// ── Level factor ──────────────────────────────────────────────────────────
|
|
45
|
+
const node = nodes[nodeId];
|
|
46
|
+
const itemLevelId = node?.parentId ?? null;
|
|
47
|
+
let levelPenalty = 0;
|
|
48
|
+
if (selectedLevelId) {
|
|
49
|
+
if (itemLevelId !== selectedLevelId) {
|
|
50
|
+
// In solo mode items on other levels are invisible — deprioritize strongly
|
|
51
|
+
levelPenalty = levelMode === 'solo' ? 100 : 0.8;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (itemLevelId) {
|
|
55
|
+
// No level selected — lightly prefer items on level index 0
|
|
56
|
+
const levelNode = nodes[itemLevelId];
|
|
57
|
+
const levelIndex = levelNode?.level ?? 0;
|
|
58
|
+
if (levelIndex !== 0)
|
|
59
|
+
levelPenalty = 0.3;
|
|
60
|
+
}
|
|
61
|
+
return angular * 0.7 + dist * 0.3 + levelPenalty;
|
|
62
|
+
}
|
|
63
|
+
export function ItemLightSystem() {
|
|
64
|
+
const lightRefs = useRef(Array.from({ length: POOL_SIZE }, () => null));
|
|
65
|
+
const slots = useRef(Array.from({ length: POOL_SIZE }, () => ({ key: null, pendingKey: null, isFadingOut: false })));
|
|
66
|
+
const reassignTimer = useRef(0);
|
|
67
|
+
// Track camera state at last reassignment to detect meaningful movement
|
|
68
|
+
const prevReassignCamPos = useRef(new Vector3());
|
|
69
|
+
const prevReassignCamFwd = useRef(new Vector3(0, 0, -1));
|
|
70
|
+
useFrame(({ camera }, delta) => {
|
|
71
|
+
const dt = Math.min(delta, 0.1);
|
|
72
|
+
const { registrations } = useItemLightPool.getState();
|
|
73
|
+
const interactiveState = useInteractive.getState();
|
|
74
|
+
// ── 1. Throttled priority reassignment ──────────────────────────────────
|
|
75
|
+
camera.getWorldPosition(_camPos);
|
|
76
|
+
camera.getWorldDirection(_camFwd);
|
|
77
|
+
const camMoved = _camPos.distanceTo(prevReassignCamPos.current) > CAM_MOVE_DIST ||
|
|
78
|
+
_camFwd.dot(prevReassignCamFwd.current) < CAM_ROT_DOT;
|
|
79
|
+
reassignTimer.current -= delta;
|
|
80
|
+
const shouldReassign = reassignTimer.current <= 0 || camMoved;
|
|
81
|
+
if (shouldReassign) {
|
|
82
|
+
reassignTimer.current = REASSIGN_INTERVAL;
|
|
83
|
+
prevReassignCamPos.current.copy(_camPos);
|
|
84
|
+
prevReassignCamFwd.current.copy(_camFwd);
|
|
85
|
+
// Read level/scene state once for the whole tick
|
|
86
|
+
const nodes = useScene.getState().nodes;
|
|
87
|
+
const viewerState = useViewer.getState();
|
|
88
|
+
const selectedLevelId = viewerState.selection.levelId;
|
|
89
|
+
const levelMode = viewerState.levelMode;
|
|
90
|
+
// Score every registration
|
|
91
|
+
const scored = [];
|
|
92
|
+
for (const [key, reg] of registrations) {
|
|
93
|
+
scored.push({
|
|
94
|
+
key,
|
|
95
|
+
score: scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveState),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
scored.sort((a, b) => a.score - b.score);
|
|
99
|
+
// Build the desired assignment (top POOL_SIZE keys)
|
|
100
|
+
const desired = scored.slice(0, POOL_SIZE).map((s) => s.key);
|
|
101
|
+
// Build a map of currently-assigned keys → slot index for hysteresis
|
|
102
|
+
const currentlyAssigned = new Map();
|
|
103
|
+
for (let i = 0; i < POOL_SIZE; i++) {
|
|
104
|
+
const s = slots.current[i];
|
|
105
|
+
if (!s)
|
|
106
|
+
continue;
|
|
107
|
+
const k = s.key ?? s.pendingKey;
|
|
108
|
+
if (k)
|
|
109
|
+
currentlyAssigned.set(k, i);
|
|
110
|
+
}
|
|
111
|
+
// Assign desired keys to slots — prefer keeping existing assignments
|
|
112
|
+
const usedSlots = new Set();
|
|
113
|
+
const assignedKeys = new Set();
|
|
114
|
+
// Pass 1: keep existing slots where the key is still in desired
|
|
115
|
+
for (const key of desired) {
|
|
116
|
+
const existingSlot = currentlyAssigned.get(key);
|
|
117
|
+
if (existingSlot !== undefined && !usedSlots.has(existingSlot)) {
|
|
118
|
+
usedSlots.add(existingSlot);
|
|
119
|
+
assignedKeys.add(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Pass 2: assign remaining desired keys to free slots
|
|
123
|
+
let freeSlot = 0;
|
|
124
|
+
for (const key of desired) {
|
|
125
|
+
if (assignedKeys.has(key))
|
|
126
|
+
continue;
|
|
127
|
+
while (freeSlot < POOL_SIZE && usedSlots.has(freeSlot))
|
|
128
|
+
freeSlot++;
|
|
129
|
+
if (freeSlot >= POOL_SIZE)
|
|
130
|
+
break;
|
|
131
|
+
// Hysteresis: only evict the current occupant if the new key scores
|
|
132
|
+
// meaningfully better than it
|
|
133
|
+
const freeSlotData = slots.current[freeSlot];
|
|
134
|
+
const currentKey = freeSlotData ? (freeSlotData.key ?? freeSlotData.pendingKey) : null;
|
|
135
|
+
if (currentKey && !desired.includes(currentKey)) {
|
|
136
|
+
const currentScore = scored.find((s) => s.key === currentKey)?.score ?? Number.POSITIVE_INFINITY;
|
|
137
|
+
const newScore = scored.find((s) => s.key === key)?.score ?? 0;
|
|
138
|
+
if (currentScore - newScore < HYSTERESIS) {
|
|
139
|
+
freeSlot++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
usedSlots.add(freeSlot);
|
|
144
|
+
assignedKeys.add(key);
|
|
145
|
+
const slot = slots.current[freeSlot];
|
|
146
|
+
if (slot && slot.key !== key) {
|
|
147
|
+
slot.pendingKey = key;
|
|
148
|
+
slot.isFadingOut = slot.key !== null;
|
|
149
|
+
if (!slot.isFadingOut) {
|
|
150
|
+
// Slot was idle — skip fade-out, assign immediately
|
|
151
|
+
slot.key = key;
|
|
152
|
+
slot.pendingKey = null;
|
|
153
|
+
const light = lightRefs.current[freeSlot];
|
|
154
|
+
const reg = registrations.get(key);
|
|
155
|
+
if (light && reg) {
|
|
156
|
+
light.color.set(reg.effect.color);
|
|
157
|
+
light.distance = reg.effect.distance ?? 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
freeSlot++;
|
|
162
|
+
}
|
|
163
|
+
// Clear slots whose key is no longer in desired and not pending
|
|
164
|
+
for (let i = 0; i < POOL_SIZE; i++) {
|
|
165
|
+
if (!usedSlots.has(i)) {
|
|
166
|
+
const slot = slots.current[i];
|
|
167
|
+
if (slot?.key && !desired.includes(slot.key)) {
|
|
168
|
+
slot.pendingKey = null;
|
|
169
|
+
slot.isFadingOut = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ── 2. Per-frame light updates ───────────────────────────────────────────
|
|
175
|
+
for (let i = 0; i < POOL_SIZE; i++) {
|
|
176
|
+
const light = lightRefs.current[i];
|
|
177
|
+
if (!light)
|
|
178
|
+
continue;
|
|
179
|
+
const slot = slots.current[i];
|
|
180
|
+
if (!slot)
|
|
181
|
+
continue;
|
|
182
|
+
// Fade-out phase: lerp intensity → 0, then complete the transition
|
|
183
|
+
if (slot.isFadingOut) {
|
|
184
|
+
light.intensity = MathUtils.lerp(light.intensity, 0, dt * 12);
|
|
185
|
+
if (light.intensity < 0.01) {
|
|
186
|
+
light.intensity = 0;
|
|
187
|
+
slot.isFadingOut = false;
|
|
188
|
+
slot.key = slot.pendingKey;
|
|
189
|
+
slot.pendingKey = null;
|
|
190
|
+
if (slot.key) {
|
|
191
|
+
const reg = registrations.get(slot.key);
|
|
192
|
+
if (reg) {
|
|
193
|
+
light.color.set(reg.effect.color);
|
|
194
|
+
light.distance = reg.effect.distance ?? 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!slot.key) {
|
|
201
|
+
// Idle slot — keep dark
|
|
202
|
+
light.intensity = 0;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const reg = registrations.get(slot.key);
|
|
206
|
+
if (!reg) {
|
|
207
|
+
slot.key = null;
|
|
208
|
+
light.intensity = 0;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Snap world position each frame
|
|
212
|
+
const obj = sceneRegistry.nodes.get(reg.nodeId);
|
|
213
|
+
if (obj) {
|
|
214
|
+
obj.getWorldPosition(_itemPos);
|
|
215
|
+
const [ox, oy, oz] = reg.effect.offset;
|
|
216
|
+
light.position.set(_itemPos.x + ox, _itemPos.y + oy, _itemPos.z + oz);
|
|
217
|
+
}
|
|
218
|
+
// Compute target intensity
|
|
219
|
+
const values = interactiveState.items[reg.nodeId]?.controlValues;
|
|
220
|
+
const isOn = reg.toggleIndex >= 0 ? Boolean(values?.[reg.toggleIndex]) : true;
|
|
221
|
+
let t = 1;
|
|
222
|
+
if (reg.hasSlider) {
|
|
223
|
+
const raw = values?.[reg.sliderIndex] ?? reg.sliderMin;
|
|
224
|
+
t = (raw - reg.sliderMin) / (reg.sliderMax - reg.sliderMin);
|
|
225
|
+
}
|
|
226
|
+
const targetIntensity = isOn
|
|
227
|
+
? MathUtils.lerp(reg.effect.intensityRange[0], reg.effect.intensityRange[1], t)
|
|
228
|
+
: reg.effect.intensityRange[0];
|
|
229
|
+
light.intensity = MathUtils.lerp(light.intensity, targetIntensity, dt * 12);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, ref: (el) => {
|
|
233
|
+
lightRefs.current[i] = el;
|
|
234
|
+
} }, i))) }));
|
|
235
|
+
}
|