@selvajs/ui 6.1.2 → 6.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.
- package/dist/components/ConfirmDialog.svelte +59 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +13 -0
- package/dist/components/compute/ComputeApp.svelte +10 -0
- package/dist/components/compute/ComputeApp.svelte.d.ts +3 -0
- package/dist/components/compute/ParameterPresetManager.svelte +3 -3
- package/dist/components/layout/AppShell.svelte +19 -2
- package/dist/components/layout/SideNav.svelte +16 -2
- package/dist/components/layout/SideNav.svelte.d.ts +5 -0
- package/dist/components/preview/inputs/FileInput.svelte +3 -2
- package/dist/components/primitives/Callout.svelte +73 -0
- package/dist/components/primitives/Callout.svelte.d.ts +40 -0
- package/dist/components/primitives/index.d.ts +2 -0
- package/dist/components/primitives/index.js +2 -0
- package/dist/components/viewer/MeshMetadataDialog.svelte +1 -1
- package/dist/components/viewer/SceneManager.svelte +274 -103
- package/dist/components/viewer/SceneManager.svelte.d.ts +6 -0
- package/dist/components/viewer/Viewer.svelte +104 -32
- package/dist/i18n/messages.d.ts +4 -2
- package/dist/i18n/messages.js +8 -4
- package/package.json +14 -14
- package/src/lib/components/ConfirmDialog.svelte +59 -0
- package/src/lib/components/compute/ComputeApp.svelte +10 -0
- package/src/lib/components/compute/ParameterPresetManager.svelte +3 -3
- package/src/lib/components/layout/AppShell.svelte +19 -2
- package/src/lib/components/layout/SideNav.svelte +16 -2
- package/src/lib/components/preview/inputs/FileInput.svelte +3 -2
- package/src/lib/components/primitives/Callout.svelte +73 -0
- package/src/lib/components/primitives/index.ts +2 -0
- package/src/lib/components/viewer/MeshMetadataDialog.svelte +1 -1
- package/src/lib/components/viewer/SceneManager.svelte +274 -103
- package/src/lib/components/viewer/Viewer.svelte +104 -32
- package/src/lib/i18n/messages.ts +12 -6
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import * as THREE from 'three';
|
|
3
|
-
import { Eye, EyeOff, ChevronRight, Search, X } from '@lucide/svelte';
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Eye,
|
|
4
|
+
EyeOff,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
ChevronsDownUp,
|
|
7
|
+
ChevronsUpDown,
|
|
8
|
+
Search,
|
|
9
|
+
X
|
|
10
|
+
} from '@lucide/svelte';
|
|
11
|
+
import {
|
|
12
|
+
getMemberKeys,
|
|
7
13
|
getTypeLabel,
|
|
14
|
+
type SceneEntry,
|
|
8
15
|
type SceneOutliner
|
|
9
16
|
} from '@selvajs/visualization/scene';
|
|
10
17
|
import { getLocaleContext } from '../../i18n/localeContext.svelte';
|
|
@@ -20,20 +27,46 @@
|
|
|
20
27
|
*/
|
|
21
28
|
outliner: SceneOutliner;
|
|
22
29
|
sceneVersion?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Request a viewer repaint. Toggling `.visible` mutates three objects directly, and the
|
|
32
|
+
* render loop is on-demand — without this the canvas only catches up on its ~500ms idle
|
|
33
|
+
* repaint, so the row updates instantly and the geometry lags behind.
|
|
34
|
+
*/
|
|
35
|
+
onVisibilityChange?: () => void;
|
|
23
36
|
}
|
|
24
37
|
|
|
25
|
-
let { outliner, sceneVersion = 0 }: Props = $props();
|
|
38
|
+
let { outliner, sceneVersion = 0, onVisibilityChange }: Props = $props();
|
|
39
|
+
|
|
40
|
+
// A partially hidden layer hides the rest; only a fully hidden one comes back — the same rule
|
|
41
|
+
// `visibility.toggleLayer` applies to objects, restated over entries because a layer's rows are
|
|
42
|
+
// now members, not necessarily whole objects.
|
|
43
|
+
const toggleLayer = (entries: SceneEntry[]) => {
|
|
44
|
+
const show = entries.length > 0 && entries.every((entry) => isEntryHidden(entry));
|
|
45
|
+
for (const entry of entries) outliner.visibility.setEntryVisible(entry, show);
|
|
46
|
+
onVisibilityChange?.();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const toggleEntry = (entry: SceneEntry) => {
|
|
50
|
+
outliner.toggleEntry(entry);
|
|
51
|
+
onVisibilityChange?.();
|
|
52
|
+
};
|
|
26
53
|
|
|
27
54
|
// Derived, not destructured: the prop is reassignable, and these must follow it.
|
|
28
55
|
const hidden = $derived(outliner.visibility.hidden);
|
|
29
56
|
const selected = $derived(outliner.selection.selected);
|
|
30
57
|
const collapsed = $derived(outliner.collapsed);
|
|
31
58
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
59
|
+
// Panel state, so reopening clears it — unlike hiding and collapse, which the outliner outlives.
|
|
60
|
+
// Owned here rather than on the outliner: writing it back from the derived below is what the
|
|
61
|
+
// Svelte 5 `state_unsafe_mutation` rule forbids.
|
|
34
62
|
let searchQuery = $state('');
|
|
35
63
|
let anchor = $state<string | null>(null);
|
|
36
64
|
|
|
65
|
+
// Layers start collapsed: a solve can produce dozens, and an all-expanded list buries the
|
|
66
|
+
// layer names it exists to show. Only for the first batch of content — after that the user's
|
|
67
|
+
// collapse state is theirs, and re-collapsing on every solve would fight them.
|
|
68
|
+
let collapsedOnLoad = false;
|
|
69
|
+
|
|
37
70
|
$effect(() => outliner.onAnchorChange((next) => (anchor = next)));
|
|
38
71
|
|
|
39
72
|
// `scene.children` is a plain array the render layer mutates in place, so a solve bumping
|
|
@@ -43,29 +76,135 @@
|
|
|
43
76
|
return outliner.objects();
|
|
44
77
|
});
|
|
45
78
|
|
|
79
|
+
// Entries, not objects: a merged mesh renders as one THREE object but lists as one row per
|
|
80
|
+
// source object, so an imported model shows its building elements instead of the handful of
|
|
81
|
+
// meshes they were merged into.
|
|
46
82
|
const layerGroups = $derived.by(() => {
|
|
47
83
|
void sceneVersion;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
return outliner.entryGroups(searchQuery);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const collapseAll = () => {
|
|
88
|
+
for (const layerName of outliner.entryGroups().keys()) collapsed.add(layerName);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
$effect(() => {
|
|
92
|
+
void sceneVersion;
|
|
93
|
+
if (collapsedOnLoad || outliner.entryGroups().size === 0) return;
|
|
94
|
+
collapsedOnLoad = true;
|
|
95
|
+
collapseAll();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const expandAll = () => collapsed.clear();
|
|
99
|
+
|
|
100
|
+
const allCollapsed = $derived(
|
|
101
|
+
layerGroups.size > 0 && [...layerGroups.keys()].every((name) => collapsed.has(name))
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Windowing
|
|
106
|
+
// ============================================================================
|
|
107
|
+
//
|
|
108
|
+
// A scene can hold thousands of objects. Mounting a row per object makes expanding a layer,
|
|
109
|
+
// typing in the search box, and scrolling all pay for the whole list, so only the rows
|
|
110
|
+
// overlapping the viewport are rendered and the rest are replaced by spacer height.
|
|
111
|
+
//
|
|
112
|
+
// The two row types have different heights, so the flat model records each row's height and a
|
|
113
|
+
// running offset rather than assuming a single row size. Keep these in sync with the markup:
|
|
114
|
+
// a header is `py-1` around a 3.5-unit icon, an object row `py-0.5` around a 3-unit one.
|
|
115
|
+
const LAYER_ROW_HEIGHT = 30;
|
|
116
|
+
const OBJECT_ROW_HEIGHT = 24;
|
|
117
|
+
// Rows rendered beyond each edge of the viewport, so a fast scroll doesn't show blank space.
|
|
118
|
+
const OVERSCAN = 8;
|
|
119
|
+
|
|
120
|
+
type Row =
|
|
121
|
+
| {
|
|
122
|
+
kind: 'layer';
|
|
123
|
+
key: string;
|
|
124
|
+
top: number;
|
|
125
|
+
height: number;
|
|
126
|
+
layerName: string;
|
|
127
|
+
entries: SceneEntry[];
|
|
128
|
+
}
|
|
129
|
+
| { kind: 'object'; key: string; top: number; height: number; entry: SceneEntry };
|
|
130
|
+
|
|
131
|
+
const rows = $derived.by(() => {
|
|
132
|
+
const flat: Row[] = [];
|
|
133
|
+
let top = 0;
|
|
134
|
+
for (const [layerName, entries] of layerGroups) {
|
|
135
|
+
flat.push({
|
|
136
|
+
kind: 'layer',
|
|
137
|
+
key: `layer:${layerName}`,
|
|
138
|
+
top,
|
|
139
|
+
height: LAYER_ROW_HEIGHT,
|
|
140
|
+
layerName,
|
|
141
|
+
entries
|
|
142
|
+
});
|
|
143
|
+
top += LAYER_ROW_HEIGHT;
|
|
144
|
+
if (collapsed.has(layerName)) continue;
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
flat.push({
|
|
147
|
+
kind: 'object',
|
|
148
|
+
key: entry.key,
|
|
149
|
+
top,
|
|
150
|
+
height: OBJECT_ROW_HEIGHT,
|
|
151
|
+
entry
|
|
152
|
+
});
|
|
153
|
+
top += OBJECT_ROW_HEIGHT;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return flat;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const totalHeight = $derived(
|
|
160
|
+
rows.length === 0 ? 0 : rows[rows.length - 1]!.top + rows[rows.length - 1]!.height
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let scrollTop = $state(0);
|
|
164
|
+
let viewportHeight = $state(0);
|
|
165
|
+
|
|
166
|
+
/** First row index whose bottom edge is at or past `offset`. Rows are ordered by `top`. */
|
|
167
|
+
const findRowAt = (offset: number) => {
|
|
168
|
+
let low = 0;
|
|
169
|
+
let high = rows.length - 1;
|
|
170
|
+
while (low < high) {
|
|
171
|
+
const mid = (low + high) >> 1;
|
|
172
|
+
if (rows[mid]!.top + rows[mid]!.height <= offset) low = mid + 1;
|
|
173
|
+
else high = mid;
|
|
174
|
+
}
|
|
175
|
+
return low;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const visibleRows = $derived.by(() => {
|
|
179
|
+
if (rows.length === 0) return [];
|
|
180
|
+
// Before the first measurement `viewportHeight` is 0; render a screenful so the list is
|
|
181
|
+
// never briefly empty.
|
|
182
|
+
const height = viewportHeight || 600;
|
|
183
|
+
const start = Math.max(0, findRowAt(scrollTop) - OVERSCAN);
|
|
184
|
+
const end = Math.min(rows.length, findRowAt(scrollTop + height) + 1 + OVERSCAN);
|
|
185
|
+
return rows.slice(start, end);
|
|
51
186
|
});
|
|
52
187
|
|
|
53
188
|
// `SvelteSet.has()` is the reactive read, so go through the set rather than calling
|
|
54
189
|
// `visibility.isHidden` — that reaches the set through a plain reference inside the outliner
|
|
55
190
|
// and returns a correct value that never re-renders this row.
|
|
56
|
-
const
|
|
191
|
+
const isEntryHidden = (entry: SceneEntry) =>
|
|
192
|
+
entry.memberIndex === null
|
|
193
|
+
? getMemberKeys(entry.object).every((key) => hidden.has(key))
|
|
194
|
+
: hidden.has(entry.key);
|
|
57
195
|
|
|
58
|
-
// Same reason: count through the reactive set so the layer's tri-state eye tracks its
|
|
59
|
-
const hiddenCount = (
|
|
60
|
-
|
|
196
|
+
// Same reason: count through the reactive set so the layer's tri-state eye tracks its entries.
|
|
197
|
+
const hiddenCount = (entries: SceneEntry[]) =>
|
|
198
|
+
entries.filter((entry) => isEntryHidden(entry)).length;
|
|
61
199
|
|
|
62
200
|
// Reading `anchor` keeps the shift-range dependent on it; the outliner owns the value.
|
|
63
201
|
const selectObject = (uuid: string, event: MouseEvent) => {
|
|
64
202
|
void anchor;
|
|
65
|
-
outliner.select(
|
|
66
|
-
|
|
67
|
-
toggleKey: event.ctrlKey || event.metaKey
|
|
68
|
-
|
|
203
|
+
outliner.select(
|
|
204
|
+
uuid,
|
|
205
|
+
{ shiftKey: event.shiftKey, toggleKey: event.ctrlKey || event.metaKey },
|
|
206
|
+
searchQuery
|
|
207
|
+
);
|
|
69
208
|
};
|
|
70
209
|
</script>
|
|
71
210
|
|
|
@@ -88,110 +227,142 @@
|
|
|
88
227
|
/>
|
|
89
228
|
</button>
|
|
90
229
|
{/if}
|
|
230
|
+
|
|
231
|
+
<button
|
|
232
|
+
class="shrink-0"
|
|
233
|
+
onclick={() => (allCollapsed ? expandAll() : collapseAll())}
|
|
234
|
+
title={allCollapsed ? t.expandAll : t.collapseAll}
|
|
235
|
+
aria-label={allCollapsed ? t.expandAll : t.collapseAll}
|
|
236
|
+
>
|
|
237
|
+
{#if allCollapsed}
|
|
238
|
+
<ChevronsUpDown
|
|
239
|
+
class="h-3.5 w-3.5 text-muted-foreground/50 transition-colors hover:text-muted-foreground"
|
|
240
|
+
/>
|
|
241
|
+
{:else}
|
|
242
|
+
<ChevronsDownUp
|
|
243
|
+
class="h-3.5 w-3.5 text-muted-foreground/50 transition-colors hover:text-muted-foreground"
|
|
244
|
+
/>
|
|
245
|
+
{/if}
|
|
246
|
+
</button>
|
|
91
247
|
</div>
|
|
92
248
|
|
|
93
|
-
<div
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<button
|
|
112
|
-
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
113
|
-
onclick={() => outliner.visibility.toggleLayer(objects)}
|
|
114
|
-
title={layerHidden ? t.showLayer : t.hideLayer}
|
|
115
|
-
aria-label={layerHidden ? t.showLayer : t.hideLayer}
|
|
116
|
-
>
|
|
117
|
-
{#if layerHidden}
|
|
118
|
-
<EyeOff class="h-3.5 w-3.5 text-muted-foreground/40" />
|
|
119
|
-
{:else if layerPartial}
|
|
120
|
-
<Eye class="h-3.5 w-3.5 text-muted-foreground/60" />
|
|
121
|
-
{:else}
|
|
122
|
-
<Eye class="h-3.5 w-3.5 text-muted-foreground" />
|
|
123
|
-
{/if}
|
|
124
|
-
</button>
|
|
125
|
-
|
|
126
|
-
<span
|
|
127
|
-
class="min-w-0 text-xs font-medium flex-1 truncate {layerHidden
|
|
128
|
-
? 'text-muted-foreground/40 line-through'
|
|
129
|
-
: 'text-foreground'}"
|
|
130
|
-
>
|
|
131
|
-
{layerName}
|
|
132
|
-
</span>
|
|
133
|
-
|
|
134
|
-
<span class="shrink-0 text-[10px] text-muted-foreground/50 tabular-nums">
|
|
135
|
-
{objects.length}
|
|
136
|
-
</span>
|
|
137
|
-
</div>
|
|
249
|
+
<div
|
|
250
|
+
class="py-1 flex-1 overflow-y-auto"
|
|
251
|
+
bind:clientHeight={viewportHeight}
|
|
252
|
+
onscroll={(e) => (scrollTop = e.currentTarget.scrollTop)}
|
|
253
|
+
>
|
|
254
|
+
<!-- Spacer sized to the full list; windowed rows are placed into it by offset. The listbox
|
|
255
|
+
sits here rather than per layer: windowing renders one flat row list, so every option
|
|
256
|
+
shares a single owner. -->
|
|
257
|
+
<div role="listbox" aria-multiselectable="true" class="relative" style:height="{totalHeight}px">
|
|
258
|
+
{#each visibleRows as row (row.key)}
|
|
259
|
+
<div class="inset-x-0 absolute" style:top="{row.top}px" style:height="{row.height}px">
|
|
260
|
+
{#if row.kind === 'layer'}
|
|
261
|
+
{@const entries = row.entries}
|
|
262
|
+
{@const numHidden = hiddenCount(entries)}
|
|
263
|
+
{@const layerHidden = entries.length > 0 && numHidden === entries.length}
|
|
264
|
+
{@const layerPartial = numHidden > 0 && numHidden < entries.length}
|
|
265
|
+
{@const isCollapsed = collapsed.has(row.layerName)}
|
|
138
266
|
|
|
139
|
-
{#if !isCollapsed}
|
|
140
|
-
<div role="listbox" aria-multiselectable="true" class="ml-3 border-l border-border">
|
|
141
|
-
{#each objects as object (object.uuid)}
|
|
142
|
-
<!-- Visibility is keyed by Grasshopper identity (so it survives a solve), selection
|
|
143
|
-
by uuid (so it does not) — hence the two different lookups. -->
|
|
144
|
-
{@const isHidden = isObjectHidden(object)}
|
|
145
|
-
{@const isSelected = selected.has(object.uuid)}
|
|
146
267
|
<div
|
|
147
|
-
|
|
148
|
-
aria-selected={isSelected}
|
|
149
|
-
tabindex="-1"
|
|
150
|
-
class="gap-1.5 pl-5 pr-2 py-0.5 flex cursor-pointer items-center transition-colors
|
|
151
|
-
{isSelected ? 'bg-primary/10 hover:bg-primary/15' : 'hover:bg-muted'}
|
|
152
|
-
{isHidden ? 'opacity-40' : ''}"
|
|
153
|
-
onmousedown={(e) => {
|
|
154
|
-
e.stopPropagation();
|
|
155
|
-
if (e.shiftKey) e.preventDefault();
|
|
156
|
-
}}
|
|
157
|
-
onclick={(e) => selectObject(object.uuid, e)}
|
|
158
|
-
onkeydown={(e) =>
|
|
159
|
-
e.key === 'Enter' && selectObject(object.uuid, e as unknown as MouseEvent)}
|
|
268
|
+
class="gap-1 pl-1 pr-2 group flex h-full items-center transition-colors hover:bg-muted"
|
|
160
269
|
>
|
|
270
|
+
<button
|
|
271
|
+
class="rounded p-0.5 shrink-0 text-muted-foreground transition-colors hover:text-muted-foreground"
|
|
272
|
+
onclick={() => outliner.toggleCollapsed(row.layerName)}
|
|
273
|
+
aria-label={isCollapsed ? t.expandLayer : t.collapseLayer}
|
|
274
|
+
>
|
|
275
|
+
<ChevronRight
|
|
276
|
+
class="h-3.5 w-3.5 transition-transform duration-150 {isCollapsed
|
|
277
|
+
? ''
|
|
278
|
+
: 'rotate-90'}"
|
|
279
|
+
/>
|
|
280
|
+
</button>
|
|
281
|
+
|
|
161
282
|
<button
|
|
162
283
|
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
163
|
-
onclick={(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}}
|
|
167
|
-
title={isHidden ? t.showObject : t.hideObject}
|
|
168
|
-
aria-label={isHidden ? t.showObject : t.hideObject}
|
|
284
|
+
onclick={() => toggleLayer(entries)}
|
|
285
|
+
title={layerHidden ? t.showLayer : t.hideLayer}
|
|
286
|
+
aria-label={layerHidden ? t.showLayer : t.hideLayer}
|
|
169
287
|
>
|
|
170
|
-
{#if
|
|
171
|
-
<EyeOff class="h-3 w-3 text-muted-foreground/
|
|
288
|
+
{#if layerHidden}
|
|
289
|
+
<EyeOff class="h-3.5 w-3.5 text-muted-foreground/40" />
|
|
290
|
+
{:else if layerPartial}
|
|
291
|
+
<Eye class="h-3.5 w-3.5 text-muted-foreground/60" />
|
|
172
292
|
{:else}
|
|
173
|
-
<Eye class="h-3 w-3 text-muted-foreground" />
|
|
293
|
+
<Eye class="h-3.5 w-3.5 text-muted-foreground" />
|
|
174
294
|
{/if}
|
|
175
295
|
</button>
|
|
176
296
|
|
|
177
297
|
<span
|
|
178
|
-
class="min-w-0 text-xs flex-1 truncate {
|
|
179
|
-
? 'text-muted-foreground line-through'
|
|
180
|
-
: 'text-foreground
|
|
298
|
+
class="min-w-0 text-xs font-medium flex-1 truncate {layerHidden
|
|
299
|
+
? 'text-muted-foreground/40 line-through'
|
|
300
|
+
: 'text-foreground'}"
|
|
181
301
|
>
|
|
182
|
-
{
|
|
302
|
+
{row.layerName}
|
|
183
303
|
</span>
|
|
184
304
|
|
|
185
|
-
<span
|
|
186
|
-
|
|
187
|
-
>
|
|
188
|
-
{getTypeLabel(object)}
|
|
305
|
+
<span class="shrink-0 text-[10px] text-muted-foreground/50 tabular-nums">
|
|
306
|
+
{entries.length}
|
|
189
307
|
</span>
|
|
190
308
|
</div>
|
|
191
|
-
{
|
|
309
|
+
{:else}
|
|
310
|
+
{@const entry = row.entry}
|
|
311
|
+
<!-- Both keyed by the entry's stable identity: a merged mesh holds many entries, so
|
|
312
|
+
the object's uuid cannot tell its members apart. -->
|
|
313
|
+
{@const isHidden = isEntryHidden(entry)}
|
|
314
|
+
{@const isSelected = selected.has(entry.key)}
|
|
315
|
+
<div class="ml-3 h-full border-l border-border">
|
|
316
|
+
<div
|
|
317
|
+
role="option"
|
|
318
|
+
aria-selected={isSelected}
|
|
319
|
+
tabindex="-1"
|
|
320
|
+
class="gap-1.5 pl-5 pr-2 flex h-full cursor-pointer items-center transition-colors
|
|
321
|
+
{isSelected ? 'bg-primary/10 hover:bg-primary/15' : 'hover:bg-muted'}
|
|
322
|
+
{isHidden ? 'opacity-40' : ''}"
|
|
323
|
+
onmousedown={(e) => {
|
|
324
|
+
e.stopPropagation();
|
|
325
|
+
if (e.shiftKey) e.preventDefault();
|
|
326
|
+
}}
|
|
327
|
+
onclick={(e) => selectObject(entry.key, e)}
|
|
328
|
+
onkeydown={(e) =>
|
|
329
|
+
e.key === 'Enter' && selectObject(entry.key, e as unknown as MouseEvent)}
|
|
330
|
+
>
|
|
331
|
+
<button
|
|
332
|
+
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
333
|
+
onclick={(e) => {
|
|
334
|
+
e.stopPropagation();
|
|
335
|
+
toggleEntry(entry);
|
|
336
|
+
}}
|
|
337
|
+
title={isHidden ? t.showObject : t.hideObject}
|
|
338
|
+
aria-label={isHidden ? t.showObject : t.hideObject}
|
|
339
|
+
>
|
|
340
|
+
{#if isHidden}
|
|
341
|
+
<EyeOff class="h-3 w-3 text-muted-foreground/60" />
|
|
342
|
+
{:else}
|
|
343
|
+
<Eye class="h-3 w-3 text-muted-foreground" />
|
|
344
|
+
{/if}
|
|
345
|
+
</button>
|
|
346
|
+
|
|
347
|
+
<span
|
|
348
|
+
class="min-w-0 text-xs flex-1 truncate {isHidden
|
|
349
|
+
? 'text-muted-foreground line-through'
|
|
350
|
+
: 'text-foreground/80'}"
|
|
351
|
+
>
|
|
352
|
+
{entry.label}
|
|
353
|
+
</span>
|
|
354
|
+
|
|
355
|
+
<span
|
|
356
|
+
class="rounded px-1 py-0.5 font-medium shrink-0 bg-muted text-[9px] text-muted-foreground/70"
|
|
357
|
+
>
|
|
358
|
+
{getTypeLabel(entry.object)}
|
|
359
|
+
</span>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
{/if}
|
|
192
363
|
</div>
|
|
193
|
-
{/
|
|
194
|
-
|
|
364
|
+
{/each}
|
|
365
|
+
</div>
|
|
195
366
|
|
|
196
367
|
{#if sceneObjects.length === 0}
|
|
197
368
|
<div class="py-12 flex flex-col items-center justify-center text-center">
|
|
@@ -7,6 +7,12 @@ interface Props {
|
|
|
7
7
|
*/
|
|
8
8
|
outliner: SceneOutliner;
|
|
9
9
|
sceneVersion?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Request a viewer repaint. Toggling `.visible` mutates three objects directly, and the
|
|
12
|
+
* render loop is on-demand — without this the canvas only catches up on its ~500ms idle
|
|
13
|
+
* repaint, so the row updates instantly and the geometry lags behind.
|
|
14
|
+
*/
|
|
15
|
+
onVisibilityChange?: () => void;
|
|
10
16
|
}
|
|
11
17
|
declare const SceneManager: import("svelte").Component<Props, {}, "">;
|
|
12
18
|
type SceneManager = ReturnType<typeof SceneManager>;
|