@selvajs/ui 6.0.0-beta.8 → 6.1.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/README.md +5 -7
- package/dist/components/compute/AppLayout.svelte +3 -8
- package/dist/components/compute/AppLayout.svelte.d.ts +0 -2
- package/dist/components/compute/ComputeApp.svelte +30 -43
- package/dist/components/compute/ComputeApp.svelte.d.ts +11 -24
- package/dist/components/compute/ParameterPresetManager.svelte +4 -9
- package/dist/components/compute/ParameterPresetManager.svelte.d.ts +1 -1
- package/dist/components/layout/AppShell.svelte +5 -13
- package/dist/components/layout/AppShell.svelte.d.ts +1 -2
- package/dist/components/layout/EmptyState.svelte +2 -3
- package/dist/components/layout/PageFooter.svelte +0 -4
- package/dist/components/layout/index.js +0 -1
- package/dist/components/preview/Group.svelte +2 -5
- package/dist/components/preview/ImageOutput.svelte +5 -4
- package/dist/components/preview/InputControl.svelte +27 -28
- package/dist/components/preview/InputControl.svelte.d.ts +3 -3
- package/dist/components/preview/OutputDisplay.svelte +3 -6
- package/dist/components/preview/TabLayout.svelte +3 -3
- package/dist/components/preview/inputs/FileInput.svelte +9 -12
- package/dist/components/primitives/field/field-error.svelte +1 -3
- package/dist/components/primitives/index.js +0 -1
- package/dist/components/viewer/SceneManager.svelte +9 -20
- package/dist/components/viewer/SceneManager.svelte.d.ts +3 -3
- package/dist/components/viewer/Viewer.svelte +42 -63
- package/dist/components/viewer/Viewer.svelte.d.ts +8 -19
- package/dist/composables/useFooterItem.svelte.d.ts +3 -4
- package/dist/composables/useFooterItem.svelte.js +3 -3
- package/dist/compute/solving.svelte.d.ts +6 -6
- package/dist/compute/solving.svelte.js +6 -6
- package/dist/compute/useSolveSession.svelte.d.ts +5 -7
- package/dist/compute/useSolveSession.svelte.js +11 -24
- package/dist/constants.js +4 -4
- package/dist/contexts/clientSlotContext.svelte.d.ts +3 -5
- package/dist/contexts/footerContext.svelte.d.ts +6 -8
- package/dist/external/storage.js +2 -3
- package/dist/i18n/localeContext.svelte.d.ts +5 -7
- package/dist/i18n/localeContext.svelte.js +8 -15
- package/dist/i18n/messages.d.ts +0 -2
- package/dist/i18n/messages.js +3 -8
- package/dist/index.js +7 -13
- package/dist/public.js +14 -30
- package/dist/schema/defaults.js +2 -3
- package/dist/schema/dynamic-value-list.js +13 -15
- package/dist/schema/param-exporter.d.ts +7 -14
- package/dist/schema/param-exporter.js +6 -12
- package/dist/schema/traversal.js +3 -4
- package/dist/schema/visibility-rules.d.ts +4 -8
- package/dist/schema/visibility-rules.js +4 -8
- package/dist/types/actionButton.d.ts +1 -25
- package/dist/types/presetLabels.d.ts +2 -3
- package/dist/utils/color.js +6 -9
- package/dist/utils/debounce.d.ts +1 -11
- package/dist/utils/debounce.js +1 -11
- package/dist/utils/loadScript.d.ts +2 -7
- package/dist/utils/loadScript.js +2 -7
- package/package.json +17 -17
- package/src/lib/components/compute/AppLayout.svelte +3 -8
- package/src/lib/components/compute/ComputeApp.svelte +30 -43
- package/src/lib/components/compute/ParameterPresetManager.svelte +4 -9
- package/src/lib/components/layout/AppShell.svelte +5 -13
- package/src/lib/components/layout/EmptyState.svelte +2 -3
- package/src/lib/components/layout/PageFooter.svelte +0 -4
- package/src/lib/components/layout/index.ts +0 -1
- package/src/lib/components/preview/Group.svelte +2 -5
- package/src/lib/components/preview/ImageOutput.svelte +5 -4
- package/src/lib/components/preview/InputControl.svelte +27 -28
- package/src/lib/components/preview/OutputDisplay.svelte +3 -6
- package/src/lib/components/preview/TabLayout.svelte +3 -3
- package/src/lib/components/preview/inputs/FileInput.svelte +9 -12
- package/src/lib/components/primitives/field/field-error.svelte +1 -3
- package/src/lib/components/primitives/index.ts +0 -2
- package/src/lib/components/viewer/SceneManager.svelte +9 -20
- package/src/lib/components/viewer/Viewer.svelte +42 -63
- package/src/lib/composables/useFooterItem.svelte.ts +3 -4
- package/src/lib/compute/solving.svelte.ts +6 -6
- package/src/lib/compute/useSolveSession.svelte.ts +11 -24
- package/src/lib/constants.ts +4 -4
- package/src/lib/contexts/clientSlotContext.svelte.ts +7 -12
- package/src/lib/contexts/footerContext.svelte.ts +6 -8
- package/src/lib/external/storage.ts +2 -3
- package/src/lib/i18n/localeContext.svelte.ts +8 -17
- package/src/lib/i18n/messages.ts +3 -10
- package/src/lib/index.ts +8 -13
- package/src/lib/public.ts +22 -40
- package/src/lib/schema/defaults.ts +2 -3
- package/src/lib/schema/dynamic-value-list.ts +13 -15
- package/src/lib/schema/param-exporter.ts +9 -16
- package/src/lib/schema/traversal.ts +3 -4
- package/src/lib/schema/visibility-rules.ts +4 -8
- package/src/lib/types/actionButton.ts +1 -25
- package/src/lib/types/presetLabels.ts +2 -3
- package/src/lib/utils/color.ts +6 -9
- package/src/lib/utils/debounce.ts +1 -11
- package/src/lib/utils/loadScript.ts +2 -7
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
value?: SupportedTypes;
|
|
35
35
|
displayName?: string;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* `forceSolve` solves even in manual-solve mode. Used for system-initiated changes
|
|
38
|
+
* (e.g. pruning a vanished dynamic-list selection), where leaving the previous output
|
|
39
|
+
* on screen would misrepresent the input that now sits above it.
|
|
40
40
|
*/
|
|
41
41
|
onChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
42
42
|
disabled?: boolean;
|
|
@@ -56,9 +56,8 @@
|
|
|
56
56
|
const inputId = $derived(`input-${item.paramId}`);
|
|
57
57
|
const label = $derived(displayName || item.displayName || item.paramId);
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// client slot source means presentation === 'slot'.
|
|
59
|
+
// The 'hidden' presentation never reaches here — visible:false filters it out
|
|
60
|
+
// upstream — so a client-sourced input here always means presentation === 'slot'.
|
|
62
61
|
const isClientSlot = $derived.by(() => {
|
|
63
62
|
const source = (item as { source?: { kind?: string; client?: { presentation?: string } } })
|
|
64
63
|
.source;
|
|
@@ -66,7 +65,7 @@
|
|
|
66
65
|
});
|
|
67
66
|
const clientSlot = getClientSlot();
|
|
68
67
|
|
|
69
|
-
//
|
|
68
|
+
// Min/max hint rendered next to the label. `hideRange` lets the author suppress it.
|
|
70
69
|
const numberRangeHint = $derived.by(() => {
|
|
71
70
|
if (!isNumberWidget(item)) return null;
|
|
72
71
|
const cfg = item.config;
|
|
@@ -81,8 +80,8 @@
|
|
|
81
80
|
|
|
82
81
|
const showRangeInLabel = $derived(isNumberWidget(item) && numberRangeHint !== null);
|
|
83
82
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
83
|
+
// Options from the last solve win over the author's seed list, so the control is empty
|
|
84
|
+
// until the first solve unless defaultOptions is set.
|
|
86
85
|
const dynamicListConfig = $derived(
|
|
87
86
|
isDynamicValueListWidget(item)
|
|
88
87
|
? (item.config as DynamicValueListWidgetConfig | undefined)
|
|
@@ -109,22 +108,22 @@
|
|
|
109
108
|
(dynamicListConfig?.emptyBehavior ?? 'hide') === 'hide'
|
|
110
109
|
);
|
|
111
110
|
|
|
112
|
-
// When a dynamic value list recomputes,
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
111
|
+
// When a dynamic value list recomputes, the selected value may no longer be an option.
|
|
112
|
+
// Prune it so the control shows a valid option instead of rendering the orphaned raw
|
|
113
|
+
// value as its own label. The user didn't pick this, so force a solve — otherwise a
|
|
114
|
+
// manual-solve schema keeps the prior output on screen, making it look like the
|
|
115
|
+
// auto-picked option produced it.
|
|
117
116
|
//
|
|
118
|
-
// INVARIANT: a dynamic value list must never dispatch an empty/null value to solve.
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// is deliberately no "user cleared it, stay empty" path.
|
|
117
|
+
// INVARIANT: a dynamic value list must never dispatch an empty/null value to solve. An
|
|
118
|
+
// empty selection reaches the definition as null/"", which throws NREs in downstream
|
|
119
|
+
// geometry components (e.g. Bounding Rectangle) and nulls every output beyond them.
|
|
120
|
+
// There is always at least one option, so every terminal state below resolves to one;
|
|
121
|
+
// there is deliberately no "user cleared it, stay empty" path.
|
|
123
122
|
//
|
|
124
|
-
// LOOP BREAKER: a definition whose computed options DEPEND on the selection can oscillate
|
|
125
|
-
// auto-pick A → solve → new options exclude A → auto-pick B → solve → … Each cycle
|
|
126
|
-
// solves and re-parses
|
|
127
|
-
//
|
|
123
|
+
// LOOP BREAKER: a definition whose computed options DEPEND on the selection can oscillate
|
|
124
|
+
// — auto-pick A → solve → new options exclude A → auto-pick B → solve → … Each cycle
|
|
125
|
+
// force-solves and re-parses the (possibly multi-MB) options, freezing the tab: every
|
|
126
|
+
// solve result blocks the main thread and the loop never yields. Bound consecutive
|
|
128
127
|
// system-initiated picks; any real user commit resets the budget.
|
|
129
128
|
const MAX_CONSECUTIVE_AUTO_PICKS = 3;
|
|
130
129
|
let autoPickCount = 0;
|
|
@@ -133,11 +132,11 @@
|
|
|
133
132
|
if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
|
|
134
133
|
const validValues = new Set(Object.values(dynamicListOptions));
|
|
135
134
|
const firstOption = Object.values(dynamicListOptions)[0];
|
|
136
|
-
// Already valid → stable; refill the auto-pick budget and stop.
|
|
137
135
|
const isValid = Array.isArray(value)
|
|
138
136
|
? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
|
|
139
137
|
: typeof value === 'string' && value !== '' && validValues.has(value);
|
|
140
138
|
if (isValid) {
|
|
139
|
+
// Settled on a valid option — refill the budget.
|
|
141
140
|
autoPickCount = 0;
|
|
142
141
|
return;
|
|
143
142
|
}
|
|
@@ -151,21 +150,21 @@
|
|
|
151
150
|
}
|
|
152
151
|
if (Array.isArray(value)) {
|
|
153
152
|
const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
|
|
154
|
-
// Empty
|
|
155
|
-
//
|
|
153
|
+
// Empty — never selected or fully pruned — falls back to the first option: a
|
|
154
|
+
// checklist that solves empty produces the same null cascade as a single value.
|
|
156
155
|
if (pruned.length !== value.length || value.length === 0) {
|
|
157
156
|
autoPickCount++;
|
|
158
157
|
onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
|
|
159
158
|
}
|
|
160
159
|
} else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
|
|
161
|
-
//
|
|
160
|
+
// Same fallback for a single value: never-selected and stale both resolve, never clear.
|
|
162
161
|
autoPickCount++;
|
|
163
162
|
onChange(item.paramId, firstOption, true);
|
|
164
163
|
}
|
|
165
164
|
});
|
|
166
165
|
|
|
167
166
|
function commit(newValue: SupportedTypes) {
|
|
168
|
-
// A real user pick re-arms the
|
|
167
|
+
// A real user pick re-arms the auto-pick budget.
|
|
169
168
|
autoPickCount = 0;
|
|
170
169
|
value = newValue;
|
|
171
170
|
onChange(item.paramId, newValue);
|
|
@@ -4,9 +4,9 @@ interface Props {
|
|
|
4
4
|
value?: SupportedTypes;
|
|
5
5
|
displayName?: string;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* `forceSolve` solves even in manual-solve mode. Used for system-initiated changes
|
|
8
|
+
* (e.g. pruning a vanished dynamic-list selection), where leaving the previous output
|
|
9
|
+
* on screen would misrepresent the input that now sits above it.
|
|
10
10
|
*/
|
|
11
11
|
onChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
12
12
|
disabled?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { OutputLayoutItem } from '@selvajs/schemas';
|
|
3
|
-
import type
|
|
3
|
+
import { subFolderSegments, type FileData } from '@selvajs/compute/core';
|
|
4
4
|
import ChartOutput from './ChartOutput.svelte';
|
|
5
5
|
import ImageOutput from './ImageOutput.svelte';
|
|
6
6
|
import {
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
let { item, value, displayName }: Props = $props();
|
|
25
25
|
|
|
26
|
-
// --- text/number state ---
|
|
27
26
|
let copied = $state(false);
|
|
28
27
|
let copyTimeout: ReturnType<typeof setTimeout>;
|
|
29
28
|
|
|
@@ -47,7 +46,6 @@
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
// --- file state ---
|
|
51
49
|
let downloading = $state(false);
|
|
52
50
|
let downloadError = $state<string | null>(null);
|
|
53
51
|
|
|
@@ -57,7 +55,6 @@
|
|
|
57
55
|
const fileCount = $derived(filesArray.length);
|
|
58
56
|
const totalSize = $derived(filesArray.reduce((sum, f) => sum + getBase64FileSize(f.data), 0));
|
|
59
57
|
|
|
60
|
-
// --- folder tree ---
|
|
61
58
|
type TreeNode =
|
|
62
59
|
| { type: 'file'; file: FileData }
|
|
63
60
|
| { type: 'folder'; name: string; children: SvelteMap<string, TreeNode> };
|
|
@@ -66,7 +63,7 @@
|
|
|
66
63
|
const root = new SvelteMap<string, TreeNode>();
|
|
67
64
|
for (let i = 0; i < files.length; i++) {
|
|
68
65
|
const file = files[i];
|
|
69
|
-
const parts = (file.subFolder
|
|
66
|
+
const parts = subFolderSegments(file.subFolder);
|
|
70
67
|
let current = root;
|
|
71
68
|
for (const part of parts) {
|
|
72
69
|
if (!current.has(part)) {
|
|
@@ -98,7 +95,7 @@
|
|
|
98
95
|
const fileTree = $derived(hasSubFolders ? buildTree(filesArray) : null);
|
|
99
96
|
|
|
100
97
|
function fullPath(f: FileData): string {
|
|
101
|
-
const folder = (f.subFolder
|
|
98
|
+
const folder = subFolderSegments(f.subFolder).join('/');
|
|
102
99
|
const name = `${f.fileName}${f.fileType ?? ''}`;
|
|
103
100
|
return folder ? `${folder}/${name}` : name;
|
|
104
101
|
}
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
// Computed value list options keyed by the target input id, derived from solved outputs.
|
|
37
37
|
const dynamicOptions = $derived(buildDynamicValueListOptions(schema, values));
|
|
38
38
|
|
|
39
|
-
// Tab selection
|
|
40
39
|
$effect(() => {
|
|
41
40
|
if (requestedTabId && visibleTabs.some((t) => t.id === requestedTabId)) {
|
|
42
41
|
activeTabId = requestedTabId;
|
|
@@ -45,7 +44,7 @@
|
|
|
45
44
|
}
|
|
46
45
|
});
|
|
47
46
|
|
|
48
|
-
//
|
|
47
|
+
// Seed each group's collapsed state once; re-seeding would discard the user's own toggles.
|
|
49
48
|
$effect(() => {
|
|
50
49
|
if (schema.layout.type !== 'tabbed') return;
|
|
51
50
|
const initial: Record<string, boolean> = {};
|
|
@@ -57,7 +56,8 @@
|
|
|
57
56
|
if (Object.keys(initial).length > 0) Object.assign(collapsedGroups, initial);
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
//
|
|
59
|
+
// A hidden or disabled input still solves, so reset it to its default — otherwise the
|
|
60
|
+
// value the user last set while it was visible keeps feeding the definition.
|
|
61
61
|
$effect(() => {
|
|
62
62
|
if (schema.layout.type !== 'tabbed') return;
|
|
63
63
|
const updates: Record<string, unknown> = {};
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
onChange
|
|
22
22
|
}: Props = $props();
|
|
23
23
|
|
|
24
|
-
// Resolve which modes are actually available
|
|
25
24
|
let effectiveModes = $derived(
|
|
26
25
|
allowedInputModes && allowedInputModes.length > 0
|
|
27
26
|
? allowedInputModes
|
|
@@ -29,7 +28,6 @@
|
|
|
29
28
|
);
|
|
30
29
|
let showToggle = $derived(effectiveModes.length > 1);
|
|
31
30
|
|
|
32
|
-
// Active mode: default to defaultInputMode if allowed, otherwise first allowed mode
|
|
33
31
|
let activeMode = $state<'upload' | 'url'>('upload');
|
|
34
32
|
$effect(() => {
|
|
35
33
|
const preferred = defaultInputMode ?? 'upload';
|
|
@@ -44,7 +42,7 @@
|
|
|
44
42
|
let urlError = $state<{ message: string; isCors: boolean; hint?: string } | null>(null);
|
|
45
43
|
let urlSuccess = $state('');
|
|
46
44
|
|
|
47
|
-
//
|
|
45
|
+
// The committed value is a JSON envelope; rebuild the display state from it.
|
|
48
46
|
$effect(() => {
|
|
49
47
|
if (value) {
|
|
50
48
|
try {
|
|
@@ -79,8 +77,7 @@
|
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
function isCorsError(error: unknown): boolean {
|
|
82
|
-
//
|
|
83
|
-
// because the browser blocks the response entirely
|
|
80
|
+
// The browser blocks the response entirely, so this is all we get — no status code.
|
|
84
81
|
if (error instanceof TypeError && error.message === 'Failed to fetch') return true;
|
|
85
82
|
return false;
|
|
86
83
|
}
|
|
@@ -158,9 +155,8 @@
|
|
|
158
155
|
return '';
|
|
159
156
|
}
|
|
160
157
|
})();
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// Give a message that covers both cases.
|
|
158
|
+
// The opaque TypeError hides CORS blocks and plain network failures (404, DNS)
|
|
159
|
+
// alike, so the message has to cover both.
|
|
164
160
|
let hint =
|
|
165
161
|
'Check that the URL is correct and the file is publicly accessible. If the server requires login, download the file and use the Upload option instead.';
|
|
166
162
|
if (host.includes('sharepoint.com') || host.includes('onedrive.com')) {
|
|
@@ -201,10 +197,11 @@
|
|
|
201
197
|
return;
|
|
202
198
|
}
|
|
203
199
|
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
200
|
+
// The file is base64-embedded into the compute request body, so without a
|
|
201
|
+
// client-side cap an oversize file comes back as an opaque 413. This cap is
|
|
202
|
+
// currently LOOSER than the server's COMPUTE_REQUEST_MAX_BYTES (see the TEMP
|
|
203
|
+
// note in constants.ts) — some files pass here and still 413. The URL import
|
|
204
|
+
// path applies the same cap.
|
|
208
205
|
if (file.size > APP_DEFAULTS.FILE_UPLOAD.MAX_SIZE_BYTES) {
|
|
209
206
|
alert(
|
|
210
207
|
`File too large: ${(file.size / 1024 / 1024).toFixed(2)}MB (max ${APP_DEFAULTS.FILE_UPLOAD.MAX_SIZE_MB}MB).`
|
|
@@ -15,13 +15,11 @@
|
|
|
15
15
|
} = $props();
|
|
16
16
|
|
|
17
17
|
const hasContent = $derived.by(() => {
|
|
18
|
-
// has slotted error
|
|
19
18
|
if (children) return true;
|
|
20
19
|
|
|
21
|
-
// no errors
|
|
22
20
|
if (!errors || errors.length === 0) return false;
|
|
23
21
|
|
|
24
|
-
//
|
|
22
|
+
// A lone error with no message renders an empty alert box — suppress it.
|
|
25
23
|
if (errors.length === 1 && !errors[0]?.message) {
|
|
26
24
|
return false;
|
|
27
25
|
}
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
16
|
/**
|
|
17
|
-
* Owned by `Viewer.svelte`,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Owned by `Viewer.svelte`, never built here: this panel unmounts when it closes, and hidden
|
|
18
|
+
* objects must stay hidden — and be re-hidden after each solve — while it is. Backed by
|
|
19
|
+
* SvelteSets, so mutating one re-renders this list.
|
|
20
20
|
*/
|
|
21
21
|
outliner: SceneOutliner;
|
|
22
22
|
sceneVersion?: number;
|
|
@@ -24,24 +24,20 @@
|
|
|
24
24
|
|
|
25
25
|
let { outliner, sceneVersion = 0 }: Props = $props();
|
|
26
26
|
|
|
27
|
-
// All list logic (content filtering, layer grouping, search, visibility, selection) lives in
|
|
28
|
-
// @selvajs/visualization/scene. This component only renders it and forwards clicks.
|
|
29
27
|
// Derived, not destructured: the prop is reassignable, and these must follow it.
|
|
30
28
|
const hidden = $derived(outliner.visibility.hidden);
|
|
31
29
|
const selected = $derived(outliner.selection.selected);
|
|
32
30
|
const collapsed = $derived(outliner.collapsed);
|
|
33
31
|
|
|
34
|
-
// Mirrored into runes because the outliner holds
|
|
35
|
-
//
|
|
36
|
-
// them, while hiding and collapse persist in the outliner that outlives it.
|
|
32
|
+
// Mirrored into runes because the outliner holds these as plain fields, not sets. Both are panel
|
|
33
|
+
// state, so reopening clears them — unlike hiding and collapse, which the outliner outlives.
|
|
37
34
|
let searchQuery = $state('');
|
|
38
35
|
let anchor = $state<string | null>(null);
|
|
39
36
|
|
|
40
37
|
$effect(() => outliner.onAnchorChange((next) => (anchor = next)));
|
|
41
38
|
|
|
42
39
|
// `scene.children` is a plain array the render layer mutates in place, so a solve bumping
|
|
43
|
-
// `sceneVersion` is the only signal that content changed
|
|
44
|
-
// is what makes one walk per solve serve every template site that needs the list.
|
|
40
|
+
// `sceneVersion` is the only signal that content changed — hence the `void` reads below.
|
|
45
41
|
const sceneObjects = $derived.by(() => {
|
|
46
42
|
void sceneVersion;
|
|
47
43
|
return outliner.objects();
|
|
@@ -54,12 +50,9 @@
|
|
|
54
50
|
return outliner.layerGroups();
|
|
55
51
|
});
|
|
56
52
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// re-renders this row
|
|
60
|
-
// `SvelteSet.has()` is itself the reactive read, so go through the set rather than calling
|
|
61
|
-
// `visibility.isHidden` — that reads the set through a plain reference inside the outliner and
|
|
62
|
-
// would not re-render this row.
|
|
53
|
+
// `SvelteSet.has()` is the reactive read, so go through the set rather than calling
|
|
54
|
+
// `visibility.isHidden` — that reaches the set through a plain reference inside the outliner
|
|
55
|
+
// and returns a correct value that never re-renders this row.
|
|
63
56
|
const isObjectHidden = (object: THREE.Object3D) => hidden.has(getTrackingKey(object));
|
|
64
57
|
|
|
65
58
|
// Same reason: count through the reactive set so the layer's tri-state eye tracks its objects.
|
|
@@ -165,7 +158,6 @@
|
|
|
165
158
|
onkeydown={(e) =>
|
|
166
159
|
e.key === 'Enter' && selectObject(object.uuid, e as unknown as MouseEvent)}
|
|
167
160
|
>
|
|
168
|
-
<!-- Object visibility toggle -->
|
|
169
161
|
<button
|
|
170
162
|
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
171
163
|
onclick={(e) => {
|
|
@@ -182,7 +174,6 @@
|
|
|
182
174
|
{/if}
|
|
183
175
|
</button>
|
|
184
176
|
|
|
185
|
-
<!-- Object name -->
|
|
186
177
|
<span
|
|
187
178
|
class="min-w-0 text-xs flex-1 truncate {isHidden
|
|
188
179
|
? 'text-muted-foreground line-through'
|
|
@@ -191,7 +182,6 @@
|
|
|
191
182
|
{getObjectLabel(object)}
|
|
192
183
|
</span>
|
|
193
184
|
|
|
194
|
-
<!-- Type badge -->
|
|
195
185
|
<span
|
|
196
186
|
class="rounded px-1 py-0.5 font-medium shrink-0 bg-muted text-[9px] text-muted-foreground/70"
|
|
197
187
|
>
|
|
@@ -203,7 +193,6 @@
|
|
|
203
193
|
{/if}
|
|
204
194
|
{/each}
|
|
205
195
|
|
|
206
|
-
<!-- Empty state -->
|
|
207
196
|
{#if sceneObjects.length === 0}
|
|
208
197
|
<div class="py-12 flex flex-col items-center justify-center text-center">
|
|
209
198
|
<EyeOff class="mb-2 h-5 w-5 text-muted-foreground/30" />
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type SceneOutliner } from '@selvajs/visualization/scene';
|
|
2
2
|
interface Props {
|
|
3
3
|
/**
|
|
4
|
-
* Owned by `Viewer.svelte`,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Owned by `Viewer.svelte`, never built here: this panel unmounts when it closes, and hidden
|
|
5
|
+
* objects must stay hidden — and be re-hidden after each solve — while it is. Backed by
|
|
6
|
+
* SvelteSets, so mutating one re-renders this list.
|
|
7
7
|
*/
|
|
8
8
|
outliner: SceneOutliner;
|
|
9
9
|
sceneVersion?: number;
|