@selvajs/ui 5.0.0 → 6.0.0-beta.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/compute/ComputeApp.svelte +15 -7
- package/dist/components/compute/ComputeApp.svelte.d.ts +1 -1
- package/dist/components/primitives/alert/alert.svelte.d.ts +8 -8
- package/dist/components/primitives/badge/badge.svelte.d.ts +14 -14
- package/dist/components/primitives/button/button.svelte.d.ts +41 -41
- package/dist/components/primitives/button-group/button-group.svelte.d.ts +8 -8
- package/dist/components/primitives/field/field.svelte.d.ts +5 -5
- package/dist/components/primitives/textarea/textarea.svelte +1 -2
- package/dist/components/viewer/SceneManager.svelte +75 -160
- package/dist/components/viewer/SceneManager.svelte.d.ts +8 -3
- package/dist/components/viewer/Viewer.svelte +20 -3
- package/dist/compute/useSolveSession.svelte.d.ts +12 -0
- package/dist/compute/useSolveSession.svelte.js +76 -0
- package/dist/external/storage.d.ts +1 -15
- package/dist/external/storage.js +4 -54
- package/dist/index.d.ts +4 -3
- package/dist/index.js +7 -4
- package/dist/public.d.ts +4 -3
- package/dist/public.js +9 -5
- package/package.json +17 -15
- package/src/lib/components/compute/ComputeApp.svelte +15 -7
- package/src/lib/components/primitives/textarea/textarea.svelte +1 -2
- package/src/lib/components/viewer/SceneManager.svelte +75 -160
- package/src/lib/components/viewer/Viewer.svelte +20 -3
- package/src/lib/compute/mesh-policy-wiring.test.ts +72 -0
- package/src/lib/compute/useSolveSession.svelte.ts +83 -0
- package/src/lib/external/storage.ts +12 -64
- package/src/lib/index.ts +15 -5
- package/src/lib/public.ts +17 -6
- package/dist/compute/computeThrottle.svelte.d.ts +0 -24
- package/dist/compute/computeThrottle.svelte.js +0 -82
- package/dist/compute/createSolveSession.svelte.d.ts +0 -66
- package/dist/compute/createSolveSession.svelte.js +0 -159
- package/dist/compute/solve-session-core.d.ts +0 -54
- package/dist/compute/solve-session-core.js +0 -87
- package/dist/compute/solveMemo.d.ts +0 -22
- package/dist/compute/solveMemo.js +0 -124
- package/dist/types/solveFn.d.ts +0 -25
- package/dist/types/solveFn.js +0 -1
- package/src/lib/compute/computeThrottle.svelte.ts +0 -109
- package/src/lib/compute/computeThrottle.test.ts +0 -106
- package/src/lib/compute/createSolveSession.svelte.ts +0 -239
- package/src/lib/compute/createSolveSession.test.ts +0 -168
- package/src/lib/compute/solve-session-core.test.ts +0 -153
- package/src/lib/compute/solve-session-core.ts +0 -122
- package/src/lib/compute/solveMemo.test.ts +0 -220
- package/src/lib/compute/solveMemo.ts +0 -140
- package/src/lib/external/storage.test.ts +0 -99
- package/src/lib/types/solveFn.ts +0 -29
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
import { page } from '$app/state';
|
|
4
4
|
import type { UISchema, ParameterPreset } from '@selvajs/schemas';
|
|
5
5
|
import type { ActionButton } from '../../types/actionButton';
|
|
6
|
-
import type { SolveFn } from '
|
|
6
|
+
import type { SolveFn } from '@selvajs/solve/shared';
|
|
7
7
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
8
8
|
import { createSolvingIndicator } from '../../compute/solving.svelte';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from '../../compute/createSolveSession.svelte';
|
|
9
|
+
import { createRequestResponseDriver } from '@selvajs/solve/client';
|
|
10
|
+
import { meshPolicy } from '@selvajs/visualization/parse';
|
|
11
|
+
import { useSolveSession } from '../../compute/useSolveSession.svelte';
|
|
13
12
|
import { useFooterItem } from '../../composables/useFooterItem.svelte';
|
|
14
13
|
import { hexToOklch } from '../../utils/color';
|
|
15
14
|
import AppShell from '../layout/AppShell.svelte';
|
|
@@ -115,10 +114,19 @@
|
|
|
115
114
|
// reads the reporter lazily so it can capture the session it's wired into.
|
|
116
115
|
// svelte-ignore state_referenced_locally
|
|
117
116
|
const driver = createRequestResponseDriver(onSolve, () => session, {
|
|
118
|
-
timeout: solveTimeoutMs
|
|
117
|
+
timeout: solveTimeoutMs,
|
|
118
|
+
// The driver's result memo caches whole solve results, meshes included — and the viewer
|
|
119
|
+
// disposes what it renders on the next scene update. `@selvajs/solve` keeps meshes opaque,
|
|
120
|
+
// so the three.js clone/dispose rules are injected from the renderer that owns them
|
|
121
|
+
// (audit C1). Without this a memo hit serves an already-disposed mesh.
|
|
122
|
+
meshPolicy,
|
|
123
|
+
// `session.isSolving` forwards to the driver, which the session can't observe on its
|
|
124
|
+
// own — republish so the spinner and disabled states track it. Deferred into a
|
|
125
|
+
// callback, so it reads `session` after initialization rather than during it.
|
|
126
|
+
onChange: () => session.notify()
|
|
119
127
|
});
|
|
120
128
|
// svelte-ignore state_referenced_locally
|
|
121
|
-
const session =
|
|
129
|
+
const session = useSolveSession({
|
|
122
130
|
schema,
|
|
123
131
|
scopeKey: externalScopeKey || definitionKey || schema?.id || '',
|
|
124
132
|
driver
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { UISchema, ParameterPreset } from '@selvajs/schemas';
|
|
2
2
|
import type { ActionButton } from '../../types/actionButton';
|
|
3
|
-
import type { SolveFn } from '
|
|
3
|
+
import type { SolveFn } from '@selvajs/solve/shared';
|
|
4
4
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
5
5
|
import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
|
|
6
6
|
import type { Locale } from '../../i18n/messages';
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { type VariantProps } from 'tailwind-variants';
|
|
2
2
|
export declare const alertVariants: import("tailwind-variants").TVReturnType<{
|
|
3
3
|
variant: {
|
|
4
|
-
default:
|
|
5
|
-
destructive:
|
|
4
|
+
default: "bg-card text-card-foreground";
|
|
5
|
+
destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current";
|
|
6
6
|
};
|
|
7
7
|
}, undefined, "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", {
|
|
8
8
|
variant: {
|
|
9
|
-
default:
|
|
10
|
-
destructive:
|
|
9
|
+
default: "bg-card text-card-foreground";
|
|
10
|
+
destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current";
|
|
11
11
|
};
|
|
12
|
-
}, undefined, import("tailwind-variants").
|
|
12
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
13
13
|
variant: {
|
|
14
|
-
default:
|
|
15
|
-
destructive:
|
|
14
|
+
default: "bg-card text-card-foreground";
|
|
15
|
+
destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current";
|
|
16
16
|
};
|
|
17
|
-
}, undefined
|
|
17
|
+
}, undefined>>;
|
|
18
18
|
export type AlertVariant = VariantProps<typeof alertVariants>['variant'];
|
|
19
19
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
20
20
|
import { type WithElementRef } from '../../../utils.js';
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import { type VariantProps } from 'tailwind-variants';
|
|
2
2
|
export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
|
|
3
3
|
variant: {
|
|
4
|
-
default:
|
|
5
|
-
secondary:
|
|
6
|
-
destructive:
|
|
7
|
-
outline:
|
|
4
|
+
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent";
|
|
5
|
+
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent";
|
|
6
|
+
destructive: "bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white";
|
|
7
|
+
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground";
|
|
8
8
|
};
|
|
9
9
|
}, undefined, "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3", {
|
|
10
10
|
variant: {
|
|
11
|
-
default:
|
|
12
|
-
secondary:
|
|
13
|
-
destructive:
|
|
14
|
-
outline:
|
|
11
|
+
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent";
|
|
12
|
+
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent";
|
|
13
|
+
destructive: "bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white";
|
|
14
|
+
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground";
|
|
15
15
|
};
|
|
16
|
-
}, undefined, import("tailwind-variants").
|
|
16
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
17
17
|
variant: {
|
|
18
|
-
default:
|
|
19
|
-
secondary:
|
|
20
|
-
destructive:
|
|
21
|
-
outline:
|
|
18
|
+
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent";
|
|
19
|
+
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent";
|
|
20
|
+
destructive: "bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white";
|
|
21
|
+
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground";
|
|
22
22
|
};
|
|
23
|
-
}, undefined
|
|
23
|
+
}, undefined>>;
|
|
24
24
|
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
|
|
25
25
|
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
26
26
|
import { type WithElementRef } from '../../../utils.js';
|
|
@@ -3,59 +3,59 @@ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements
|
|
|
3
3
|
import { type VariantProps } from 'tailwind-variants';
|
|
4
4
|
export declare const buttonVariants: import("tailwind-variants").TVReturnType<{
|
|
5
5
|
variant: {
|
|
6
|
-
default:
|
|
7
|
-
destructive:
|
|
8
|
-
outline:
|
|
9
|
-
secondary:
|
|
10
|
-
ghost:
|
|
11
|
-
dashed:
|
|
12
|
-
link:
|
|
6
|
+
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90";
|
|
7
|
+
destructive: "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white";
|
|
8
|
+
outline: "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border";
|
|
9
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80";
|
|
10
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50";
|
|
11
|
+
dashed: "border border-dashed border-input bg-background text-muted-foreground hover:bg-muted hover:text-foreground";
|
|
12
|
+
link: "text-primary underline-offset-4 hover:underline";
|
|
13
13
|
};
|
|
14
14
|
size: {
|
|
15
|
-
default:
|
|
16
|
-
sm:
|
|
17
|
-
lg:
|
|
18
|
-
icon:
|
|
19
|
-
'icon-sm':
|
|
20
|
-
'icon-lg':
|
|
15
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3";
|
|
16
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5";
|
|
17
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4";
|
|
18
|
+
icon: "size-9";
|
|
19
|
+
'icon-sm': "size-8";
|
|
20
|
+
'icon-lg': "size-10";
|
|
21
21
|
};
|
|
22
22
|
}, undefined, "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", {
|
|
23
23
|
variant: {
|
|
24
|
-
default:
|
|
25
|
-
destructive:
|
|
26
|
-
outline:
|
|
27
|
-
secondary:
|
|
28
|
-
ghost:
|
|
29
|
-
dashed:
|
|
30
|
-
link:
|
|
24
|
+
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90";
|
|
25
|
+
destructive: "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white";
|
|
26
|
+
outline: "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border";
|
|
27
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80";
|
|
28
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50";
|
|
29
|
+
dashed: "border border-dashed border-input bg-background text-muted-foreground hover:bg-muted hover:text-foreground";
|
|
30
|
+
link: "text-primary underline-offset-4 hover:underline";
|
|
31
31
|
};
|
|
32
32
|
size: {
|
|
33
|
-
default:
|
|
34
|
-
sm:
|
|
35
|
-
lg:
|
|
36
|
-
icon:
|
|
37
|
-
'icon-sm':
|
|
38
|
-
'icon-lg':
|
|
33
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3";
|
|
34
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5";
|
|
35
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4";
|
|
36
|
+
icon: "size-9";
|
|
37
|
+
'icon-sm': "size-8";
|
|
38
|
+
'icon-lg': "size-10";
|
|
39
39
|
};
|
|
40
|
-
}, undefined, import("tailwind-variants").
|
|
40
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
41
41
|
variant: {
|
|
42
|
-
default:
|
|
43
|
-
destructive:
|
|
44
|
-
outline:
|
|
45
|
-
secondary:
|
|
46
|
-
ghost:
|
|
47
|
-
dashed:
|
|
48
|
-
link:
|
|
42
|
+
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90";
|
|
43
|
+
destructive: "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white";
|
|
44
|
+
outline: "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border";
|
|
45
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80";
|
|
46
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50";
|
|
47
|
+
dashed: "border border-dashed border-input bg-background text-muted-foreground hover:bg-muted hover:text-foreground";
|
|
48
|
+
link: "text-primary underline-offset-4 hover:underline";
|
|
49
49
|
};
|
|
50
50
|
size: {
|
|
51
|
-
default:
|
|
52
|
-
sm:
|
|
53
|
-
lg:
|
|
54
|
-
icon:
|
|
55
|
-
'icon-sm':
|
|
56
|
-
'icon-lg':
|
|
51
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3";
|
|
52
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5";
|
|
53
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4";
|
|
54
|
+
icon: "size-9";
|
|
55
|
+
'icon-sm': "size-8";
|
|
56
|
+
'icon-lg': "size-10";
|
|
57
57
|
};
|
|
58
|
-
}, undefined
|
|
58
|
+
}, undefined>>;
|
|
59
59
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
|
60
60
|
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
|
61
61
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> & WithElementRef<HTMLAnchorAttributes> & {
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { type VariantProps } from 'tailwind-variants';
|
|
2
2
|
export declare const buttonGroupVariants: import("tailwind-variants").TVReturnType<{
|
|
3
3
|
orientation: {
|
|
4
|
-
horizontal:
|
|
5
|
-
vertical:
|
|
4
|
+
horizontal: "[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none";
|
|
5
|
+
vertical: "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none";
|
|
6
6
|
};
|
|
7
7
|
}, undefined, "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", {
|
|
8
8
|
orientation: {
|
|
9
|
-
horizontal:
|
|
10
|
-
vertical:
|
|
9
|
+
horizontal: "[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none";
|
|
10
|
+
vertical: "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none";
|
|
11
11
|
};
|
|
12
|
-
}, undefined, import("tailwind-variants").
|
|
12
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
13
13
|
orientation: {
|
|
14
|
-
horizontal:
|
|
15
|
-
vertical:
|
|
14
|
+
horizontal: "[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none";
|
|
15
|
+
vertical: "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none";
|
|
16
16
|
};
|
|
17
|
-
}, undefined
|
|
17
|
+
}, undefined>>;
|
|
18
18
|
export type ButtonGroupOrientation = VariantProps<typeof buttonGroupVariants>['orientation'];
|
|
19
19
|
import { type WithElementRef } from '../../../utils.js';
|
|
20
20
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { type VariantProps } from 'tailwind-variants';
|
|
2
2
|
export declare const fieldVariants: import("tailwind-variants").TVReturnType<{
|
|
3
3
|
orientation: {
|
|
4
|
-
vertical:
|
|
4
|
+
vertical: "flex-col [&>*]:w-full [&>.sr-only]:w-auto";
|
|
5
5
|
horizontal: string[];
|
|
6
6
|
responsive: string[];
|
|
7
7
|
};
|
|
8
8
|
}, undefined, "group/field data-[invalid=true]:text-destructive flex w-full gap-3", {
|
|
9
9
|
orientation: {
|
|
10
|
-
vertical:
|
|
10
|
+
vertical: "flex-col [&>*]:w-full [&>.sr-only]:w-auto";
|
|
11
11
|
horizontal: string[];
|
|
12
12
|
responsive: string[];
|
|
13
13
|
};
|
|
14
|
-
}, undefined, import("tailwind-variants").
|
|
14
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
15
15
|
orientation: {
|
|
16
|
-
vertical:
|
|
16
|
+
vertical: "flex-col [&>*]:w-full [&>.sr-only]:w-auto";
|
|
17
17
|
horizontal: string[];
|
|
18
18
|
responsive: string[];
|
|
19
19
|
};
|
|
20
|
-
}, undefined
|
|
20
|
+
}, undefined>>;
|
|
21
21
|
export type FieldOrientation = VariantProps<typeof fieldVariants>['orientation'];
|
|
22
22
|
import { type WithElementRef } from '../../../utils.js';
|
|
23
23
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
@@ -1,173 +1,85 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import { Eye, EyeOff, ChevronRight, Search, X } from '@lucide/svelte';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getObjectLabel,
|
|
6
|
+
getTrackingKey,
|
|
7
|
+
getTypeLabel,
|
|
8
|
+
type SceneOutliner
|
|
9
|
+
} from '@selvajs/visualization/scene';
|
|
5
10
|
import { getLocaleContext } from '../../i18n/localeContext.svelte';
|
|
6
11
|
|
|
7
12
|
const locale = getLocaleContext();
|
|
8
13
|
const t = $derived(locale.messages);
|
|
9
14
|
|
|
10
15
|
interface Props {
|
|
11
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Owned by `Viewer.svelte`, not built here: this panel unmounts when it is closed, and
|
|
18
|
+
* hidden objects have to stay hidden — and keep being re-hidden after each solve — while it
|
|
19
|
+
* is. Its state is backed by SvelteSets, so mutating one re-renders this list.
|
|
20
|
+
*/
|
|
21
|
+
outliner: SceneOutliner;
|
|
12
22
|
sceneVersion?: number;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
let {
|
|
25
|
+
let { outliner, sceneVersion = 0 }: Props = $props();
|
|
16
26
|
|
|
17
|
-
//
|
|
18
|
-
// @selvajs/
|
|
19
|
-
|
|
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
|
+
// Derived, not destructured: the prop is reassignable, and these must follow it.
|
|
30
|
+
const hidden = $derived(outliner.visibility.hidden);
|
|
31
|
+
const selected = $derived(outliner.selection.selected);
|
|
32
|
+
const collapsed = $derived(outliner.collapsed);
|
|
20
33
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
34
|
+
// Mirrored into runes because the outliner holds them as plain fields, not sets. Both start
|
|
35
|
+
// fresh: the search box and the shift-anchor are panel state, so reopening the panel clears
|
|
36
|
+
// them, while hiding and collapse persist in the outliner that outlives it.
|
|
37
|
+
let searchQuery = $state('');
|
|
38
|
+
let anchor = $state<string | null>(null);
|
|
39
|
+
|
|
40
|
+
$effect(() => outliner.onAnchorChange((next) => (anchor = next)));
|
|
41
|
+
|
|
42
|
+
// `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. Both derivations below read it, which
|
|
44
|
+
// is what makes one walk per solve serve every template site that needs the list.
|
|
25
45
|
const sceneObjects = $derived.by(() => {
|
|
26
46
|
void sceneVersion;
|
|
27
|
-
return
|
|
28
|
-
(obj) =>
|
|
29
|
-
!(obj instanceof THREE.Camera) &&
|
|
30
|
-
!(obj instanceof THREE.Light) &&
|
|
31
|
-
!HELPER_IDS.has(obj.userData?.id)
|
|
32
|
-
);
|
|
47
|
+
return outliner.objects();
|
|
33
48
|
});
|
|
34
49
|
|
|
35
|
-
// Content grouped by layer. Derived from `sceneObjects`, so the grouping map is rebuilt once per
|
|
36
|
-
// solve rather than every time the list renders.
|
|
37
50
|
const layerGroups = $derived.by(() => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
groups.get(layer)!.push(obj);
|
|
43
|
-
}
|
|
44
|
-
return groups;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
let hiddenUuids = new SvelteSet<string>();
|
|
48
|
-
|
|
49
|
-
let collapsedLayers = new SvelteSet<string>();
|
|
50
|
-
|
|
51
|
-
const setObjectVisible = (object: THREE.Object3D, visible: boolean) => {
|
|
52
|
-
object.visible = visible;
|
|
53
|
-
object.traverse((child) => {
|
|
54
|
-
child.visible = visible;
|
|
55
|
-
});
|
|
56
|
-
if (visible) {
|
|
57
|
-
hiddenUuids.delete(object.uuid);
|
|
58
|
-
} else {
|
|
59
|
-
hiddenUuids.add(object.uuid);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const toggleObject = (object: THREE.Object3D) => {
|
|
64
|
-
if (selectedUuids.has(object.uuid) && selectedUuids.size > 1) {
|
|
65
|
-
const selected = sceneObjects.filter((o) => selectedUuids.has(o.uuid));
|
|
66
|
-
const allHidden = selected.every((o) => hiddenUuids.has(o.uuid));
|
|
67
|
-
for (const o of selected) setObjectVisible(o, allHidden);
|
|
68
|
-
} else {
|
|
69
|
-
setObjectVisible(object, hiddenUuids.has(object.uuid));
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const isLayerHidden = (objects: THREE.Object3D[]) =>
|
|
74
|
-
objects.every((obj) => hiddenUuids.has(obj.uuid));
|
|
75
|
-
|
|
76
|
-
const isLayerPartial = (objects: THREE.Object3D[]) => {
|
|
77
|
-
const hidden = objects.filter((obj) => hiddenUuids.has(obj.uuid)).length;
|
|
78
|
-
return hidden > 0 && hidden < objects.length;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const toggleLayer = (_layerName: string, objects: THREE.Object3D[]) => {
|
|
82
|
-
const allHidden = isLayerHidden(objects);
|
|
83
|
-
for (const obj of objects) {
|
|
84
|
-
setObjectVisible(obj, allHidden);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const toggleLayerCollapsed = (layerName: string) => {
|
|
89
|
-
if (collapsedLayers.has(layerName)) {
|
|
90
|
-
collapsedLayers.delete(layerName);
|
|
91
|
-
} else {
|
|
92
|
-
collapsedLayers.add(layerName);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const prettyType = (type: string) =>
|
|
97
|
-
type
|
|
98
|
-
.replace(/^Line(Segments)?2$/, 'Curve')
|
|
99
|
-
.replace('Mesh', '')
|
|
100
|
-
.replace('Object3D', 'Obj') || type;
|
|
101
|
-
|
|
102
|
-
const getObjectLabel = (object: THREE.Object3D) =>
|
|
103
|
-
object.userData?.name || object.userData?.fileName || object.name || prettyType(object.type);
|
|
104
|
-
|
|
105
|
-
const getTypeLabel = (object: THREE.Object3D) => prettyType(object.type);
|
|
106
|
-
|
|
107
|
-
let searchQuery = $state('');
|
|
108
|
-
|
|
109
|
-
// Layer groups after the search filter. Derived from `layerGroups` + `searchQuery`, so filtering
|
|
110
|
-
// runs once per search keystroke — not twice per render (it's read by both the list and the
|
|
111
|
-
// empty-state check below).
|
|
112
|
-
const filteredLayerGroups = $derived.by(() => {
|
|
113
|
-
if (!searchQuery.trim()) return layerGroups;
|
|
114
|
-
const q = searchQuery.toLowerCase();
|
|
115
|
-
const filtered = new SvelteMap<string, THREE.Object3D[]>();
|
|
116
|
-
for (const [layerName, objects] of layerGroups) {
|
|
117
|
-
const matchingObjects = layerName.toLowerCase().includes(q)
|
|
118
|
-
? objects
|
|
119
|
-
: objects.filter((obj) => getObjectLabel(obj).toLowerCase().includes(q));
|
|
120
|
-
if (matchingObjects.length > 0) filtered.set(layerName, matchingObjects);
|
|
121
|
-
}
|
|
122
|
-
return filtered;
|
|
51
|
+
void sceneVersion;
|
|
52
|
+
void searchQuery;
|
|
53
|
+
outliner.searchQuery = searchQuery;
|
|
54
|
+
return outliner.layerGroups();
|
|
123
55
|
});
|
|
124
56
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return result;
|
|
133
|
-
};
|
|
57
|
+
// Hidden state is keyed by Grasshopper identity, not by uuid, so ask the outliner rather than
|
|
58
|
+
// looking the object up in the set directly. Touching `hidden` registers the reactive read that
|
|
59
|
+
// re-renders this row when the eye is clicked.
|
|
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.
|
|
63
|
+
const isObjectHidden = (object: THREE.Object3D) => hidden.has(getTrackingKey(object));
|
|
134
64
|
|
|
135
|
-
|
|
136
|
-
|
|
65
|
+
// Same reason: count through the reactive set so the layer's tri-state eye tracks its objects.
|
|
66
|
+
const hiddenCount = (objects: THREE.Object3D[]) =>
|
|
67
|
+
objects.filter((object) => isObjectHidden(object)).length;
|
|
137
68
|
|
|
69
|
+
// Reading `anchor` keeps the shift-range dependent on it; the outliner owns the value.
|
|
138
70
|
const selectObject = (uuid: string, event: MouseEvent) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const [lo, hi] = a < b ? [a, b] : [b, a];
|
|
145
|
-
if (!event.ctrlKey && !event.metaKey) selectedUuids.clear();
|
|
146
|
-
for (let i = lo; i <= hi; i++) selectedUuids.add(flat[i]);
|
|
147
|
-
}
|
|
148
|
-
} else if (event.ctrlKey || event.metaKey) {
|
|
149
|
-
if (selectedUuids.has(uuid)) {
|
|
150
|
-
selectedUuids.delete(uuid);
|
|
151
|
-
} else {
|
|
152
|
-
selectedUuids.add(uuid);
|
|
153
|
-
lastSelectedUuid = uuid;
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
selectedUuids.clear();
|
|
157
|
-
selectedUuids.add(uuid);
|
|
158
|
-
lastSelectedUuid = uuid;
|
|
159
|
-
}
|
|
160
|
-
if (!event.shiftKey) lastSelectedUuid = uuid;
|
|
71
|
+
void anchor;
|
|
72
|
+
outliner.select(uuid, {
|
|
73
|
+
shiftKey: event.shiftKey,
|
|
74
|
+
toggleKey: event.ctrlKey || event.metaKey
|
|
75
|
+
});
|
|
161
76
|
};
|
|
162
77
|
</script>
|
|
163
78
|
|
|
164
79
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
165
80
|
<div
|
|
166
81
|
class="flex h-full flex-col bg-card text-card-foreground"
|
|
167
|
-
onmousedown={() =>
|
|
168
|
-
selectedUuids.clear();
|
|
169
|
-
lastSelectedUuid = null;
|
|
170
|
-
}}
|
|
82
|
+
onmousedown={() => outliner.selection.clear()}
|
|
171
83
|
>
|
|
172
84
|
<div class="px-3 py-2 gap-2 flex items-center border-b border-border">
|
|
173
85
|
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
|
|
@@ -186,25 +98,26 @@
|
|
|
186
98
|
</div>
|
|
187
99
|
|
|
188
100
|
<div class="py-1 flex-1 overflow-y-auto">
|
|
189
|
-
{#each [...
|
|
190
|
-
{@const
|
|
191
|
-
{@const
|
|
192
|
-
{@const
|
|
101
|
+
{#each [...layerGroups] as [layerName, objects] (layerName)}
|
|
102
|
+
{@const numHidden = hiddenCount(objects)}
|
|
103
|
+
{@const layerHidden = objects.length > 0 && numHidden === objects.length}
|
|
104
|
+
{@const layerPartial = numHidden > 0 && numHidden < objects.length}
|
|
105
|
+
{@const isCollapsed = collapsed.has(layerName)}
|
|
193
106
|
|
|
194
107
|
<div class="gap-1 pl-1 pr-2 py-1 group flex items-center transition-colors hover:bg-muted">
|
|
195
108
|
<button
|
|
196
109
|
class="rounded p-0.5 shrink-0 text-muted-foreground transition-colors hover:text-muted-foreground"
|
|
197
|
-
onclick={() =>
|
|
198
|
-
aria-label={
|
|
110
|
+
onclick={() => outliner.toggleCollapsed(layerName)}
|
|
111
|
+
aria-label={isCollapsed ? t.expandLayer : t.collapseLayer}
|
|
199
112
|
>
|
|
200
113
|
<ChevronRight
|
|
201
|
-
class="h-3.5 w-3.5 transition-transform duration-150 {
|
|
114
|
+
class="h-3.5 w-3.5 transition-transform duration-150 {isCollapsed ? '' : 'rotate-90'}"
|
|
202
115
|
/>
|
|
203
116
|
</button>
|
|
204
117
|
|
|
205
118
|
<button
|
|
206
119
|
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
207
|
-
onclick={() => toggleLayer(
|
|
120
|
+
onclick={() => outliner.visibility.toggleLayer(objects)}
|
|
208
121
|
title={layerHidden ? t.showLayer : t.hideLayer}
|
|
209
122
|
aria-label={layerHidden ? t.showLayer : t.hideLayer}
|
|
210
123
|
>
|
|
@@ -230,18 +143,20 @@
|
|
|
230
143
|
</span>
|
|
231
144
|
</div>
|
|
232
145
|
|
|
233
|
-
{#if !
|
|
146
|
+
{#if !isCollapsed}
|
|
234
147
|
<div role="listbox" aria-multiselectable="true" class="ml-3 border-l border-border">
|
|
235
148
|
{#each objects as object (object.uuid)}
|
|
236
|
-
|
|
237
|
-
|
|
149
|
+
<!-- Visibility is keyed by Grasshopper identity (so it survives a solve), selection
|
|
150
|
+
by uuid (so it does not) — hence the two different lookups. -->
|
|
151
|
+
{@const isHidden = isObjectHidden(object)}
|
|
152
|
+
{@const isSelected = selected.has(object.uuid)}
|
|
238
153
|
<div
|
|
239
154
|
role="option"
|
|
240
|
-
aria-selected={
|
|
155
|
+
aria-selected={isSelected}
|
|
241
156
|
tabindex="-1"
|
|
242
157
|
class="gap-1.5 pl-5 pr-2 py-0.5 flex cursor-pointer items-center transition-colors
|
|
243
|
-
{
|
|
244
|
-
{
|
|
158
|
+
{isSelected ? 'bg-primary/10 hover:bg-primary/15' : 'hover:bg-muted'}
|
|
159
|
+
{isHidden ? 'opacity-40' : ''}"
|
|
245
160
|
onmousedown={(e) => {
|
|
246
161
|
e.stopPropagation();
|
|
247
162
|
if (e.shiftKey) e.preventDefault();
|
|
@@ -255,12 +170,12 @@
|
|
|
255
170
|
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
256
171
|
onclick={(e) => {
|
|
257
172
|
e.stopPropagation();
|
|
258
|
-
toggleObject(object);
|
|
173
|
+
outliner.toggleObject(object);
|
|
259
174
|
}}
|
|
260
|
-
title={
|
|
261
|
-
aria-label={
|
|
175
|
+
title={isHidden ? t.showObject : t.hideObject}
|
|
176
|
+
aria-label={isHidden ? t.showObject : t.hideObject}
|
|
262
177
|
>
|
|
263
|
-
{#if
|
|
178
|
+
{#if isHidden}
|
|
264
179
|
<EyeOff class="h-3 w-3 text-muted-foreground/60" />
|
|
265
180
|
{:else}
|
|
266
181
|
<Eye class="h-3 w-3 text-muted-foreground" />
|
|
@@ -269,7 +184,7 @@
|
|
|
269
184
|
|
|
270
185
|
<!-- Object name -->
|
|
271
186
|
<span
|
|
272
|
-
class="min-w-0 text-xs flex-1 truncate {
|
|
187
|
+
class="min-w-0 text-xs flex-1 truncate {isHidden
|
|
273
188
|
? 'text-muted-foreground line-through'
|
|
274
189
|
: 'text-foreground/80'}"
|
|
275
190
|
>
|
|
@@ -294,7 +209,7 @@
|
|
|
294
209
|
<EyeOff class="mb-2 h-5 w-5 text-muted-foreground/30" />
|
|
295
210
|
<p class="text-xs text-muted-foreground">{t.noObjects}</p>
|
|
296
211
|
</div>
|
|
297
|
-
{:else if
|
|
212
|
+
{:else if layerGroups.size === 0}
|
|
298
213
|
<div class="py-12 flex flex-col items-center justify-center text-center">
|
|
299
214
|
<Search class="mb-2 h-5 w-5 text-muted-foreground/30" />
|
|
300
215
|
<p class="text-xs text-muted-foreground">
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type SceneOutliner } from '@selvajs/visualization/scene';
|
|
2
2
|
interface Props {
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Owned by `Viewer.svelte`, not built here: this panel unmounts when it is closed, and
|
|
5
|
+
* hidden objects have to stay hidden — and keep being re-hidden after each solve — while it
|
|
6
|
+
* is. Its state is backed by SvelteSets, so mutating one re-renders this list.
|
|
7
|
+
*/
|
|
8
|
+
outliner: SceneOutliner;
|
|
4
9
|
sceneVersion?: number;
|
|
5
10
|
}
|
|
6
|
-
declare const SceneManager: import("svelte").Component<Props, {}, "
|
|
11
|
+
declare const SceneManager: import("svelte").Component<Props, {}, "">;
|
|
7
12
|
type SceneManager = ReturnType<typeof SceneManager>;
|
|
8
13
|
export default SceneManager;
|