@ifc-lite/viewer 1.10.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/assets/{Arrow.dom-Bw5JMdDs.js → Arrow.dom-p9ppgFLr.js} +1 -1
- package/dist/assets/{browser-DdRf3aWl.js → browser-lKzgHsnJ.js} +1 -1
- package/dist/assets/{ifc-lite_bg-C1-gLAHo.wasm → ifc-lite_bg-B6s-pcv0.wasm} +0 -0
- package/dist/assets/index-BoYyWYAu.css +1 -0
- package/dist/assets/{index-1ff6P0kc.js → index-CF854G-8.js} +42703 -41097
- package/dist/assets/{index-Bz7vHRxl.js → index-DQlpY6aJ.js} +4 -4
- package/dist/assets/{native-bridge-C5hD5vae.js → native-bridge-BgRWyawy.js} +1 -1
- package/dist/assets/{wasm-bridge-CaNKXFGM.js → wasm-bridge-BZxGtE7z.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +20 -19
- package/src/components/viewer/BasketPresentationDock.tsx +422 -0
- package/src/components/viewer/CommandPalette.tsx +29 -32
- package/src/components/viewer/EntityContextMenu.tsx +37 -22
- package/src/components/viewer/HierarchyPanel.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +56 -113
- package/src/components/viewer/Section2DPanel.tsx +8 -1
- package/src/components/viewer/ThemeSwitch.tsx +55 -0
- package/src/components/viewer/Viewport.tsx +66 -105
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/ViewportOverlays.tsx +9 -3
- package/src/components/viewer/useGeometryStreaming.ts +25 -0
- package/src/components/viewer/useKeyboardControls.ts +2 -2
- package/src/components/viewer/useRenderUpdates.ts +10 -3
- package/src/hooks/meshColorUpdates.test.ts +56 -0
- package/src/hooks/meshColorUpdates.ts +20 -0
- package/src/hooks/useIDS.ts +7 -8
- package/src/hooks/useIfcLoader.ts +25 -1
- package/src/hooks/useKeyboardShortcuts.ts +51 -84
- package/src/hooks/useViewerSelectors.ts +4 -0
- package/src/store/basket/basketCommands.ts +81 -0
- package/src/store/basket/basketViewActivator.ts +54 -0
- package/src/store/basketSave.ts +122 -0
- package/src/store/basketVisibleSet.test.ts +161 -0
- package/src/store/basketVisibleSet.ts +487 -0
- package/src/store/homeView.ts +21 -0
- package/src/store/index.ts +8 -0
- package/src/store/slices/dataSlice.test.ts +53 -4
- package/src/store/slices/dataSlice.ts +13 -5
- package/src/store/slices/drawing2DSlice.ts +5 -0
- package/src/store/slices/pinboardSlice.test.ts +160 -0
- package/src/store/slices/pinboardSlice.ts +248 -18
- package/src/store/types.ts +11 -0
- package/dist/assets/index-mvbV6NHd.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/browser-
|
|
2
|
-
import { _ as u, b as S, __tla as __tla_0 } from "./index-
|
|
3
|
-
import { N as j, m as B, __tla as __tla_1 } from "./index-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/browser-lKzgHsnJ.js","assets/index-CF854G-8.js","assets/index-BoYyWYAu.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import { _ as u, b as S, __tla as __tla_0 } from "./index-CF854G-8.js";
|
|
3
|
+
import { N as j, m as B, __tla as __tla_1 } from "./index-CF854G-8.js";
|
|
4
4
|
let c, g, L, D, x, R, A;
|
|
5
5
|
let __tla = Promise.all([
|
|
6
6
|
(()=>{
|
|
@@ -87,7 +87,7 @@ let __tla = Promise.all([
|
|
|
87
87
|
function k() {
|
|
88
88
|
return m || (m = (async ()=>{
|
|
89
89
|
try {
|
|
90
|
-
const e = await u(()=>import("./browser-
|
|
90
|
+
const e = await u(()=>import("./browser-lKzgHsnJ.js").then((i)=>i.b), __vite__mapDeps([0,1,2])), t = e.default ?? e;
|
|
91
91
|
let s;
|
|
92
92
|
try {
|
|
93
93
|
s = (await u(()=>import("./esbuild-COv63sf-.js"), [])).default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as f,a as m}from"./index-
|
|
1
|
+
import{I as f,a as m}from"./index-CF854G-8.js";class u{bridge;initialized=!1;constructor(){this.bridge=new f}async init(){this.initialized||(await this.bridge.init(),this.initialized=!0)}isInitialized(){return this.initialized}async processGeometry(s){this.initialized||await this.init(),performance.now();const i=new m(this.bridge.getApi(),s),n=i.collectMeshes(),r=i.getBuildingRotation();performance.now();let e=0,o=0;for(const c of n)e+=c.positions.length/3,o+=c.indices.length/3;return{meshes:n,totalVertices:e,totalTriangles:o,coordinateInfo:{originShift:{x:0,y:0,z:0},originalBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},shiftedBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},hasLargeCoordinates:!1,buildingRotation:r}}}async processGeometryStreaming(s,i){this.initialized||await this.init();const n=performance.now(),r=new m(this.bridge.getApi(),s);let e=0,o=0,a=0;try{for await(const t of r.collectMeshesStreaming(50)){if(t&&typeof t=="object"&&"type"in t&&t.type==="colorUpdate")continue;const l=t;e+=l.length;for(const d of l)o+=d.positions.length/3,a+=d.indices.length/3;i.onBatch?.({meshes:l,progress:{processed:e,total:e,currentType:"processing"}})}}catch(t){throw i.onError?.(t instanceof Error?t:new Error(String(t))),t}const h=performance.now()-n,g={totalMeshes:e,totalVertices:o,totalTriangles:a,parseTimeMs:h*.3,geometryTimeMs:h*.7};return i.onComplete?.(g),g}getApi(){return this.bridge.getApi()}}export{u as WasmBridge};
|
package/dist/index.html
CHANGED
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
<meta name="theme-color" content="#7aa2f7">
|
|
45
45
|
<meta name="msapplication-TileColor" content="#1a1b26">
|
|
46
46
|
<meta name="msapplication-TileImage" content="/favicon-192x192-cropped.png">
|
|
47
|
-
<script type="module" crossorigin src="/assets/index-
|
|
48
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
47
|
+
<script type="module" crossorigin src="/assets/index-CF854G-8.js"></script>
|
|
48
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BoYyWYAu.css">
|
|
49
49
|
</head>
|
|
50
50
|
<body>
|
|
51
51
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"@tanstack/react-virtual": "^3.13.18",
|
|
30
30
|
"apache-arrow": "^14.0.2",
|
|
31
31
|
"autoprefixer": "^10.4.23",
|
|
32
|
+
"beautiful-theme-toggle": "^1.0.1",
|
|
32
33
|
"class-variance-authority": "^0.7.1",
|
|
33
34
|
"clsx": "^2.1.1",
|
|
34
35
|
"lucide-react": "^0.562.0",
|
|
@@ -40,24 +41,24 @@
|
|
|
40
41
|
"tailwind-merge": "^3.4.0",
|
|
41
42
|
"tailwindcss": "^4.1.18",
|
|
42
43
|
"zustand": "^4.4.0",
|
|
43
|
-
"@ifc-lite/bcf": "^1.
|
|
44
|
-
"@ifc-lite/cache": "^1.
|
|
45
|
-
"@ifc-lite/data": "^1.
|
|
46
|
-
"@ifc-lite/drawing-2d": "^1.
|
|
47
|
-
"@ifc-lite/encoding": "^1.
|
|
48
|
-
"@ifc-lite/export": "^1.
|
|
49
|
-
"@ifc-lite/geometry": "^1.
|
|
50
|
-
"@ifc-lite/ids": "^1.
|
|
51
|
-
"@ifc-lite/lens": "^1.
|
|
52
|
-
"@ifc-lite/lists": "^1.
|
|
53
|
-
"@ifc-lite/mutations": "^1.
|
|
54
|
-
"@ifc-lite/parser": "^1.
|
|
55
|
-
"@ifc-lite/query": "^1.
|
|
56
|
-
"@ifc-lite/renderer": "^1.
|
|
57
|
-
"@ifc-lite/sandbox": "^1.
|
|
58
|
-
"@ifc-lite/server-client": "^1.
|
|
59
|
-
"@ifc-lite/spatial": "^1.
|
|
60
|
-
"@ifc-lite/wasm": "^1.
|
|
44
|
+
"@ifc-lite/bcf": "^1.11.1",
|
|
45
|
+
"@ifc-lite/cache": "^1.11.1",
|
|
46
|
+
"@ifc-lite/data": "^1.11.1",
|
|
47
|
+
"@ifc-lite/drawing-2d": "^1.11.1",
|
|
48
|
+
"@ifc-lite/encoding": "^1.11.1",
|
|
49
|
+
"@ifc-lite/export": "^1.11.1",
|
|
50
|
+
"@ifc-lite/geometry": "^1.11.1",
|
|
51
|
+
"@ifc-lite/ids": "^1.11.1",
|
|
52
|
+
"@ifc-lite/lens": "^1.11.1",
|
|
53
|
+
"@ifc-lite/lists": "^1.11.1",
|
|
54
|
+
"@ifc-lite/mutations": "^1.11.1",
|
|
55
|
+
"@ifc-lite/parser": "^1.11.1",
|
|
56
|
+
"@ifc-lite/query": "^1.11.1",
|
|
57
|
+
"@ifc-lite/renderer": "^1.11.1",
|
|
58
|
+
"@ifc-lite/sandbox": "^1.11.1",
|
|
59
|
+
"@ifc-lite/server-client": "^1.11.1",
|
|
60
|
+
"@ifc-lite/spatial": "^1.11.1",
|
|
61
|
+
"@ifc-lite/wasm": "^1.11.1"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@tailwindcss/postcss": "^4.1.18",
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
ChevronLeft,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
Equal,
|
|
10
|
+
Eye,
|
|
11
|
+
EyeOff,
|
|
12
|
+
Minus,
|
|
13
|
+
Pencil,
|
|
14
|
+
Play,
|
|
15
|
+
Plus,
|
|
16
|
+
RotateCcw,
|
|
17
|
+
Save,
|
|
18
|
+
Square,
|
|
19
|
+
Timer,
|
|
20
|
+
Trash2,
|
|
21
|
+
} from 'lucide-react';
|
|
22
|
+
import { Button } from '@/components/ui/button';
|
|
23
|
+
import { Input } from '@/components/ui/input';
|
|
24
|
+
import { cn } from '@/lib/utils';
|
|
25
|
+
import { useViewerStore } from '@/store';
|
|
26
|
+
import {
|
|
27
|
+
executeBasketSet,
|
|
28
|
+
executeBasketAdd,
|
|
29
|
+
executeBasketRemove,
|
|
30
|
+
executeBasketSaveView,
|
|
31
|
+
executeBasketClear,
|
|
32
|
+
} from '@/store/basket/basketCommands';
|
|
33
|
+
import { activateBasketViewFromStore } from '@/store/basket/basketViewActivator';
|
|
34
|
+
import { getSmartBasketInputFromStore, isBasketIsolationActiveFromStore } from '@/store/basketVisibleSet';
|
|
35
|
+
|
|
36
|
+
export function BasketPresentationDock() {
|
|
37
|
+
const [savingThumbnail, setSavingThumbnail] = useState(false);
|
|
38
|
+
const [editingViewId, setEditingViewId] = useState<string | null>(null);
|
|
39
|
+
const [editingName, setEditingName] = useState('');
|
|
40
|
+
const [playingAll, setPlayingAll] = useState(false);
|
|
41
|
+
const stripRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const stopPlayRef = useRef(false);
|
|
43
|
+
const loopPlayRef = useRef(false);
|
|
44
|
+
|
|
45
|
+
const pinboardEntities = useViewerStore((s) => s.pinboardEntities);
|
|
46
|
+
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
47
|
+
const basketViews = useViewerStore((s) => s.basketViews);
|
|
48
|
+
const activeBasketViewId = useViewerStore((s) => s.activeBasketViewId);
|
|
49
|
+
const basketPresentationVisible = useViewerStore((s) => s.basketPresentationVisible);
|
|
50
|
+
|
|
51
|
+
const showPinboard = useViewerStore((s) => s.showPinboard);
|
|
52
|
+
const clearIsolation = useViewerStore((s) => s.clearIsolation);
|
|
53
|
+
const setBasketPresentationVisible = useViewerStore((s) => s.setBasketPresentationVisible);
|
|
54
|
+
|
|
55
|
+
const removeBasketView = useViewerStore((s) => s.removeBasketView);
|
|
56
|
+
const renameBasketView = useViewerStore((s) => s.renameBasketView);
|
|
57
|
+
const setBasketViewTransitionMs = useViewerStore((s) => s.setBasketViewTransitionMs);
|
|
58
|
+
|
|
59
|
+
const basketIsVisible = useMemo(
|
|
60
|
+
() => pinboardEntities.size > 0 && isolatedEntities !== null && isBasketIsolationActiveFromStore(),
|
|
61
|
+
[pinboardEntities, isolatedEntities],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const applySource = useCallback((mode: 'set' | 'add' | 'remove') => {
|
|
65
|
+
if (mode === 'set') executeBasketSet();
|
|
66
|
+
else if (mode === 'add') executeBasketAdd();
|
|
67
|
+
else executeBasketRemove();
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleSaveCurrent = useCallback(async () => {
|
|
71
|
+
if (pinboardEntities.size === 0 || savingThumbnail) return;
|
|
72
|
+
|
|
73
|
+
setSavingThumbnail(true);
|
|
74
|
+
try {
|
|
75
|
+
const { source } = getSmartBasketInputFromStore();
|
|
76
|
+
await executeBasketSaveView(source === 'empty' ? 'manual' : source);
|
|
77
|
+
} finally {
|
|
78
|
+
setSavingThumbnail(false);
|
|
79
|
+
}
|
|
80
|
+
}, [pinboardEntities, savingThumbnail]);
|
|
81
|
+
|
|
82
|
+
const startRename = useCallback((viewId: string, name: string) => {
|
|
83
|
+
setEditingViewId(viewId);
|
|
84
|
+
setEditingName(name);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const cancelRename = useCallback(() => {
|
|
88
|
+
setEditingViewId(null);
|
|
89
|
+
setEditingName('');
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const commitRename = useCallback(() => {
|
|
93
|
+
if (!editingViewId) return;
|
|
94
|
+
const nextName = editingName.trim();
|
|
95
|
+
if (nextName.length > 0) {
|
|
96
|
+
renameBasketView(editingViewId, nextName);
|
|
97
|
+
}
|
|
98
|
+
setEditingViewId(null);
|
|
99
|
+
setEditingName('');
|
|
100
|
+
}, [editingViewId, editingName, renameBasketView]);
|
|
101
|
+
|
|
102
|
+
const scrollStrip = useCallback((delta: number) => {
|
|
103
|
+
stripRef.current?.scrollBy({ left: delta, behavior: 'smooth' });
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const toTransitionMs = useCallback((value: number | null | undefined) => {
|
|
107
|
+
if (!value || !Number.isFinite(value) || value <= 0) return 700;
|
|
108
|
+
return Math.max(150, Math.min(15000, Math.round(value)));
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const wait = useCallback((ms: number) => new Promise<void>((resolve) => {
|
|
112
|
+
window.setTimeout(resolve, ms);
|
|
113
|
+
}), []);
|
|
114
|
+
|
|
115
|
+
const stopPlayAll = useCallback(() => {
|
|
116
|
+
stopPlayRef.current = true;
|
|
117
|
+
loopPlayRef.current = false;
|
|
118
|
+
setPlayingAll(false);
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const startPlayAll = useCallback(async (loop = false) => {
|
|
122
|
+
if (playingAll || basketViews.length === 0) return;
|
|
123
|
+
stopPlayRef.current = false;
|
|
124
|
+
loopPlayRef.current = loop;
|
|
125
|
+
setPlayingAll(true);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const orderedViews = [...basketViews];
|
|
129
|
+
do {
|
|
130
|
+
for (const view of orderedViews) {
|
|
131
|
+
if (stopPlayRef.current) break;
|
|
132
|
+
activateBasketViewFromStore(view.id);
|
|
133
|
+
const transitionMs = toTransitionMs(view.transitionMs);
|
|
134
|
+
await wait(transitionMs + 180);
|
|
135
|
+
}
|
|
136
|
+
} while (loopPlayRef.current && !stopPlayRef.current && orderedViews.length > 0);
|
|
137
|
+
} finally {
|
|
138
|
+
loopPlayRef.current = false;
|
|
139
|
+
setPlayingAll(false);
|
|
140
|
+
}
|
|
141
|
+
}, [basketViews, playingAll, toTransitionMs, wait]);
|
|
142
|
+
|
|
143
|
+
const setViewTransitionDuration = useCallback((viewId: string, currentTransitionMs: number | null) => {
|
|
144
|
+
const defaultSeconds = currentTransitionMs && currentTransitionMs > 0
|
|
145
|
+
? (currentTransitionMs / 1000).toFixed(1)
|
|
146
|
+
: '';
|
|
147
|
+
const input = window.prompt(
|
|
148
|
+
'Transition duration in seconds (optional). Leave empty for default smooth transition.',
|
|
149
|
+
defaultSeconds,
|
|
150
|
+
);
|
|
151
|
+
if (input === null) return;
|
|
152
|
+
|
|
153
|
+
const trimmed = input.trim();
|
|
154
|
+
if (!trimmed) {
|
|
155
|
+
setBasketViewTransitionMs(viewId, null);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const seconds = Number(trimmed);
|
|
160
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return;
|
|
161
|
+
setBasketViewTransitionMs(viewId, Math.round(seconds * 1000));
|
|
162
|
+
}, [setBasketViewTransitionMs]);
|
|
163
|
+
|
|
164
|
+
if (!basketPresentationVisible) {
|
|
165
|
+
return (
|
|
166
|
+
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-30 pointer-events-none">
|
|
167
|
+
<Button
|
|
168
|
+
type="button"
|
|
169
|
+
variant="secondary"
|
|
170
|
+
size="sm"
|
|
171
|
+
className="pointer-events-auto shadow-lg gap-2"
|
|
172
|
+
onClick={() => setBasketPresentationVisible(true)}
|
|
173
|
+
>
|
|
174
|
+
Presentation
|
|
175
|
+
<span className="rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] font-semibold text-primary">
|
|
176
|
+
{basketViews.length}
|
|
177
|
+
</span>
|
|
178
|
+
</Button>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-30 w-[min(980px,calc(100%-2rem))] pointer-events-none">
|
|
185
|
+
<div className="pointer-events-auto rounded-xl border bg-background/90 backdrop-blur-sm shadow-lg p-3 space-y-3">
|
|
186
|
+
<div className="flex items-center justify-between gap-3">
|
|
187
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
188
|
+
<div className="text-sm font-semibold">Presentation</div>
|
|
189
|
+
<span className="rounded-full border px-2 py-0.5 text-[11px] text-muted-foreground">
|
|
190
|
+
{pinboardEntities.size} in basket
|
|
191
|
+
</span>
|
|
192
|
+
<span className="rounded-full border px-2 py-0.5 text-[11px] text-muted-foreground">
|
|
193
|
+
{basketViews.length} views
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="flex items-center gap-1.5">
|
|
198
|
+
<div className="flex items-center gap-1 rounded-md border bg-background/70 p-1">
|
|
199
|
+
<Button type="button" variant="outline" size="icon-sm" onClick={() => applySource('set')} title="Set basket from current context">
|
|
200
|
+
<Equal className="h-4 w-4" />
|
|
201
|
+
</Button>
|
|
202
|
+
<Button type="button" variant="outline" size="icon-sm" onClick={() => applySource('add')} title="Add current context to basket">
|
|
203
|
+
<Plus className="h-4 w-4" />
|
|
204
|
+
</Button>
|
|
205
|
+
<Button
|
|
206
|
+
type="button"
|
|
207
|
+
variant="outline"
|
|
208
|
+
size="icon-sm"
|
|
209
|
+
onClick={() => applySource('remove')}
|
|
210
|
+
disabled={pinboardEntities.size === 0}
|
|
211
|
+
title="Remove current context from basket"
|
|
212
|
+
>
|
|
213
|
+
<Minus className="h-4 w-4" />
|
|
214
|
+
</Button>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="flex items-center gap-1 rounded-md border bg-background/70 p-1">
|
|
217
|
+
<Button
|
|
218
|
+
type="button"
|
|
219
|
+
variant="outline"
|
|
220
|
+
size="icon-sm"
|
|
221
|
+
onClick={() => {
|
|
222
|
+
if (basketIsVisible) clearIsolation();
|
|
223
|
+
else showPinboard();
|
|
224
|
+
}}
|
|
225
|
+
disabled={pinboardEntities.size === 0}
|
|
226
|
+
title={basketIsVisible ? 'Hide active basket' : 'Show active basket'}
|
|
227
|
+
>
|
|
228
|
+
{basketIsVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
229
|
+
</Button>
|
|
230
|
+
<Button
|
|
231
|
+
type="button"
|
|
232
|
+
variant="outline"
|
|
233
|
+
size="icon-sm"
|
|
234
|
+
onClick={executeBasketClear}
|
|
235
|
+
disabled={pinboardEntities.size === 0}
|
|
236
|
+
title="Clear active basket"
|
|
237
|
+
>
|
|
238
|
+
<RotateCcw className="h-4 w-4" />
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
<Button
|
|
242
|
+
type="button"
|
|
243
|
+
variant="default"
|
|
244
|
+
size="icon-sm"
|
|
245
|
+
onClick={handleSaveCurrent}
|
|
246
|
+
disabled={pinboardEntities.size === 0 || savingThumbnail}
|
|
247
|
+
title="Save current basket as presentation view"
|
|
248
|
+
>
|
|
249
|
+
<Save className="h-4 w-4" />
|
|
250
|
+
</Button>
|
|
251
|
+
<Button
|
|
252
|
+
type="button"
|
|
253
|
+
variant={playingAll ? 'secondary' : 'outline'}
|
|
254
|
+
size="icon-sm"
|
|
255
|
+
onClick={playingAll ? stopPlayAll : (e) => { void startPlayAll(e.shiftKey); }}
|
|
256
|
+
disabled={basketViews.length === 0}
|
|
257
|
+
title={playingAll ? 'Stop playback' : 'Play all saved views (Shift+Click to loop)'}
|
|
258
|
+
>
|
|
259
|
+
{playingAll ? <Square className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
|
260
|
+
</Button>
|
|
261
|
+
<Button
|
|
262
|
+
type="button"
|
|
263
|
+
variant="ghost"
|
|
264
|
+
size="sm"
|
|
265
|
+
className="ml-1 text-xs"
|
|
266
|
+
onClick={() => setBasketPresentationVisible(false)}
|
|
267
|
+
>
|
|
268
|
+
Hide
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div className="flex items-center gap-2">
|
|
274
|
+
<Button
|
|
275
|
+
type="button"
|
|
276
|
+
variant="outline"
|
|
277
|
+
size="icon-sm"
|
|
278
|
+
onClick={() => scrollStrip(-280)}
|
|
279
|
+
disabled={basketViews.length <= 1}
|
|
280
|
+
title="Scroll left"
|
|
281
|
+
>
|
|
282
|
+
<ChevronLeft className="h-4 w-4" />
|
|
283
|
+
</Button>
|
|
284
|
+
|
|
285
|
+
<div
|
|
286
|
+
ref={stripRef}
|
|
287
|
+
className="flex-1 min-w-0 overflow-x-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent snap-x snap-mandatory"
|
|
288
|
+
>
|
|
289
|
+
<div className="flex items-stretch gap-2 pr-1">
|
|
290
|
+
{basketViews.length === 0 && (
|
|
291
|
+
<div className="h-[102px] min-w-[340px] rounded-md border border-dashed text-xs text-muted-foreground px-3 py-2 flex items-center">
|
|
292
|
+
Save basket views here. Click any card to restore both visibility and viewpoint.
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{basketViews.map((view) => (
|
|
297
|
+
<div key={view.id} className="relative w-[186px] h-[102px] shrink-0 snap-start">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
onClick={() => {
|
|
301
|
+
if (editingViewId) return;
|
|
302
|
+
activateBasketViewFromStore(view.id);
|
|
303
|
+
}}
|
|
304
|
+
className={cn(
|
|
305
|
+
'h-full w-full rounded-md border bg-card text-left overflow-hidden transition-colors',
|
|
306
|
+
activeBasketViewId === view.id && 'ring-2 ring-primary border-primary',
|
|
307
|
+
)}
|
|
308
|
+
>
|
|
309
|
+
{view.thumbnailDataUrl ? (
|
|
310
|
+
<img
|
|
311
|
+
src={view.thumbnailDataUrl}
|
|
312
|
+
alt={view.name}
|
|
313
|
+
className="absolute inset-0 h-full w-full object-cover"
|
|
314
|
+
/>
|
|
315
|
+
) : (
|
|
316
|
+
<div className="absolute inset-0 bg-muted" />
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{activeBasketViewId === view.id && (
|
|
320
|
+
<div className="absolute left-1 top-1 rounded bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-primary-foreground">
|
|
321
|
+
Active
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
</button>
|
|
326
|
+
|
|
327
|
+
<div
|
|
328
|
+
className={cn(
|
|
329
|
+
'absolute inset-x-0 bottom-0 bg-black/60 text-white px-2 py-1',
|
|
330
|
+
editingViewId !== view.id && 'pointer-events-none',
|
|
331
|
+
)}
|
|
332
|
+
onClick={(e) => e.stopPropagation()}
|
|
333
|
+
>
|
|
334
|
+
{editingViewId === view.id ? (
|
|
335
|
+
<Input
|
|
336
|
+
autoFocus
|
|
337
|
+
value={editingName}
|
|
338
|
+
onChange={(e) => setEditingName(e.target.value)}
|
|
339
|
+
onBlur={commitRename}
|
|
340
|
+
onKeyDown={(e) => {
|
|
341
|
+
if (e.key === 'Enter') {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
commitRename();
|
|
344
|
+
} else if (e.key === 'Escape') {
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
cancelRename();
|
|
347
|
+
}
|
|
348
|
+
}}
|
|
349
|
+
className="h-6 bg-black/40 text-xs border-white/30 text-white placeholder:text-white/60"
|
|
350
|
+
/>
|
|
351
|
+
) : (
|
|
352
|
+
<>
|
|
353
|
+
<div className="text-[12px] font-medium truncate">{view.name}</div>
|
|
354
|
+
<div className="text-[10px] opacity-80">
|
|
355
|
+
{view.entityRefs.length} objects
|
|
356
|
+
{view.transitionMs ? ` · ${(view.transitionMs / 1000).toFixed(1)}s` : ''}
|
|
357
|
+
</div>
|
|
358
|
+
</>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<Button
|
|
363
|
+
type="button"
|
|
364
|
+
variant="secondary"
|
|
365
|
+
size="icon-xs"
|
|
366
|
+
className="absolute top-1 right-7"
|
|
367
|
+
title="Rename view"
|
|
368
|
+
onClick={(e) => {
|
|
369
|
+
e.stopPropagation();
|
|
370
|
+
startRename(view.id, view.name);
|
|
371
|
+
}}
|
|
372
|
+
>
|
|
373
|
+
<Pencil className="h-3 w-3" />
|
|
374
|
+
</Button>
|
|
375
|
+
<Button
|
|
376
|
+
type="button"
|
|
377
|
+
variant="secondary"
|
|
378
|
+
size="icon-xs"
|
|
379
|
+
className="absolute top-1 right-[3.25rem]"
|
|
380
|
+
title="Set transition duration"
|
|
381
|
+
onClick={(e) => {
|
|
382
|
+
e.stopPropagation();
|
|
383
|
+
setViewTransitionDuration(view.id, view.transitionMs);
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
<Timer className="h-3 w-3" />
|
|
387
|
+
</Button>
|
|
388
|
+
<Button
|
|
389
|
+
type="button"
|
|
390
|
+
variant="secondary"
|
|
391
|
+
size="icon-xs"
|
|
392
|
+
className="absolute top-1 right-1"
|
|
393
|
+
title="Delete view"
|
|
394
|
+
onClick={(e) => {
|
|
395
|
+
e.stopPropagation();
|
|
396
|
+
if (playingAll) stopPlayAll();
|
|
397
|
+
if (editingViewId === view.id) cancelRename();
|
|
398
|
+
removeBasketView(view.id);
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
<Trash2 className="h-3 w-3" />
|
|
402
|
+
</Button>
|
|
403
|
+
</div>
|
|
404
|
+
))}
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<Button
|
|
409
|
+
type="button"
|
|
410
|
+
variant="outline"
|
|
411
|
+
size="icon-sm"
|
|
412
|
+
onClick={() => scrollStrip(280)}
|
|
413
|
+
disabled={basketViews.length <= 1}
|
|
414
|
+
title="Scroll right"
|
|
415
|
+
>
|
|
416
|
+
<ChevronRight className="h-4 w-4" />
|
|
417
|
+
</Button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
@@ -54,10 +54,19 @@ import {
|
|
|
54
54
|
Orbit,
|
|
55
55
|
FolderOpen,
|
|
56
56
|
Clock,
|
|
57
|
+
Save,
|
|
57
58
|
} from 'lucide-react';
|
|
58
59
|
import { cn } from '@/lib/utils';
|
|
59
|
-
import { useViewerStore
|
|
60
|
-
import
|
|
60
|
+
import { useViewerStore } from '@/store';
|
|
61
|
+
import { goHomeFromStore, resetVisibilityForHomeFromStore } from '@/store/homeView';
|
|
62
|
+
import {
|
|
63
|
+
executeBasketSet,
|
|
64
|
+
executeBasketAdd,
|
|
65
|
+
executeBasketRemove,
|
|
66
|
+
executeBasketToggleVisibility,
|
|
67
|
+
executeBasketSaveView,
|
|
68
|
+
executeBasketClear,
|
|
69
|
+
} from '@/store/basket/basketCommands';
|
|
61
70
|
import { useSandbox } from '@/hooks/useSandbox';
|
|
62
71
|
import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
|
|
63
72
|
import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
|
|
@@ -172,22 +181,6 @@ function downloadBlob(data: BlobPart, name: string, mime: string) {
|
|
|
172
181
|
URL.revokeObjectURL(url);
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
function getSelectionRefs(): EntityRef[] {
|
|
176
|
-
const s = useViewerStore.getState();
|
|
177
|
-
if (s.selectedEntitiesSet.size > 0) {
|
|
178
|
-
const refs: EntityRef[] = [];
|
|
179
|
-
for (const str of s.selectedEntitiesSet) refs.push(stringToEntityRef(str));
|
|
180
|
-
return refs;
|
|
181
|
-
}
|
|
182
|
-
return s.selectedEntity ? [s.selectedEntity] : [];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function clearMultiSelect() {
|
|
186
|
-
const s = useViewerStore.getState();
|
|
187
|
-
if (s.selectedEntitiesSet.size > 0)
|
|
188
|
-
useViewerStore.setState({ selectedEntitiesSet: new Set(), selectedEntityIds: new Set() });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
184
|
/** Exclusively activate a right-panel content panel (BCF / IDS / Lens).
|
|
192
185
|
* Closes all others first so the if-else chain in ViewerLayout renders it.
|
|
193
186
|
* If the target is already active, closes it (back to Properties). */
|
|
@@ -294,7 +287,7 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
294
287
|
// ── View ──
|
|
295
288
|
c.push(
|
|
296
289
|
{ id: 'view:home', label: 'Home', keywords: 'isometric reset camera', category: 'View', icon: Home, shortcut: 'H',
|
|
297
|
-
action: () => {
|
|
290
|
+
action: () => { goHomeFromStore(); } },
|
|
298
291
|
{ id: 'view:fit', label: 'Fit All', keywords: 'zoom extents entire model', category: 'View', icon: Maximize2, shortcut: 'Z',
|
|
299
292
|
action: () => { useViewerStore.getState().cameraCallbacks.fitAll?.(); } },
|
|
300
293
|
{ id: 'view:frame', label: 'Frame Selection', keywords: 'zoom focus selected', category: 'View', icon: Crosshair, shortcut: 'F',
|
|
@@ -340,19 +333,23 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
340
333
|
if (ids.length > 0) { s.hideEntities(ids); s.clearSelection(); }
|
|
341
334
|
} },
|
|
342
335
|
{ id: 'vis:show', label: 'Show All', keywords: 'unhide reset visible', category: 'Visibility', icon: Eye, shortcut: 'A',
|
|
343
|
-
action: () => {
|
|
344
|
-
{ id: 'vis:
|
|
345
|
-
action: () =>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
{ id: 'vis:
|
|
351
|
-
action: () =>
|
|
352
|
-
{ id: 'vis:
|
|
353
|
-
action: () =>
|
|
354
|
-
|
|
355
|
-
|
|
336
|
+
action: () => { resetVisibilityForHomeFromStore(); } },
|
|
337
|
+
{ id: 'vis:set-iso', label: 'Isolate (Set Basket)', keywords: 'basket isolate set selection hierarchy view equals', category: 'Visibility', icon: Equal, shortcut: 'I',
|
|
338
|
+
action: () => executeBasketSet() },
|
|
339
|
+
{ id: 'vis:add-iso', label: 'Add to Basket', keywords: 'basket plus selection hierarchy view', category: 'Visibility', icon: Plus, shortcut: '+',
|
|
340
|
+
action: () => executeBasketAdd() },
|
|
341
|
+
{ id: 'vis:remove-iso', label: 'Remove from Basket', keywords: 'basket minus selection hierarchy view', category: 'Visibility', icon: Minus, shortcut: '−',
|
|
342
|
+
action: () => executeBasketRemove() },
|
|
343
|
+
{ id: 'vis:toggle-iso', label: 'Toggle Basket Visibility', keywords: 'basket show hide', category: 'Visibility', icon: Eye,
|
|
344
|
+
action: () => executeBasketToggleVisibility() },
|
|
345
|
+
{ id: 'vis:save-view', label: 'Save Basket as View', keywords: 'basket presentation thumbnail', category: 'Visibility', icon: Save,
|
|
346
|
+
action: () => executeBasketSaveView().catch((err) => {
|
|
347
|
+
console.error('[CommandPalette] Failed to save basket view:', err);
|
|
348
|
+
}) },
|
|
349
|
+
{ id: 'vis:toggle-presentation', label: 'Toggle Basket Presentation Dock', keywords: 'basket panel carousel thumbnails', category: 'Visibility', icon: Layout,
|
|
350
|
+
action: () => { useViewerStore.getState().toggleBasketPresentationVisible(); } },
|
|
351
|
+
{ id: 'vis:clear-iso', label: 'Clear Basket', keywords: 'basket clear reset', category: 'Visibility', icon: RotateCcw,
|
|
352
|
+
action: () => executeBasketClear() },
|
|
356
353
|
{ id: 'vis:spaces', label: 'Spaces', keywords: 'IfcSpace rooms show hide', category: 'Visibility', icon: Box,
|
|
357
354
|
action: () => { useViewerStore.getState().toggleTypeVisibility('spaces'); } },
|
|
358
355
|
{ id: 'vis:openings', label: 'Openings', keywords: 'IfcOpeningElement show hide', category: 'Visibility', icon: SquareX,
|