@selvajs/ui 6.6.0-beta.8 → 6.6.0-beta.9
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/AppLayout.svelte +37 -7
- package/dist/components/compute/AppLayout.svelte.d.ts +5 -0
- package/dist/components/compute/ParameterPresetManager.svelte +8 -20
- package/dist/components/compute/SolveMessages.svelte +26 -3
- package/dist/components/compute/SolveMessages.svelte.d.ts +7 -0
- package/dist/components/layout/PageFooter.svelte +3 -1
- package/dist/components/layout/SideNav.svelte +55 -14
- package/dist/components/preview/ChartOutput.svelte +18 -13
- package/dist/components/preview/Group.svelte +6 -1
- package/dist/components/preview/ImageOutput.svelte +3 -1
- package/dist/components/preview/InputControl.svelte +108 -44
- package/dist/components/preview/InputControl.svelte.d.ts +5 -1
- package/dist/components/preview/TabContent.svelte +61 -29
- package/dist/components/preview/TabContent.svelte.d.ts +5 -1
- package/dist/components/preview/TabLayout.svelte +25 -3
- package/dist/components/preview/TabLayout.svelte.d.ts +5 -0
- package/dist/components/preview/inputs/ChecklistInput.svelte +7 -2
- package/dist/components/preview/inputs/ChecklistInput.svelte.d.ts +2 -1
- package/dist/components/preview/inputs/DropdownInput.svelte +16 -4
- package/dist/components/preview/inputs/DropdownInput.svelte.d.ts +2 -1
- package/dist/components/preview/inputs/FileInput.svelte +56 -65
- package/dist/components/preview/inputs/GeometryInput.svelte +128 -0
- package/dist/components/preview/inputs/GeometryInput.svelte.d.ts +21 -0
- package/dist/components/preview/inputs/OptionGlyph.svelte +29 -0
- package/dist/components/preview/inputs/OptionGlyph.svelte.d.ts +8 -0
- package/dist/components/preview/inputs/SegmentedInput.svelte +44 -0
- package/dist/components/preview/inputs/SegmentedInput.svelte.d.ts +12 -0
- package/dist/components/preview/inputs/TileInput.svelte +80 -0
- package/dist/components/preview/inputs/TileInput.svelte.d.ts +12 -0
- package/dist/components/preview/inputs/index.d.ts +3 -0
- package/dist/components/preview/inputs/index.js +3 -0
- package/dist/components/primitives/SolvingIndicator.svelte +28 -3
- package/dist/components/primitives/SolvingIndicator.svelte.d.ts +7 -0
- package/dist/components/primitives/button/button.svelte +3 -0
- package/dist/components/primitives/button/button.svelte.d.ts +3 -0
- package/dist/components/primitives/toggle/toggle.svelte +3 -1
- package/dist/components/primitives/toggle/toggle.svelte.d.ts +3 -0
- package/dist/components/viewer/Viewer.svelte +11 -11
- package/dist/compute/solving.svelte.d.ts +7 -1
- package/dist/compute/solving.svelte.js +12 -1
- package/dist/contexts/hostContext.svelte.d.ts +39 -0
- package/dist/contexts/hostContext.svelte.js +9 -0
- package/dist/i18n/messages.d.ts +33 -0
- package/dist/i18n/messages.js +58 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/schema/host-support.d.ts +29 -0
- package/dist/schema/host-support.js +97 -0
- package/dist/schema/option-display.d.ts +12 -0
- package/dist/schema/option-display.js +29 -0
- package/dist/schema/param-exporter.d.ts +4 -1
- package/dist/schema/param-exporter.js +10 -6
- package/dist/styles/base.css +6 -3
- package/dist/utils/file-download.js +6 -10
- package/dist/utils/host-files.d.ts +12 -0
- package/dist/utils/host-files.js +46 -0
- package/dist/utils/webp-to-png.d.ts +10 -0
- package/dist/utils/webp-to-png.js +41 -0
- package/package.json +9 -9
- package/src/lib/components/compute/AppLayout.svelte +37 -7
- package/src/lib/components/compute/ParameterPresetManager.svelte +8 -20
- package/src/lib/components/compute/SolveMessages.svelte +26 -3
- package/src/lib/components/layout/PageFooter.svelte +3 -1
- package/src/lib/components/layout/SideNav.svelte +55 -14
- package/src/lib/components/preview/ChartOutput.svelte +18 -13
- package/src/lib/components/preview/Group.svelte +6 -1
- package/src/lib/components/preview/ImageOutput.svelte +3 -1
- package/src/lib/components/preview/InputControl.svelte +108 -44
- package/src/lib/components/preview/TabContent.svelte +61 -29
- package/src/lib/components/preview/TabLayout.svelte +25 -3
- package/src/lib/components/preview/inputs/ChecklistInput.svelte +7 -2
- package/src/lib/components/preview/inputs/DropdownInput.svelte +16 -4
- package/src/lib/components/preview/inputs/FileInput.svelte +56 -65
- package/src/lib/components/preview/inputs/GeometryInput.svelte +128 -0
- package/src/lib/components/preview/inputs/OptionGlyph.svelte +29 -0
- package/src/lib/components/preview/inputs/SegmentedInput.svelte +44 -0
- package/src/lib/components/preview/inputs/TileInput.svelte +80 -0
- package/src/lib/components/preview/inputs/index.ts +3 -0
- package/src/lib/components/primitives/SolvingIndicator.svelte +28 -3
- package/src/lib/components/primitives/button/button.svelte +3 -0
- package/src/lib/components/primitives/toggle/toggle.svelte +3 -1
- package/src/lib/components/viewer/Viewer.svelte +11 -11
- package/src/lib/compute/solving.svelte.ts +16 -1
- package/src/lib/contexts/hostContext.svelte.ts +51 -0
- package/src/lib/i18n/messages.ts +96 -0
- package/src/lib/index.ts +20 -0
- package/src/lib/schema/host-support.ts +129 -0
- package/src/lib/schema/option-display.ts +44 -0
- package/src/lib/schema/param-exporter.ts +10 -7
- package/src/lib/styles/base.css +6 -3
- package/src/lib/utils/file-download.ts +7 -11
- package/src/lib/utils/host-files.ts +61 -0
- package/src/lib/utils/webp-to-png.ts +48 -0
|
@@ -8,6 +8,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
|
|
|
8
8
|
default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
|
|
9
9
|
sm: "h-7 min-w-7 rounded-md px-2.5 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5";
|
|
10
10
|
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
|
|
11
|
+
stacked: "min-h-12 min-w-0 flex-col gap-1 px-1 py-1.5 text-xs";
|
|
11
12
|
};
|
|
12
13
|
}, undefined, "hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-lg text-sm font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle inline-flex items-center justify-center whitespace-nowrap outline-none hover:bg-muted focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", {
|
|
13
14
|
variant: {
|
|
@@ -18,6 +19,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
|
|
|
18
19
|
default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
|
|
19
20
|
sm: "h-7 min-w-7 rounded-md px-2.5 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5";
|
|
20
21
|
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
|
|
22
|
+
stacked: "min-h-12 min-w-0 flex-col gap-1 px-1 py-1.5 text-xs";
|
|
21
23
|
};
|
|
22
24
|
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
23
25
|
variant: {
|
|
@@ -28,6 +30,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
|
|
|
28
30
|
default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
|
|
29
31
|
sm: "h-7 min-w-7 rounded-md px-2.5 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5";
|
|
30
32
|
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
|
|
33
|
+
stacked: "min-h-12 min-w-0 flex-col gap-1 px-1 py-1.5 text-xs";
|
|
31
34
|
};
|
|
32
35
|
}, undefined>>;
|
|
33
36
|
export type ToggleVariant = VariantProps<typeof toggleVariants>['variant'];
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
import { Button } from '../primitives/button/index.js';
|
|
41
41
|
import type { Locale } from '../../i18n/messages';
|
|
42
42
|
import { layerLocaleContext } from '../../i18n/localeContext.svelte';
|
|
43
|
+
import { saveBlob } from '../../utils/host-files';
|
|
43
44
|
|
|
44
45
|
export interface ViewerConfig {
|
|
45
46
|
showScreenshotButton?: boolean;
|
|
@@ -433,15 +434,8 @@
|
|
|
433
434
|
const blob = await captureImage?.();
|
|
434
435
|
if (!blob) return;
|
|
435
436
|
|
|
436
|
-
const url = URL.createObjectURL(blob);
|
|
437
|
-
const link = document.createElement('a');
|
|
438
437
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
|
439
|
-
|
|
440
|
-
link.download = `viewer-${timestamp}.png`;
|
|
441
|
-
document.body.appendChild(link);
|
|
442
|
-
link.click();
|
|
443
|
-
document.body.removeChild(link);
|
|
444
|
-
URL.revokeObjectURL(url);
|
|
438
|
+
await saveBlob(blob, `viewer-${timestamp}.png`);
|
|
445
439
|
}
|
|
446
440
|
</script>
|
|
447
441
|
|
|
@@ -462,7 +456,7 @@
|
|
|
462
456
|
the DOM, so this is the only observable proof a solve's geometry decoded and
|
|
463
457
|
rendered; e2e asserts on it. -->
|
|
464
458
|
<canvas
|
|
465
|
-
class="block h-full w-full"
|
|
459
|
+
class="viewer-canvas block h-full w-full select-none"
|
|
466
460
|
data-testid="viewer-canvas"
|
|
467
461
|
data-mesh-count={meshes.length}
|
|
468
462
|
bind:this={canvas}
|
|
@@ -513,7 +507,7 @@
|
|
|
513
507
|
{#snippet child({ props })}
|
|
514
508
|
<Button
|
|
515
509
|
{...props}
|
|
516
|
-
variant="
|
|
510
|
+
variant="overlay"
|
|
517
511
|
size="icon-lg"
|
|
518
512
|
title={t.toolsMenu}
|
|
519
513
|
aria-label={t.toolsMenu}
|
|
@@ -630,7 +624,7 @@
|
|
|
630
624
|
<!-- Touch has no F key, and fit-to-view is the way back from a framed part. -->
|
|
631
625
|
<div class="hidden pointer-coarse:block">
|
|
632
626
|
<Button
|
|
633
|
-
variant="
|
|
627
|
+
variant="overlay"
|
|
634
628
|
size="icon-lg"
|
|
635
629
|
title={t.fitToView}
|
|
636
630
|
aria-label={t.fitToView}
|
|
@@ -666,6 +660,12 @@
|
|
|
666
660
|
border-radius: 0 !important;
|
|
667
661
|
}
|
|
668
662
|
|
|
663
|
+
/* A long press on touch otherwise selects the canvas as an image and opens the save/copy callout. */
|
|
664
|
+
.viewer-canvas {
|
|
665
|
+
-webkit-touch-callout: none;
|
|
666
|
+
-webkit-tap-highlight-color: transparent;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
669
|
.blur-overlay {
|
|
670
670
|
pointer-events: none;
|
|
671
671
|
}
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* - Average over SLOW_THRESHOLD_MS: shown immediately
|
|
7
7
|
* - Between the two: shown after a proportional slice of ANIMATION_DELAY
|
|
8
8
|
* - First solve: shown immediately, there is no history yet
|
|
9
|
+
*
|
|
10
|
+
* `freezesDuringSolve` is for a host that can't paint while it solves (an embedded WebView on
|
|
11
|
+
* the solver's thread). There the indicator has to be on screen before the solve starts, when its
|
|
12
|
+
* length is still unknown, so it follows `isSolving` exactly: no history, no delay.
|
|
9
13
|
*/
|
|
10
|
-
export declare function createSolvingIndicator(isSolving: () => boolean
|
|
14
|
+
export declare function createSolvingIndicator(isSolving: () => boolean, options?: {
|
|
15
|
+
freezesDuringSolve?: boolean;
|
|
16
|
+
}): {
|
|
11
17
|
readonly show: boolean;
|
|
12
18
|
};
|
|
@@ -8,8 +8,19 @@ import { APP_DEFAULTS } from '../constants';
|
|
|
8
8
|
* - Average over SLOW_THRESHOLD_MS: shown immediately
|
|
9
9
|
* - Between the two: shown after a proportional slice of ANIMATION_DELAY
|
|
10
10
|
* - First solve: shown immediately, there is no history yet
|
|
11
|
+
*
|
|
12
|
+
* `freezesDuringSolve` is for a host that can't paint while it solves (an embedded WebView on
|
|
13
|
+
* the solver's thread). There the indicator has to be on screen before the solve starts, when its
|
|
14
|
+
* length is still unknown, so it follows `isSolving` exactly: no history, no delay.
|
|
11
15
|
*/
|
|
12
|
-
export function createSolvingIndicator(isSolving) {
|
|
16
|
+
export function createSolvingIndicator(isSolving, options = {}) {
|
|
17
|
+
if (options.freezesDuringSolve) {
|
|
18
|
+
return {
|
|
19
|
+
get show() {
|
|
20
|
+
return isSolving();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
13
24
|
const { FAST_THRESHOLD_MS, SLOW_THRESHOLD_MS, HISTORY_SIZE, ANIMATION_DELAY } = APP_DEFAULTS.SOLVING_INDICATOR;
|
|
14
25
|
let show = $state(false);
|
|
15
26
|
let timeout = null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { GeometryKind } from '@selvajs/schemas';
|
|
2
|
+
import type { UIHost } from '../schema/host-support';
|
|
3
|
+
export type GeometryPickResult = {
|
|
4
|
+
outcome: 'picked';
|
|
5
|
+
summary: string;
|
|
6
|
+
} | {
|
|
7
|
+
outcome: 'cancelled';
|
|
8
|
+
} | {
|
|
9
|
+
outcome: 'failed';
|
|
10
|
+
error: string;
|
|
11
|
+
};
|
|
12
|
+
/** What a pick button can ask for: a Get Geometry filter's kinds, or Get Point/Line/Plane's own. */
|
|
13
|
+
export type PickKind = GeometryKind | 'line' | 'plane';
|
|
14
|
+
/** What a geometry input lets the user pick, as the host discovered it on the canvas. */
|
|
15
|
+
export interface GeometryPickTarget {
|
|
16
|
+
/** The kinds its filter allows; absent when it takes anything. */
|
|
17
|
+
kinds?: readonly GeometryKind[];
|
|
18
|
+
/** 1 picks a single object. */
|
|
19
|
+
atMost?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface HostActions {
|
|
22
|
+
host: UIHost;
|
|
23
|
+
/** Prompts in the Rhino viewport for a geometry input. The picked geometry stays in Grasshopper. */
|
|
24
|
+
pickGeometry?: (paramId: string) => Promise<GeometryPickResult>;
|
|
25
|
+
/** Empties a geometry input again, back to the definition's own value. */
|
|
26
|
+
clearGeometry?: (paramId: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Reports what the param holds after every pick or clear, including those another page asked
|
|
29
|
+
* for; null when nothing is picked into it. Returns an unsubscribe.
|
|
30
|
+
*/
|
|
31
|
+
onGeometrySummary?: (paramId: string, listener: (summary: string | null) => void) => () => void;
|
|
32
|
+
describeGeometry?: (paramId: string) => GeometryPickTarget | undefined;
|
|
33
|
+
/** Presses a button input: one solve with `true`, then it goes back to `false`. */
|
|
34
|
+
trigger?: (paramId: string) => void;
|
|
35
|
+
/** Grasshopper refuses a press while it solves, so the button waits it out disabled. */
|
|
36
|
+
isSolving?: () => boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function setHostContext(actions: HostActions): void;
|
|
39
|
+
export declare function getHostContext(): HostActions;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const SELVA_HOST = { host: 'selva' };
|
|
3
|
+
const HOST_CONTEXT_KEY = Symbol('host-context');
|
|
4
|
+
export function setHostContext(actions) {
|
|
5
|
+
setContext(HOST_CONTEXT_KEY, actions);
|
|
6
|
+
}
|
|
7
|
+
export function getHostContext() {
|
|
8
|
+
return getContext(HOST_CONTEXT_KEY) ?? SELVA_HOST;
|
|
9
|
+
}
|
package/dist/i18n/messages.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface ViewerMessages {
|
|
|
50
50
|
solveMessagesConfirm: string;
|
|
51
51
|
solveMessagesDiscard: string;
|
|
52
52
|
solveMessagesAbort: string;
|
|
53
|
+
/** Stands in for Abort where the page can't take a click mid-solve. */
|
|
54
|
+
solveMessagesEscHint: string;
|
|
53
55
|
solveMessagesDismiss: string;
|
|
54
56
|
solveSolving: string;
|
|
55
57
|
parametersTab: string;
|
|
@@ -101,6 +103,37 @@ export interface ViewerMessages {
|
|
|
101
103
|
maxLength: string;
|
|
102
104
|
invalidFormat: string;
|
|
103
105
|
invalidPattern: string;
|
|
106
|
+
/** The pick button when the input takes several kinds of object. */
|
|
107
|
+
geometryPickAnyOne: string;
|
|
108
|
+
geometryPickAnyOther: string;
|
|
109
|
+
/** Shown while Rhino's viewport prompt for a pick is open. */
|
|
110
|
+
geometryPicking: string;
|
|
111
|
+
/** Where this page may not pick, such as a browser tab beside the Rhino panel. */
|
|
112
|
+
geometryPickInPanel: string;
|
|
113
|
+
geometryPickAgain: string;
|
|
114
|
+
geometryClear: string;
|
|
115
|
+
geometryPickPointOne: string;
|
|
116
|
+
geometryPickPointOther: string;
|
|
117
|
+
geometryPickPointCloudOne: string;
|
|
118
|
+
geometryPickPointCloudOther: string;
|
|
119
|
+
geometryPickCurveOne: string;
|
|
120
|
+
geometryPickCurveOther: string;
|
|
121
|
+
geometryPickLineOne: string;
|
|
122
|
+
geometryPickLineOther: string;
|
|
123
|
+
geometryPickPlaneOne: string;
|
|
124
|
+
geometryPickPlaneOther: string;
|
|
125
|
+
geometryPickSurfaceOne: string;
|
|
126
|
+
geometryPickSurfaceOther: string;
|
|
127
|
+
geometryPickPolysurfaceOne: string;
|
|
128
|
+
geometryPickPolysurfaceOther: string;
|
|
129
|
+
geometryPickExtrusionOne: string;
|
|
130
|
+
geometryPickExtrusionOther: string;
|
|
131
|
+
geometryPickMeshOne: string;
|
|
132
|
+
geometryPickMeshOther: string;
|
|
133
|
+
geometryPickSubdOne: string;
|
|
134
|
+
geometryPickSubdOther: string;
|
|
135
|
+
geometryPickBlockOne: string;
|
|
136
|
+
geometryPickBlockOther: string;
|
|
104
137
|
fileModeUpload: string;
|
|
105
138
|
fileModeUrl: string;
|
|
106
139
|
fileUrlLabel: string;
|
package/dist/i18n/messages.js
CHANGED
|
@@ -58,6 +58,7 @@ const en = {
|
|
|
58
58
|
solveMessagesConfirm: 'Continue',
|
|
59
59
|
solveMessagesDiscard: 'Discard',
|
|
60
60
|
solveMessagesAbort: 'Abort',
|
|
61
|
+
solveMessagesEscHint: 'Esc aborts the solve',
|
|
61
62
|
solveMessagesDismiss: 'Dismiss',
|
|
62
63
|
solveSolving: 'Solving…',
|
|
63
64
|
parametersTab: 'Parameters',
|
|
@@ -98,6 +99,34 @@ const en = {
|
|
|
98
99
|
maxLength: 'Maximum {max} characters allowed',
|
|
99
100
|
invalidFormat: 'Invalid format',
|
|
100
101
|
invalidPattern: 'Invalid validation pattern configured',
|
|
102
|
+
geometryPickAnyOne: 'Pick an object',
|
|
103
|
+
geometryPickAnyOther: 'Pick objects',
|
|
104
|
+
geometryPicking: 'Pick in the viewport, Esc to cancel',
|
|
105
|
+
geometryPickInPanel: 'Pick in the Rhino panel',
|
|
106
|
+
geometryPickAgain: 'Pick again',
|
|
107
|
+
geometryClear: 'Clear',
|
|
108
|
+
geometryPickPointOne: 'Pick a point',
|
|
109
|
+
geometryPickPointOther: 'Pick points',
|
|
110
|
+
geometryPickPointCloudOne: 'Pick a point cloud',
|
|
111
|
+
geometryPickPointCloudOther: 'Pick point clouds',
|
|
112
|
+
geometryPickCurveOne: 'Pick a curve',
|
|
113
|
+
geometryPickCurveOther: 'Pick curves',
|
|
114
|
+
geometryPickLineOne: 'Pick a line',
|
|
115
|
+
geometryPickLineOther: 'Pick lines',
|
|
116
|
+
geometryPickPlaneOne: 'Pick a plane',
|
|
117
|
+
geometryPickPlaneOther: 'Pick planes',
|
|
118
|
+
geometryPickSurfaceOne: 'Pick a surface',
|
|
119
|
+
geometryPickSurfaceOther: 'Pick surfaces',
|
|
120
|
+
geometryPickPolysurfaceOne: 'Pick a polysurface',
|
|
121
|
+
geometryPickPolysurfaceOther: 'Pick polysurfaces',
|
|
122
|
+
geometryPickExtrusionOne: 'Pick an extrusion',
|
|
123
|
+
geometryPickExtrusionOther: 'Pick extrusions',
|
|
124
|
+
geometryPickMeshOne: 'Pick a mesh',
|
|
125
|
+
geometryPickMeshOther: 'Pick meshes',
|
|
126
|
+
geometryPickSubdOne: 'Pick a SubD',
|
|
127
|
+
geometryPickSubdOther: 'Pick SubDs',
|
|
128
|
+
geometryPickBlockOne: 'Pick a block',
|
|
129
|
+
geometryPickBlockOther: 'Pick blocks',
|
|
101
130
|
fileModeUpload: 'Upload',
|
|
102
131
|
fileModeUrl: 'URL',
|
|
103
132
|
fileUrlLabel: 'File URL',
|
|
@@ -203,6 +232,7 @@ const de = {
|
|
|
203
232
|
solveMessagesConfirm: 'Weiter',
|
|
204
233
|
solveMessagesDiscard: 'Verwerfen',
|
|
205
234
|
solveMessagesAbort: 'Abbrechen',
|
|
235
|
+
solveMessagesEscHint: 'Esc bricht die Berechnung ab',
|
|
206
236
|
solveMessagesDismiss: 'Schliessen',
|
|
207
237
|
solveSolving: 'Berechnung läuft…',
|
|
208
238
|
parametersTab: 'Parameter',
|
|
@@ -243,6 +273,34 @@ const de = {
|
|
|
243
273
|
maxLength: 'Höchstens {max} Zeichen erlaubt',
|
|
244
274
|
invalidFormat: 'Ungültiges Format',
|
|
245
275
|
invalidPattern: 'Ungültiges Prüfmuster hinterlegt',
|
|
276
|
+
geometryPickAnyOne: 'Objekt wählen',
|
|
277
|
+
geometryPickAnyOther: 'Objekte wählen',
|
|
278
|
+
geometryPicking: 'Im Ansichtsfenster wählen, Esc bricht ab',
|
|
279
|
+
geometryPickInPanel: 'Im Rhino-Panel wählen',
|
|
280
|
+
geometryPickAgain: 'Neu wählen',
|
|
281
|
+
geometryClear: 'Leeren',
|
|
282
|
+
geometryPickPointOne: 'Punkt wählen',
|
|
283
|
+
geometryPickPointOther: 'Punkte wählen',
|
|
284
|
+
geometryPickPointCloudOne: 'Punktwolke wählen',
|
|
285
|
+
geometryPickPointCloudOther: 'Punktwolken wählen',
|
|
286
|
+
geometryPickCurveOne: 'Kurve wählen',
|
|
287
|
+
geometryPickCurveOther: 'Kurven wählen',
|
|
288
|
+
geometryPickLineOne: 'Linie wählen',
|
|
289
|
+
geometryPickLineOther: 'Linien wählen',
|
|
290
|
+
geometryPickPlaneOne: 'Ebene wählen',
|
|
291
|
+
geometryPickPlaneOther: 'Ebenen wählen',
|
|
292
|
+
geometryPickSurfaceOne: 'Fläche wählen',
|
|
293
|
+
geometryPickSurfaceOther: 'Flächen wählen',
|
|
294
|
+
geometryPickPolysurfaceOne: 'Flächenverband wählen',
|
|
295
|
+
geometryPickPolysurfaceOther: 'Flächenverbände wählen',
|
|
296
|
+
geometryPickExtrusionOne: 'Extrusion wählen',
|
|
297
|
+
geometryPickExtrusionOther: 'Extrusionen wählen',
|
|
298
|
+
geometryPickMeshOne: 'Netz wählen',
|
|
299
|
+
geometryPickMeshOther: 'Netze wählen',
|
|
300
|
+
geometryPickSubdOne: 'SubD wählen',
|
|
301
|
+
geometryPickSubdOther: 'SubDs wählen',
|
|
302
|
+
geometryPickBlockOne: 'Block wählen',
|
|
303
|
+
geometryPickBlockOther: 'Blöcke wählen',
|
|
246
304
|
fileModeUpload: 'Hochladen',
|
|
247
305
|
fileModeUrl: 'URL',
|
|
248
306
|
fileUrlLabel: 'Datei-URL',
|
package/dist/index.d.ts
CHANGED
|
@@ -9,15 +9,18 @@ export { default as Viewer } from './components/viewer/Viewer.svelte';
|
|
|
9
9
|
export * from './schema/defaults';
|
|
10
10
|
export * from './schema/dynamic-value-list';
|
|
11
11
|
export * from './schema/traversal';
|
|
12
|
+
export { type UIHost, type ElementKind, HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
|
|
12
13
|
export * from './compute/solving.svelte';
|
|
13
14
|
export { useSolveSession } from './compute/useSolveSession.svelte';
|
|
14
15
|
export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveSettlement, type SolveDriver, type SolveReporter, type LiveSolveState, type SolvePhase, EMPTY_LIVE_SOLVE, decodeOutcome, finalizeResult, type SolveOutcome } from '@selvajs/solve/client';
|
|
15
16
|
export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs, type ExternalValueRef, type ExternalInput } from '@selvajs/solve/client';
|
|
16
17
|
export * from './contexts/footerContext.svelte';
|
|
17
18
|
export * from './contexts/clientSlotContext.svelte';
|
|
19
|
+
export * from './contexts/hostContext.svelte';
|
|
18
20
|
export * from './composables/useFooterItem.svelte';
|
|
19
21
|
export * from './utils';
|
|
20
22
|
export { randomId } from './utils/randomId';
|
|
23
|
+
export { saveBlob, pickFile, setBlobSaver, setFilePicker, type BlobSaver, type FilePicker } from './utils/host-files';
|
|
21
24
|
export type { ActionButton } from './types/actionButton';
|
|
22
25
|
export type { SolveFn, SolveResult, SolveDiagnostic } from '@selvajs/solve/shared';
|
|
23
26
|
export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { default as Viewer } from './components/viewer/Viewer.svelte';
|
|
|
13
13
|
export * from './schema/defaults';
|
|
14
14
|
export * from './schema/dynamic-value-list';
|
|
15
15
|
export * from './schema/traversal';
|
|
16
|
+
export { HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
|
|
16
17
|
export * from './compute/solving.svelte';
|
|
17
18
|
// Solve Session seam. Re-exported so transports outside this package — e.g.
|
|
18
19
|
// plugin-ui's WebSocket driver — can satisfy SolveDriver and drive a session.
|
|
@@ -25,7 +26,9 @@ EMPTY_LIVE_SOLVE, decodeOutcome, finalizeResult } from '@selvajs/solve/client';
|
|
|
25
26
|
export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs } from '@selvajs/solve/client';
|
|
26
27
|
export * from './contexts/footerContext.svelte';
|
|
27
28
|
export * from './contexts/clientSlotContext.svelte';
|
|
29
|
+
export * from './contexts/hostContext.svelte';
|
|
28
30
|
export * from './composables/useFooterItem.svelte';
|
|
29
31
|
export * from './utils';
|
|
30
32
|
export { randomId } from './utils/randomId';
|
|
33
|
+
export { saveBlob, pickFile, setBlobSaver, setFilePicker } from './utils/host-files';
|
|
31
34
|
export { DEFAULT_PRESET_LABELS } from './types/presetLabels';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InputLayoutItem, LayoutItem, SchemaInput, UISchema } from '@selvajs/schemas';
|
|
2
|
+
/**
|
|
3
|
+
* Where a UI runs: `rhino` is the plugin's page inside Rhino, solving in the user's own
|
|
4
|
+
* Grasshopper; `selva` is the Selva app, solving headless on Rhino.Compute.
|
|
5
|
+
*/
|
|
6
|
+
export type UIHost = 'rhino' | 'selva';
|
|
7
|
+
/**
|
|
8
|
+
* - `value`: an input or output every host renders.
|
|
9
|
+
* - `geometry`: a Rhino Get Geometry/Point/Line/Plane input, filled by picking in the viewport.
|
|
10
|
+
* - `trigger`: a boolean input shown as a button; a press solves once with `true`.
|
|
11
|
+
* - `unknown`: a layout item from a newer schema than this build knows.
|
|
12
|
+
*/
|
|
13
|
+
export type ElementKind = 'value' | 'geometry' | 'trigger' | 'unknown';
|
|
14
|
+
export declare const HOST_LABELS: Record<UIHost, string>;
|
|
15
|
+
/** The control comes from this, whatever the saved widgetType says: saved ones hold `text`. */
|
|
16
|
+
export declare function isGeometryParamType(paramType: string | undefined): boolean;
|
|
17
|
+
export declare function isTriggerItem(item: LayoutItem): boolean;
|
|
18
|
+
export declare function layoutItemKind(item: LayoutItem, inputs: readonly SchemaInput[]): ElementKind;
|
|
19
|
+
export declare function hostsFor(kind: ElementKind): readonly UIHost[];
|
|
20
|
+
export declare function isSupportedOn(host: UIHost, kind: ElementKind): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Inputs the form keeps no value for: geometry lives on the Grasshopper param after a pick, and a
|
|
23
|
+
* button only sends a press. Value updates and presets skip them; holding a button's `true` would
|
|
24
|
+
* fire its action again on every solve.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isValuelessInput(item: InputLayoutItem, input: SchemaInput | undefined): boolean;
|
|
27
|
+
export declare function valuelessInputIds(schema: UISchema): Set<string>;
|
|
28
|
+
/** The placed items a host won't render, for the builder's summary of where the UI runs. */
|
|
29
|
+
export declare function unsupportedItems(schema: UISchema, host: UIHost): LayoutItem[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Which elements of a schema each host renders. Worked out from the schema on every read and
|
|
2
|
+
// never stored in it: a stored copy goes stale the way a saved paramType does. Making an element
|
|
3
|
+
// work on Selva later is a change to SUPPORT, with no schema change and no migration.
|
|
4
|
+
import { getGroups } from '@selvajs/schemas';
|
|
5
|
+
const SUPPORT = {
|
|
6
|
+
value: ['rhino', 'selva'],
|
|
7
|
+
geometry: ['rhino'],
|
|
8
|
+
trigger: ['rhino'],
|
|
9
|
+
unknown: []
|
|
10
|
+
};
|
|
11
|
+
export const HOST_LABELS = { rhino: 'Rhino', selva: 'Selva' };
|
|
12
|
+
const GEOMETRY_PARAM_TYPES = new Set(['geometry', 'point', 'line', 'plane']);
|
|
13
|
+
// Keyed by the schema's own widget unions, so a widget added to the schema fails to compile here
|
|
14
|
+
// until someone decides which hosts render it.
|
|
15
|
+
const INPUT_WIDGETS = {
|
|
16
|
+
number: true,
|
|
17
|
+
text: true,
|
|
18
|
+
dropdown: true,
|
|
19
|
+
dynamicValueList: true,
|
|
20
|
+
checkbox: true,
|
|
21
|
+
file: true,
|
|
22
|
+
color: true
|
|
23
|
+
};
|
|
24
|
+
const OUTPUT_WIDGETS = {
|
|
25
|
+
text: true,
|
|
26
|
+
number: true,
|
|
27
|
+
file: true,
|
|
28
|
+
chart: true,
|
|
29
|
+
image: true,
|
|
30
|
+
dynamicValueList: true
|
|
31
|
+
};
|
|
32
|
+
const KNOWN_WIDGETS = {
|
|
33
|
+
input: new Set(Object.keys(INPUT_WIDGETS)),
|
|
34
|
+
output: new Set(Object.keys(OUTPUT_WIDGETS))
|
|
35
|
+
};
|
|
36
|
+
/** The control comes from this, whatever the saved widgetType says: saved ones hold `text`. */
|
|
37
|
+
export function isGeometryParamType(paramType) {
|
|
38
|
+
return paramType !== undefined && GEOMETRY_PARAM_TYPES.has(paramType);
|
|
39
|
+
}
|
|
40
|
+
export function isTriggerItem(item) {
|
|
41
|
+
return (item.type === 'input' &&
|
|
42
|
+
item.widgetType === 'checkbox' &&
|
|
43
|
+
item.config?.displayAs === 'button');
|
|
44
|
+
}
|
|
45
|
+
export function layoutItemKind(item, inputs) {
|
|
46
|
+
if (item.type === 'linebreak')
|
|
47
|
+
return 'value';
|
|
48
|
+
if (item.type !== 'input' && item.type !== 'output')
|
|
49
|
+
return 'unknown';
|
|
50
|
+
if (item.type === 'input') {
|
|
51
|
+
const paramType = inputs.find((i) => i.id === item.paramId)?.paramType;
|
|
52
|
+
if (isGeometryParamType(paramType))
|
|
53
|
+
return 'geometry';
|
|
54
|
+
}
|
|
55
|
+
if (!KNOWN_WIDGETS[item.type].has(item.widgetType))
|
|
56
|
+
return 'unknown';
|
|
57
|
+
return isTriggerItem(item) ? 'trigger' : 'value';
|
|
58
|
+
}
|
|
59
|
+
export function hostsFor(kind) {
|
|
60
|
+
return SUPPORT[kind];
|
|
61
|
+
}
|
|
62
|
+
export function isSupportedOn(host, kind) {
|
|
63
|
+
return SUPPORT[kind].includes(host);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Inputs the form keeps no value for: geometry lives on the Grasshopper param after a pick, and a
|
|
67
|
+
* button only sends a press. Value updates and presets skip them; holding a button's `true` would
|
|
68
|
+
* fire its action again on every solve.
|
|
69
|
+
*/
|
|
70
|
+
export function isValuelessInput(item, input) {
|
|
71
|
+
return isGeometryParamType(input?.paramType) || isTriggerItem(item);
|
|
72
|
+
}
|
|
73
|
+
export function valuelessInputIds(schema) {
|
|
74
|
+
const ids = new Set();
|
|
75
|
+
for (const input of schema.inputs) {
|
|
76
|
+
if (isGeometryParamType(input.paramType))
|
|
77
|
+
ids.add(input.id);
|
|
78
|
+
}
|
|
79
|
+
for (const group of getGroups(schema)) {
|
|
80
|
+
for (const item of group.items ?? []) {
|
|
81
|
+
if (isTriggerItem(item))
|
|
82
|
+
ids.add(item.paramId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return ids;
|
|
86
|
+
}
|
|
87
|
+
/** The placed items a host won't render, for the builder's summary of where the UI runs. */
|
|
88
|
+
export function unsupportedItems(schema, host) {
|
|
89
|
+
const items = [];
|
|
90
|
+
for (const group of getGroups(schema)) {
|
|
91
|
+
for (const item of group.items ?? []) {
|
|
92
|
+
if (!isSupportedOn(host, layoutItemKind(item, schema.inputs)))
|
|
93
|
+
items.push(item);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return items;
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DropdownWidgetConfig, OptionMedia, UISchema } from '@selvajs/schemas';
|
|
2
|
+
export type OptionDisplay = NonNullable<DropdownWidgetConfig['displayAs']>;
|
|
3
|
+
export declare function resolveOptionDisplay(displayAs: string | undefined, optionCount: number): OptionDisplay;
|
|
4
|
+
export type GlyphSource = {
|
|
5
|
+
kind: 'icon';
|
|
6
|
+
d: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'image';
|
|
9
|
+
src: string;
|
|
10
|
+
};
|
|
11
|
+
/** What to draw beside an option: its thumbnail, else its icon. */
|
|
12
|
+
export declare function optionGlyph(media: OptionMedia | undefined, assets: UISchema['assets'] | undefined): GlyphSource | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Past this a button row no longer fits a panel column. A computed list can grow past it at
|
|
2
|
+
// runtime, so the row turns back into a dropdown instead of wrapping.
|
|
3
|
+
const MAX_SEGMENTED_OPTIONS = 6;
|
|
4
|
+
export function resolveOptionDisplay(displayAs, optionCount) {
|
|
5
|
+
switch (displayAs) {
|
|
6
|
+
case 'checklist':
|
|
7
|
+
case 'tiles':
|
|
8
|
+
return displayAs;
|
|
9
|
+
case 'segmented':
|
|
10
|
+
return optionCount <= MAX_SEGMENTED_OPTIONS ? 'segmented' : 'dropdown';
|
|
11
|
+
default:
|
|
12
|
+
return 'dropdown';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// The schema can come from any .gh, so an image reaches <img> only as an inline raster data URI.
|
|
16
|
+
// Anything else could be a URL that makes every viewer's browser call wherever the file points.
|
|
17
|
+
const RASTER_DATA_URI = /^data:image\/(webp|jpeg|png);base64,[A-Za-z0-9+/=]+$/;
|
|
18
|
+
/** What to draw beside an option: its thumbnail, else its icon. */
|
|
19
|
+
export function optionGlyph(media, assets) {
|
|
20
|
+
if (!media || !assets)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (media.image) {
|
|
23
|
+
const src = assets[media.image]?.img;
|
|
24
|
+
if (src && RASTER_DATA_URI.test(src))
|
|
25
|
+
return { kind: 'image', src };
|
|
26
|
+
}
|
|
27
|
+
const d = media.icon ? assets[media.icon]?.d : undefined;
|
|
28
|
+
return d ? { kind: 'icon', d } : undefined;
|
|
29
|
+
}
|
|
@@ -14,6 +14,8 @@ export declare function validateSavedState(savedState: ParameterPreset, currentS
|
|
|
14
14
|
* Params the current schema no longer has are dropped. That's the same condition
|
|
15
15
|
* `validateSavedState` flags as an error, but the rule is re-derived from the schema here
|
|
16
16
|
* rather than read back off the issue list — nothing couples to how sentinel ids are encoded.
|
|
17
|
+
* Valueless inputs are dropped too: a preset saved before an input became a button or a
|
|
18
|
+
* geometry input still holds a value for it.
|
|
17
19
|
*/
|
|
18
20
|
export declare function extractLoadableValues(savedState: ParameterPreset, currentSchema: UISchema): Record<string, unknown>;
|
|
19
21
|
export interface PresetLoadResult {
|
|
@@ -28,5 +30,6 @@ export interface PresetLoadResult {
|
|
|
28
30
|
}
|
|
29
31
|
/** Validates and computes the loadable values in one pass, so callers thread one object. */
|
|
30
32
|
export declare function loadPreset(savedState: ParameterPreset, currentSchema: UISchema): PresetLoadResult;
|
|
31
|
-
|
|
33
|
+
/** Resolves false when the user cancelled the save. */
|
|
34
|
+
export declare function exportStateAsJson(savedState: ParameterPreset): Promise<boolean>;
|
|
32
35
|
export declare function importStateFromJson(file: File): Promise<ParameterPreset>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomId } from '../utils/randomId';
|
|
2
|
+
import { saveBlob } from '../utils/host-files';
|
|
2
3
|
import { getGroups } from '@selvajs/schemas';
|
|
4
|
+
import { isValuelessInput, valuelessInputIds } from './host-support';
|
|
3
5
|
export function createSavedState(schema, currentValues, metadata) {
|
|
4
6
|
const parameters = [];
|
|
5
7
|
for (const group of getGroups(schema)) {
|
|
@@ -8,6 +10,8 @@ export function createSavedState(schema, currentValues, metadata) {
|
|
|
8
10
|
continue;
|
|
9
11
|
const value = currentValues[item.paramId];
|
|
10
12
|
const inputDef = schema.inputs.find((i) => i.id === item.paramId);
|
|
13
|
+
if (isValuelessInput(item, inputDef))
|
|
14
|
+
continue;
|
|
11
15
|
if (value !== undefined && inputDef) {
|
|
12
16
|
parameters.push({
|
|
13
17
|
paramId: item.paramId,
|
|
@@ -78,12 +82,15 @@ export function validateSavedState(savedState, currentSchema) {
|
|
|
78
82
|
* Params the current schema no longer has are dropped. That's the same condition
|
|
79
83
|
* `validateSavedState` flags as an error, but the rule is re-derived from the schema here
|
|
80
84
|
* rather than read back off the issue list — nothing couples to how sentinel ids are encoded.
|
|
85
|
+
* Valueless inputs are dropped too: a preset saved before an input became a button or a
|
|
86
|
+
* geometry input still holds a value for it.
|
|
81
87
|
*/
|
|
82
88
|
export function extractLoadableValues(savedState, currentSchema) {
|
|
83
89
|
const values = {};
|
|
90
|
+
const valueless = valuelessInputIds(currentSchema);
|
|
84
91
|
for (const paramState of savedState.parameters) {
|
|
85
92
|
const exists = currentSchema.inputs.some((i) => i.id === paramState.paramId);
|
|
86
|
-
if (exists) {
|
|
93
|
+
if (exists && !valueless.has(paramState.paramId)) {
|
|
87
94
|
values[paramState.paramId] = paramState.value;
|
|
88
95
|
}
|
|
89
96
|
}
|
|
@@ -99,17 +106,14 @@ export function loadPreset(savedState, currentSchema) {
|
|
|
99
106
|
canLoad: validation.canLoad
|
|
100
107
|
};
|
|
101
108
|
}
|
|
109
|
+
/** Resolves false when the user cancelled the save. */
|
|
102
110
|
export function exportStateAsJson(savedState) {
|
|
103
111
|
const blob = new Blob([JSON.stringify(savedState, null, 2)], { type: 'application/json' });
|
|
104
112
|
const safeName = savedState.name.replace(/[^a-z0-9]/gi, '_');
|
|
105
113
|
const date = savedState.timestamp.split('T')[0].replace(/-/g, '_');
|
|
106
|
-
const link = document.createElement('a');
|
|
107
|
-
link.href = URL.createObjectURL(blob);
|
|
108
114
|
// .slvp = Selva parameter preset. Import still accepts the pre-rename .sps — same JSON,
|
|
109
115
|
// only the writer's extension changed.
|
|
110
|
-
|
|
111
|
-
link.click();
|
|
112
|
-
URL.revokeObjectURL(link.href);
|
|
116
|
+
return saveBlob(blob, `${safeName}_${date}.slvp`);
|
|
113
117
|
}
|
|
114
118
|
export async function importStateFromJson(file) {
|
|
115
119
|
const state = JSON.parse(await file.text());
|
package/dist/styles/base.css
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
--page-py: 1.75rem;
|
|
8
8
|
--background: oklch(0.99 0 0);
|
|
9
9
|
--foreground: oklch(0.22 0 0);
|
|
10
|
-
|
|
10
|
+
/* White, so a card stands apart from background and muted; Group adds a border, since
|
|
11
|
+
white on background alone is too close to see. */
|
|
12
|
+
--card: oklch(1 0 0);
|
|
11
13
|
--card-foreground: oklch(0.22 0 0);
|
|
12
14
|
--popover: oklch(0.99 0 0);
|
|
13
15
|
--popover-foreground: oklch(0.22 0 0);
|
|
14
|
-
|
|
16
|
+
/* 4.5:1 as text on background, card and muted, and under white text. */
|
|
17
|
+
--primary: oklch(0.52 0.07 135);
|
|
15
18
|
--primary-foreground: oklch(0.985 0 0);
|
|
16
19
|
--secondary: oklch(0.94 0 0);
|
|
17
20
|
--secondary-foreground: oklch(0.28 0 0);
|
|
@@ -40,7 +43,7 @@
|
|
|
40
43
|
--chart-5: oklch(0.68 0.15 30);
|
|
41
44
|
--sidebar: oklch(0.96 0 0);
|
|
42
45
|
--sidebar-foreground: oklch(0.25 0 0);
|
|
43
|
-
--sidebar-primary: oklch(0.
|
|
46
|
+
--sidebar-primary: oklch(0.52 0.07 135);
|
|
44
47
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
45
48
|
--sidebar-accent: oklch(0.93 0 0);
|
|
46
49
|
--sidebar-accent-foreground: oklch(0.3 0 0);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decodeBase64ToBinary, downloadFileDataByRoot } from '@selvajs/compute/core';
|
|
2
2
|
import { SvelteMap } from 'svelte/reactivity';
|
|
3
3
|
import { APP_DEFAULTS } from '../constants';
|
|
4
|
+
import { saveBlob } from './host-files';
|
|
4
5
|
export const MIME_BY_EXT = {
|
|
5
6
|
'.png': 'image/png',
|
|
6
7
|
'.jpg': 'image/jpeg',
|
|
@@ -23,12 +24,7 @@ function saveSingleFile(file) {
|
|
|
23
24
|
const content = file.isBase64Encoded ? decodeBase64ToBinary(file.data) : file.data;
|
|
24
25
|
// decodeBase64ToBinary returns an ArrayBuffer-backed copy, never a SharedArrayBuffer view.
|
|
25
26
|
const blob = new Blob([content], { type: mime });
|
|
26
|
-
|
|
27
|
-
const a = document.createElement('a');
|
|
28
|
-
a.href = URL.createObjectURL(blob);
|
|
29
|
-
a.download = fullName;
|
|
30
|
-
a.click();
|
|
31
|
-
URL.revokeObjectURL(a.href);
|
|
27
|
+
return saveBlob(blob, `${file.fileName}${file.fileType ?? ''}`);
|
|
32
28
|
}
|
|
33
29
|
export async function downloadFiles(fileData, fileName = 'grasshopper-output') {
|
|
34
30
|
try {
|
|
@@ -37,13 +33,13 @@ export async function downloadFiles(fileData, fileName = 'grasshopper-output') {
|
|
|
37
33
|
console.warn('[FileDownload] No files to download');
|
|
38
34
|
return;
|
|
39
35
|
}
|
|
40
|
-
// A lone file is saved as itself rather than zipped
|
|
41
|
-
//
|
|
36
|
+
// A lone file is saved as itself rather than zipped. Everything else, including the
|
|
37
|
+
// per-root archive split, belongs to compute.
|
|
42
38
|
if (filesArray.length === 1) {
|
|
43
|
-
saveSingleFile(filesArray[0]);
|
|
39
|
+
await saveSingleFile(filesArray[0]);
|
|
44
40
|
return;
|
|
45
41
|
}
|
|
46
|
-
await downloadFileDataByRoot(filesArray, fileName);
|
|
42
|
+
await downloadFileDataByRoot(filesArray, fileName, null, { saveBlob });
|
|
47
43
|
}
|
|
48
44
|
catch (error) {
|
|
49
45
|
console.error('[FileDownload] Error downloading files:', error);
|