@selvajs/ui 4.2.0 → 4.4.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/AppLayout.svelte +1 -1
- package/dist/components/compute/AppLayout.svelte.d.ts +1 -1
- package/dist/components/compute/ComputeApp.svelte +54 -133
- package/dist/components/compute/ComputeApp.svelte.d.ts +2 -9
- package/dist/components/compute/ComputeMessagesDialog.svelte +125 -0
- package/dist/components/compute/ComputeMessagesDialog.svelte.d.ts +10 -0
- package/dist/components/compute/ParameterPresetManager.svelte +25 -33
- package/dist/components/layout/PageFooter.svelte +22 -144
- package/dist/components/preview/Group.svelte +6 -3
- package/dist/components/preview/ImageOutput.svelte +1 -13
- package/dist/components/preview/InputControl.svelte +96 -72
- package/dist/components/preview/OutputDisplay.svelte +6 -6
- package/dist/components/preview/TabLayout.svelte +6 -5
- package/dist/components/preview/inputs/FileInput.svelte +10 -19
- package/dist/composables/useFooterItem.svelte.d.ts +10 -12
- package/dist/composables/useFooterItem.svelte.js +9 -16
- package/dist/compute/computeThrottle.svelte.js +5 -2
- package/dist/compute/createSolveSession.svelte.d.ts +56 -0
- package/dist/compute/createSolveSession.svelte.js +122 -0
- package/dist/compute/solve-session-core.d.ts +45 -0
- package/dist/compute/solve-session-core.js +69 -0
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -2
- package/dist/contexts/clientSlotContext.svelte.d.ts +13 -0
- package/dist/contexts/clientSlotContext.svelte.js +8 -0
- package/dist/contexts/footerContext.svelte.d.ts +17 -5
- package/dist/contexts/footerContext.svelte.js +10 -2
- package/dist/external/storage.js +7 -23
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/schema/param-exporter.d.ts +25 -1
- package/dist/schema/param-exporter.js +30 -27
- package/dist/schema/traversal.d.ts +1 -0
- package/dist/schema/traversal.js +5 -0
- package/dist/schema/visibility-rules.d.ts +12 -0
- package/dist/schema/visibility-rules.js +20 -0
- package/dist/types/solveFn.d.ts +1 -2
- package/dist/utils/file-download.d.ts +5 -17
- package/dist/utils/file-download.js +12 -19
- package/dist/utils/randomId.js +3 -19
- package/package.json +10 -26
- package/src/lib/components/compute/AppLayout.svelte +1 -1
- package/src/lib/components/compute/ComputeApp.svelte +54 -133
- package/src/lib/components/compute/ComputeMessagesDialog.svelte +125 -0
- package/src/lib/components/compute/ParameterPresetManager.svelte +25 -33
- package/src/lib/components/layout/PageFooter.svelte +22 -144
- package/src/lib/components/preview/Group.svelte +6 -3
- package/src/lib/components/preview/ImageOutput.svelte +1 -13
- package/src/lib/components/preview/InputControl.svelte +96 -72
- package/src/lib/components/preview/OutputDisplay.svelte +6 -6
- package/src/lib/components/preview/TabLayout.svelte +6 -5
- package/src/lib/components/preview/inputs/FileInput.svelte +10 -19
- package/src/lib/composables/useFooterItem.svelte.ts +25 -25
- package/src/lib/compute/computeThrottle.svelte.ts +5 -2
- package/src/lib/compute/computeThrottle.test.ts +106 -0
- package/src/lib/compute/createSolveSession.svelte.ts +188 -0
- package/src/lib/compute/solve-session-core.test.ts +153 -0
- package/src/lib/compute/solve-session-core.ts +102 -0
- package/src/lib/constants.ts +0 -2
- package/src/lib/contexts/clientSlotContext.svelte.ts +33 -0
- package/src/lib/contexts/footerContext.svelte.ts +29 -14
- package/src/lib/external/storage.test.ts +99 -0
- package/src/lib/external/storage.ts +6 -22
- package/src/lib/index.ts +2 -0
- package/src/lib/schema/param-exporter.test.ts +136 -0
- package/src/lib/schema/param-exporter.ts +44 -30
- package/src/lib/schema/traversal.test.ts +92 -0
- package/src/lib/schema/traversal.ts +5 -0
- package/src/lib/schema/visibility-rules.test.ts +173 -0
- package/src/lib/schema/visibility-rules.ts +25 -0
- package/src/lib/types/solveFn.ts +1 -1
- package/src/lib/utils/file-download.ts +13 -20
- package/src/lib/utils/randomId.ts +3 -19
- package/dist/components/compute/ComputeMessages.svelte +0 -169
- package/dist/components/compute/ComputeMessages.svelte.d.ts +0 -7
- package/dist/components/compute/index.d.ts +0 -5
- package/dist/components/compute/index.js +0 -5
- package/dist/components/preview/index.d.ts +0 -3
- package/dist/components/preview/index.js +0 -4
- package/dist/components/viewer/index.d.ts +0 -3
- package/dist/components/viewer/index.js +0 -3
- package/src/lib/components/compute/ComputeMessages.svelte +0 -169
- package/src/lib/components/compute/index.ts +0 -5
- package/src/lib/components/preview/index.ts +0 -4
- package/src/lib/components/viewer/index.ts +0 -3
|
@@ -7,6 +7,18 @@ export interface VisibilityResult {
|
|
|
7
7
|
export declare function evaluateRule(rule: VisibilityRule, values: Record<string, unknown>): boolean;
|
|
8
8
|
export declare function evaluateCondition(condition: VisibilityCondition | GroupVisibilityCondition, values: Record<string, unknown>): boolean;
|
|
9
9
|
export declare function evaluateVisibility(item: LayoutItem, values: Record<string, unknown>): VisibilityResult;
|
|
10
|
+
/**
|
|
11
|
+
* Stable key for a layout item: `paramId` for inputs/outputs, `id` for linebreaks.
|
|
12
|
+
* Mirrors the key the renderers use in their `{#each}` blocks.
|
|
13
|
+
*/
|
|
14
|
+
export declare function itemKey(item: LayoutItem): string;
|
|
15
|
+
/**
|
|
16
|
+
* Evaluates every item's visibility once against the current values, keyed by itemKey.
|
|
17
|
+
* Callers that touch the same items more than once per render (column layout + cell
|
|
18
|
+
* render, plus the default-value sweep) read from this single map instead of
|
|
19
|
+
* re-evaluating per access — one source of truth for "what's visible right now".
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildVisibilityMap(items: LayoutItem[], values: Record<string, unknown>): Record<string, VisibilityResult>;
|
|
10
22
|
export declare function evaluateGroupVisibility(group: {
|
|
11
23
|
visibilityCondition?: GroupVisibilityCondition;
|
|
12
24
|
}, values: Record<string, unknown>): boolean;
|
|
@@ -57,6 +57,26 @@ export function evaluateVisibility(item, values) {
|
|
|
57
57
|
const actionFn = ACTIONS[action] ?? ACTIONS.show;
|
|
58
58
|
return actionFn(met, defaultValue);
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Stable key for a layout item: `paramId` for inputs/outputs, `id` for linebreaks.
|
|
62
|
+
* Mirrors the key the renderers use in their `{#each}` blocks.
|
|
63
|
+
*/
|
|
64
|
+
export function itemKey(item) {
|
|
65
|
+
return item.type === 'linebreak' ? item.id : item.paramId;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Evaluates every item's visibility once against the current values, keyed by itemKey.
|
|
69
|
+
* Callers that touch the same items more than once per render (column layout + cell
|
|
70
|
+
* render, plus the default-value sweep) read from this single map instead of
|
|
71
|
+
* re-evaluating per access — one source of truth for "what's visible right now".
|
|
72
|
+
*/
|
|
73
|
+
export function buildVisibilityMap(items, values) {
|
|
74
|
+
const map = {};
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
map[itemKey(item)] = evaluateVisibility(item, values);
|
|
77
|
+
}
|
|
78
|
+
return map;
|
|
79
|
+
}
|
|
60
80
|
export function evaluateGroupVisibility(group, values) {
|
|
61
81
|
if (!group.visibilityCondition?.rules)
|
|
62
82
|
return true;
|
package/dist/types/solveFn.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @property errors - Optional array of error messages that occurred
|
|
7
7
|
* @property warnings - Optional array of warning messages from the computation
|
|
8
8
|
*/
|
|
9
|
-
interface SolveResult {
|
|
9
|
+
export interface SolveResult {
|
|
10
10
|
outputs: Record<string, unknown>;
|
|
11
11
|
meshes?: any[];
|
|
12
12
|
errors?: string[];
|
|
@@ -23,4 +23,3 @@ interface SolveResult {
|
|
|
23
23
|
* @returns Promise resolving to the computation result
|
|
24
24
|
*/
|
|
25
25
|
export type SolveFn = (values: Record<string, unknown>, signal: AbortSignal) => Promise<SolveResult>;
|
|
26
|
-
export {};
|
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wrapper utilities for file downloads from Grasshopper outputs
|
|
3
|
-
* Uses the core package implementation for file handling
|
|
4
|
-
*/
|
|
5
1
|
import { type FileData } from '@selvajs/compute';
|
|
6
|
-
|
|
7
|
-
* Download file(s) from Grasshopper outputs
|
|
8
|
-
* Single files are downloaded directly, multiple files are packaged as ZIP
|
|
9
|
-
*/
|
|
2
|
+
export declare const MIME_BY_EXT: Record<string, string>;
|
|
10
3
|
export declare function downloadFiles(fileData: FileData | FileData[], fileName?: string): Promise<void>;
|
|
11
|
-
/**
|
|
12
|
-
* Check if data is FileData
|
|
13
|
-
*/
|
|
14
4
|
export declare function isFileData(data: unknown): data is FileData;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
export declare function groupMessages(messages: string[]): Array<{
|
|
6
|
+
message: string;
|
|
7
|
+
count: number;
|
|
8
|
+
}>;
|
|
18
9
|
export declare function formatFileSize(bytes: number): string;
|
|
19
|
-
/**
|
|
20
|
-
* Get approximate file size from base64 string
|
|
21
|
-
*/
|
|
22
10
|
export declare function getBase64FileSize(base64: string): number;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wrapper utilities for file downloads from Grasshopper outputs
|
|
3
|
-
* Uses the core package implementation for file handling
|
|
4
|
-
*/
|
|
5
1
|
import { downloadFileData } from '@selvajs/compute';
|
|
2
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
6
3
|
import { APP_DEFAULTS } from '../constants';
|
|
7
|
-
const MIME_BY_EXT = {
|
|
4
|
+
export const MIME_BY_EXT = {
|
|
8
5
|
'.png': 'image/png',
|
|
9
6
|
'.jpg': 'image/jpeg',
|
|
10
7
|
'.jpeg': 'image/jpeg',
|
|
@@ -41,10 +38,6 @@ function saveSingleFile(file) {
|
|
|
41
38
|
a.click();
|
|
42
39
|
URL.revokeObjectURL(a.href);
|
|
43
40
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Download file(s) from Grasshopper outputs
|
|
46
|
-
* Single files are downloaded directly, multiple files are packaged as ZIP
|
|
47
|
-
*/
|
|
48
41
|
export async function downloadFiles(fileData, fileName = 'grasshopper-output') {
|
|
49
42
|
try {
|
|
50
43
|
const filesArray = Array.isArray(fileData) ? fileData : [fileData];
|
|
@@ -63,9 +56,6 @@ export async function downloadFiles(fileData, fileName = 'grasshopper-output') {
|
|
|
63
56
|
throw error;
|
|
64
57
|
}
|
|
65
58
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Check if data is FileData
|
|
68
|
-
*/
|
|
69
59
|
export function isFileData(data) {
|
|
70
60
|
return (typeof data === 'object' &&
|
|
71
61
|
data !== null &&
|
|
@@ -73,9 +63,16 @@ export function isFileData(data) {
|
|
|
73
63
|
'data' in data &&
|
|
74
64
|
'isBase64Encoded' in data);
|
|
75
65
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
export function groupMessages(messages) {
|
|
67
|
+
const grouped = new SvelteMap();
|
|
68
|
+
for (const msg of messages) {
|
|
69
|
+
const baseMsg = msg.replace(/\([a-f0-9-]{36}\)$/i, '(...)').trim();
|
|
70
|
+
grouped.set(baseMsg, (grouped.get(baseMsg) ?? 0) + 1);
|
|
71
|
+
}
|
|
72
|
+
return Array.from(grouped.entries())
|
|
73
|
+
.map(([message, count]) => ({ message, count }))
|
|
74
|
+
.sort((a, b) => b.count - a.count);
|
|
75
|
+
}
|
|
79
76
|
export function formatFileSize(bytes) {
|
|
80
77
|
if (bytes === 0)
|
|
81
78
|
return '0 Bytes';
|
|
@@ -84,10 +81,6 @@ export function formatFileSize(bytes) {
|
|
|
84
81
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
85
82
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
86
83
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Get approximate file size from base64 string
|
|
89
|
-
*/
|
|
90
84
|
export function getBase64FileSize(base64) {
|
|
91
|
-
// Base64 encoded string is approximately 33% larger than original
|
|
92
85
|
return Math.ceil((base64.length * 3) / 4);
|
|
93
86
|
}
|
package/dist/utils/randomId.js
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Plain-HTTP deployments — common for first-time self-hosters trying things
|
|
5
|
-
// out — get `crypto.randomUUID is not a function`, which is a confusing
|
|
6
|
-
// browser-level error nobody can diagnose without devtools open.
|
|
7
|
-
//
|
|
8
|
-
// We try in order:
|
|
9
|
-
// 1. crypto.randomUUID() — fastest path, available in secure contexts
|
|
10
|
-
// 2. crypto.getRandomValues() — Web Crypto's lower-level entropy, works
|
|
11
|
-
// in any context that ships Web Crypto
|
|
12
|
-
// 3. Math.random() fallback — last resort; non-cryptographic, but the
|
|
13
|
-
// IDs we generate here are only used as
|
|
14
|
-
// collision-resistant keys for client-side
|
|
15
|
-
// form rows, not as security tokens.
|
|
16
|
-
//
|
|
17
|
-
// Callers that need cryptographic randomness (tokens, secrets) must NOT use
|
|
18
|
-
// this — they should fail loudly in insecure contexts instead of silently
|
|
19
|
-
// downgrading.
|
|
1
|
+
// Falls back through crypto.randomUUID → crypto.getRandomValues → Math.random so
|
|
2
|
+
// plain-HTTP deployments don't crash. Math.random IDs are non-cryptographic —
|
|
3
|
+
// fine for collision-resistant UI keys, not for tokens or secrets.
|
|
20
4
|
export function randomId() {
|
|
21
5
|
const c = typeof crypto !== 'undefined' ? crypto : undefined;
|
|
22
6
|
if (c?.randomUUID)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,29 +14,9 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"svelte": "./dist/index.js"
|
|
16
16
|
},
|
|
17
|
-
"./
|
|
18
|
-
"types": "./dist/
|
|
19
|
-
"svelte": "./dist/
|
|
20
|
-
},
|
|
21
|
-
"./layout": {
|
|
22
|
-
"types": "./dist/components/layout/index.d.ts",
|
|
23
|
-
"svelte": "./dist/components/layout/index.js"
|
|
24
|
-
},
|
|
25
|
-
"./compute": {
|
|
26
|
-
"types": "./dist/components/compute/index.d.ts",
|
|
27
|
-
"svelte": "./dist/components/compute/index.js"
|
|
28
|
-
},
|
|
29
|
-
"./viewer": {
|
|
30
|
-
"types": "./dist/components/viewer/index.d.ts",
|
|
31
|
-
"svelte": "./dist/components/viewer/index.js"
|
|
32
|
-
},
|
|
33
|
-
"./footer": {
|
|
34
|
-
"types": "./dist/contexts/footerContext.svelte.d.ts",
|
|
35
|
-
"svelte": "./dist/contexts/footerContext.svelte.js"
|
|
36
|
-
},
|
|
37
|
-
"./utils": {
|
|
38
|
-
"types": "./dist/utils.d.ts",
|
|
39
|
-
"import": "./dist/utils.js"
|
|
17
|
+
"./external": {
|
|
18
|
+
"types": "./dist/external/storage.d.ts",
|
|
19
|
+
"svelte": "./dist/external/storage.js"
|
|
40
20
|
},
|
|
41
21
|
"./styles/base.css": "./src/lib/styles/base.css",
|
|
42
22
|
"./styles/themes/*": "./src/lib/styles/themes/*"
|
|
@@ -59,7 +39,7 @@
|
|
|
59
39
|
"svelte": "^5",
|
|
60
40
|
"tailwind-variants": "^3.2.2",
|
|
61
41
|
"three": "^0.184.0",
|
|
62
|
-
"@selvajs/schemas": "4.
|
|
42
|
+
"@selvajs/schemas": "^4.4.0"
|
|
63
43
|
},
|
|
64
44
|
"peerDependenciesMeta": {
|
|
65
45
|
"three": {
|
|
@@ -80,11 +60,13 @@
|
|
|
80
60
|
"devDependencies": {
|
|
81
61
|
"@internationalized/date": "^3.12.1",
|
|
82
62
|
"@sveltejs/kit": "2.58.0",
|
|
63
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
83
64
|
"@types/three": "^0.184.0",
|
|
84
65
|
"bits-ui": "^2.18.0",
|
|
85
66
|
"svelte": "5.55.5",
|
|
86
67
|
"tailwind-variants": "^3.2.2",
|
|
87
|
-
"
|
|
68
|
+
"vitest": "^3.2.4",
|
|
69
|
+
"@selvajs/schemas": "4.4.0",
|
|
88
70
|
"@selvajs/config": "0.0.0"
|
|
89
71
|
},
|
|
90
72
|
"scripts": {
|
|
@@ -95,6 +77,8 @@
|
|
|
95
77
|
"preview": "vite preview",
|
|
96
78
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
97
79
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
80
|
+
"test": "vitest run",
|
|
81
|
+
"test:watch": "vitest",
|
|
98
82
|
"format": "prettier --write .",
|
|
99
83
|
"lint": "prettier --check . && eslint ."
|
|
100
84
|
}
|
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
import type { ActionButton } from '../../types/actionButton';
|
|
6
6
|
import type { SolveFn } from '../../types/solveFn';
|
|
7
7
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
8
|
-
import { getDefaultValue } from '../../schema/defaults';
|
|
9
|
-
import { createComputeThrottle } from '../../compute/computeThrottle.svelte';
|
|
10
8
|
import { createSolvingIndicator } from '../../compute/solving.svelte';
|
|
9
|
+
import {
|
|
10
|
+
createSolveSession,
|
|
11
|
+
createRequestResponseDriver
|
|
12
|
+
} from '../../compute/createSolveSession.svelte';
|
|
11
13
|
import { useFooterItem } from '../../composables/useFooterItem.svelte';
|
|
12
14
|
import { hexToOklch } from '../../utils/color';
|
|
13
15
|
import AppShell from '../layout/AppShell.svelte';
|
|
14
16
|
import AppLayout from './AppLayout.svelte';
|
|
15
17
|
import StateDisplay from '../primitives/StateDisplay.svelte';
|
|
16
|
-
import {
|
|
18
|
+
import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
|
|
17
19
|
|
|
18
20
|
import type { Snippet } from 'svelte';
|
|
19
21
|
|
|
@@ -46,17 +48,12 @@
|
|
|
46
48
|
footerItemPriority?: number;
|
|
47
49
|
onReady?: (api: { loadValues: (values: Record<string, unknown>) => void }) => void;
|
|
48
50
|
headerRight?: Snippet;
|
|
49
|
-
|
|
50
|
-
* Bring-your-own header. When provided, replaces the built-in header inside
|
|
51
|
-
* the standard-height sticky bar (so the fixed layout is unaffected).
|
|
52
|
-
* Takes precedence over `headerRight`.
|
|
53
|
-
*/
|
|
51
|
+
// Replaces the built-in header; takes precedence over `headerRight`.
|
|
54
52
|
header?: Snippet;
|
|
55
|
-
|
|
56
|
-
* Stable identifier used to scope sessionStorage entries for external-input
|
|
57
|
-
* values. If absent, falls back to definitionKey, then to schema.id.
|
|
58
|
-
*/
|
|
53
|
+
// Scopes sessionStorage for external-input values; falls back to definitionKey then schema.id.
|
|
59
54
|
externalScopeKey?: string;
|
|
55
|
+
// Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, slotLabel, value }.
|
|
56
|
+
clientSlot?: ClientSlot;
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
let {
|
|
@@ -83,91 +80,36 @@
|
|
|
83
80
|
headerRight,
|
|
84
81
|
header,
|
|
85
82
|
onReady,
|
|
86
|
-
externalScopeKey
|
|
83
|
+
externalScopeKey,
|
|
84
|
+
clientSlot
|
|
87
85
|
}: Props = $props();
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
function createInitialValues(s: UISchema, scopeKey: string) {
|
|
92
|
-
const externalSet = new Set(getExternalInputs(s).map((e) => e.paramId));
|
|
93
|
-
const v: Record<string, unknown> = {};
|
|
94
|
-
for (const input of s.inputs) {
|
|
95
|
-
if (externalSet.has(input.id)) {
|
|
96
|
-
const stored = readExternalValue({ scopeKey, inputId: input.id });
|
|
97
|
-
if (stored !== undefined) v[input.id] = stored;
|
|
98
|
-
// else: leave undefined so the missing-inputs panel can detect it
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
v[input.id] = input.default ?? getDefaultValue(input.paramType);
|
|
102
|
-
}
|
|
103
|
-
for (const output of s.outputs) {
|
|
104
|
-
v[output.id] = null;
|
|
105
|
-
}
|
|
106
|
-
return v;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// svelte-ignore state_referenced_locally
|
|
110
|
-
let values = $state<Record<string, unknown>>(
|
|
111
|
-
createInitialValues(schema, externalScopeKey || definitionKey || schema?.id || '')
|
|
112
|
-
);
|
|
113
|
-
let error = $state('');
|
|
114
|
-
let computeErrors = $state<string[]>([]);
|
|
115
|
-
let computeWarnings = $state<string[]>([]);
|
|
116
|
-
let meshes = $state<any[]>([]);
|
|
117
|
-
let pendingValues = $state<Record<string, unknown>>({});
|
|
87
|
+
// Make the host's client-input slot available to InputControl deep in the tree.
|
|
118
88
|
// svelte-ignore state_referenced_locally
|
|
119
|
-
|
|
120
|
-
// svelte-ignore state_referenced_locally
|
|
121
|
-
let hasNeverSolved = $state(schema?.instanceSolve === false);
|
|
122
|
-
let isViewerFullscreen = $state(false);
|
|
123
|
-
|
|
124
|
-
async function performSolveInternal(solveValues: Record<string, unknown>, signal: AbortSignal) {
|
|
125
|
-
try {
|
|
126
|
-
error = '';
|
|
127
|
-
computeErrors = [];
|
|
128
|
-
computeWarnings = [];
|
|
129
|
-
|
|
130
|
-
const result = await onSolve(solveValues, signal);
|
|
89
|
+
setClientSlot(clientSlot);
|
|
131
90
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
computeErrors = result.errors ?? [];
|
|
135
|
-
computeWarnings = result.warnings ?? [];
|
|
136
|
-
meshes = result.meshes ?? [];
|
|
137
|
-
|
|
138
|
-
Object.assign(values, result.outputs);
|
|
139
|
-
pendingValues = {};
|
|
140
|
-
hasPendingChanges = false;
|
|
141
|
-
hasNeverSolved = false;
|
|
142
|
-
} catch (err) {
|
|
143
|
-
if (err instanceof Error && err.name === 'AbortError') return;
|
|
144
|
-
error = err instanceof Error ? err.message : String(err);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
91
|
+
const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
|
|
147
92
|
|
|
93
|
+
// Solve Session owns the value/lifecycle state machine; the request/response driver
|
|
94
|
+
// gives it its transport (Rhino.Compute over HTTP via onSolve, throttled). The driver
|
|
95
|
+
// reads the reporter lazily so it can capture the session it's wired into.
|
|
148
96
|
// svelte-ignore state_referenced_locally
|
|
149
|
-
const
|
|
97
|
+
const driver = createRequestResponseDriver(onSolve, () => session, {
|
|
150
98
|
timeout: solveTimeoutMs
|
|
151
99
|
});
|
|
100
|
+
// svelte-ignore state_referenced_locally
|
|
101
|
+
const session = createSolveSession({
|
|
102
|
+
schema,
|
|
103
|
+
scopeKey: externalScopeKey || definitionKey || schema?.id || '',
|
|
104
|
+
driver
|
|
105
|
+
});
|
|
152
106
|
|
|
153
|
-
let
|
|
154
|
-
const solvingIndicator = createSolvingIndicator(() => solving);
|
|
107
|
+
let isViewerFullscreen = $state(false);
|
|
155
108
|
|
|
156
|
-
|
|
157
|
-
computeThrottle.trigger($state.snapshot(values));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function loadValues(incoming: Record<string, unknown>) {
|
|
161
|
-
Object.assign(values, incoming);
|
|
162
|
-
if (schema?.instanceSolve !== false) {
|
|
163
|
-
performSolve();
|
|
164
|
-
} else {
|
|
165
|
-
hasPendingChanges = true;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
109
|
+
const solvingIndicator = createSolvingIndicator(() => session.isSolving);
|
|
168
110
|
|
|
169
111
|
$effect(() => {
|
|
170
|
-
onReady?.({ loadValues });
|
|
112
|
+
onReady?.({ loadValues: (incoming) => session.loadValues(incoming) });
|
|
171
113
|
});
|
|
172
114
|
|
|
173
115
|
let previousDefinitionKey = $state('');
|
|
@@ -181,50 +123,35 @@
|
|
|
181
123
|
isInitialLoad = false;
|
|
182
124
|
previousDefinitionKey = definitionKey;
|
|
183
125
|
if (schema?.instanceSolve !== false) {
|
|
184
|
-
|
|
126
|
+
session.solve();
|
|
185
127
|
}
|
|
186
128
|
} else if (previousDefinitionKey !== definitionKey) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
error = '';
|
|
190
|
-
computeErrors = [];
|
|
191
|
-
computeWarnings = [];
|
|
192
|
-
if (schema && Object.keys(values).length > 0) {
|
|
193
|
-
performSolve();
|
|
194
|
-
}
|
|
129
|
+
// Host owns the WHEN (definitionKey changed); session owns the WHAT.
|
|
130
|
+
session.rebuild(schema, resolvedScopeKey);
|
|
195
131
|
previousDefinitionKey = definitionKey;
|
|
196
132
|
}
|
|
197
133
|
});
|
|
198
134
|
});
|
|
199
135
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (schema?.instanceSolve === false) {
|
|
204
|
-
pendingValues[id] = val;
|
|
205
|
-
hasPendingChanges = true;
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
performSolve();
|
|
136
|
+
function handleValueChange(id: string, val: unknown) {
|
|
137
|
+
session.setValue(id, val);
|
|
210
138
|
}
|
|
211
139
|
|
|
212
140
|
function handleCalculate() {
|
|
213
|
-
|
|
141
|
+
session.solve();
|
|
214
142
|
}
|
|
215
143
|
|
|
216
|
-
//
|
|
217
|
-
// footerComponentProps is intentionally
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
144
|
+
// Read static props without creating reactive dependencies (registration is fixed at
|
|
145
|
+
// mount). footerComponentProps is intentionally read live — it's a getter the renderer
|
|
146
|
+
// calls every render to keep the footer in sync. The composable no-ops when component
|
|
147
|
+
// is absent, so the hook itself stays unconditional.
|
|
148
|
+
useFooterItem({
|
|
149
|
+
id: untrack(() => footerItemId),
|
|
150
|
+
component: untrack(() => footerComponent),
|
|
151
|
+
getProps: () => footerComponentProps?.() ?? {},
|
|
152
|
+
position: 'left',
|
|
153
|
+
priority: untrack(() => footerItemPriority)
|
|
154
|
+
});
|
|
228
155
|
|
|
229
156
|
let resolvedIsEmbedded = $derived(isEmbedded ?? page.url.searchParams.get('embed') === 'true');
|
|
230
157
|
let resolvedPrimaryColor = $derived(primaryColor ?? page.url.searchParams.get('primary'));
|
|
@@ -246,12 +173,12 @@
|
|
|
246
173
|
{footerText}
|
|
247
174
|
{header}
|
|
248
175
|
rightContent={headerRight}
|
|
249
|
-
errors={computeErrors}
|
|
250
|
-
warnings={computeWarnings}
|
|
176
|
+
errors={session.computeErrors}
|
|
177
|
+
warnings={session.computeWarnings}
|
|
251
178
|
>
|
|
252
|
-
{#if error}
|
|
179
|
+
{#if session.error}
|
|
253
180
|
<div class="min-h-100 p-8 flex items-center justify-center">
|
|
254
|
-
<StateDisplay type="error" size="medium" message={error} />
|
|
181
|
+
<StateDisplay type="error" size="medium" message={session.error} />
|
|
255
182
|
</div>
|
|
256
183
|
{:else if !schema}
|
|
257
184
|
<div class="min-h-100 flex items-center justify-center">
|
|
@@ -261,13 +188,13 @@
|
|
|
261
188
|
{#key definitionKey}
|
|
262
189
|
<AppLayout
|
|
263
190
|
{schema}
|
|
264
|
-
{meshes}
|
|
265
|
-
isSolving={
|
|
191
|
+
meshes={session.meshes}
|
|
192
|
+
isSolving={session.isSolving}
|
|
266
193
|
showSolvingIndicator={schema.instanceSolve !== false && solvingIndicator.show}
|
|
267
|
-
{hasPendingChanges}
|
|
268
|
-
{hasNeverSolved}
|
|
194
|
+
hasPendingChanges={session.hasPendingChanges}
|
|
195
|
+
hasNeverSolved={session.hasNeverSolved}
|
|
269
196
|
bind:isViewerFullscreen
|
|
270
|
-
|
|
197
|
+
values={session.values}
|
|
271
198
|
{panelActions}
|
|
272
199
|
{showSaveButton}
|
|
273
200
|
{showLoadButton}
|
|
@@ -276,13 +203,7 @@
|
|
|
276
203
|
{presetLabels}
|
|
277
204
|
onValueChange={handleValueChange}
|
|
278
205
|
oncalculate={handleCalculate}
|
|
279
|
-
onLoadValues={() => {
|
|
280
|
-
if (schema?.instanceSolve !== false) {
|
|
281
|
-
performSolve();
|
|
282
|
-
} else {
|
|
283
|
-
hasPendingChanges = true;
|
|
284
|
-
}
|
|
285
|
-
}}
|
|
206
|
+
onLoadValues={() => session.loadValues({})}
|
|
286
207
|
/>
|
|
287
208
|
{/key}
|
|
288
209
|
{/if}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { CircleAlert, TriangleAlert, ChevronDown, ChevronRight } from '@lucide/svelte';
|
|
4
|
+
import * as Collapsible from '$lib/components/primitives/collapsible';
|
|
5
|
+
import * as Dialog from '$lib/components/primitives/dialog';
|
|
6
|
+
import { groupMessages } from '$lib/utils/file-download';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
errors?: string[];
|
|
10
|
+
warnings?: string[];
|
|
11
|
+
/** Renders inside Dialog.Trigger. */
|
|
12
|
+
trigger: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { errors = [], warnings = [], trigger }: Props = $props();
|
|
16
|
+
|
|
17
|
+
let open = $state(false);
|
|
18
|
+
let showErrors = $state(true);
|
|
19
|
+
let showWarnings = $state(false);
|
|
20
|
+
|
|
21
|
+
const groupedErrors = $derived(groupMessages(errors));
|
|
22
|
+
const groupedWarnings = $derived(groupMessages(warnings));
|
|
23
|
+
const totalCount = $derived(errors.length + warnings.length);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<Dialog.Root bind:open>
|
|
27
|
+
<Dialog.Trigger class="focus:outline-none">
|
|
28
|
+
{@render trigger()}
|
|
29
|
+
</Dialog.Trigger>
|
|
30
|
+
|
|
31
|
+
<Dialog.Content class="max-w-2xl max-h-[80vh]">
|
|
32
|
+
<Dialog.Header>
|
|
33
|
+
<Dialog.Title class="gap-2 flex items-center">
|
|
34
|
+
<CircleAlert class="h-5 w-5" />
|
|
35
|
+
Compute Messages
|
|
36
|
+
</Dialog.Title>
|
|
37
|
+
<Dialog.Description>
|
|
38
|
+
{totalCount}
|
|
39
|
+
{totalCount === 1 ? 'issue' : 'issues'} detected during solve
|
|
40
|
+
</Dialog.Description>
|
|
41
|
+
</Dialog.Header>
|
|
42
|
+
|
|
43
|
+
<div class="space-y-3 pr-2 overflow-y-auto" style="max-height: calc(80vh - 180px);">
|
|
44
|
+
{#if errors.length > 0}
|
|
45
|
+
<Collapsible.Root bind:open={showErrors}>
|
|
46
|
+
<div class="overflow-hidden rounded-lg border border-destructive bg-card">
|
|
47
|
+
<div class="px-4 py-3 flex items-center">
|
|
48
|
+
<Collapsible.Trigger
|
|
49
|
+
class="-mx-4 -my-3 gap-3 px-4 py-3 flex flex-1 items-center text-left transition-colors hover:bg-destructive/5"
|
|
50
|
+
>
|
|
51
|
+
{#if showErrors}
|
|
52
|
+
<ChevronDown class="h-4 w-4 shrink-0 text-destructive" />
|
|
53
|
+
{:else}
|
|
54
|
+
<ChevronRight class="h-4 w-4 shrink-0 text-destructive" />
|
|
55
|
+
{/if}
|
|
56
|
+
<CircleAlert class="h-4 w-4 shrink-0 text-destructive" />
|
|
57
|
+
<span class="text-sm font-medium text-destructive">
|
|
58
|
+
{errors.length === 1 ? '1 Error' : `${errors.length} Errors`}
|
|
59
|
+
</span>
|
|
60
|
+
</Collapsible.Trigger>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<Collapsible.Content class="space-y-0">
|
|
64
|
+
<div class="max-h-60 px-4 py-3 overflow-y-auto border-t border-destructive bg-card">
|
|
65
|
+
<ul class="space-y-2">
|
|
66
|
+
{#each groupedErrors as { message, count } (message)}
|
|
67
|
+
<li class="gap-2 text-sm flex text-destructive/90">
|
|
68
|
+
<span class="shrink-0">•</span>
|
|
69
|
+
<span class="flex-1">
|
|
70
|
+
{message}
|
|
71
|
+
{#if count > 1}
|
|
72
|
+
<span class="ml-1 font-medium text-destructive/70">×{count} </span>
|
|
73
|
+
{/if}
|
|
74
|
+
</span>
|
|
75
|
+
</li>
|
|
76
|
+
{/each}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
</Collapsible.Content>
|
|
80
|
+
</div>
|
|
81
|
+
</Collapsible.Root>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
{#if warnings.length > 0}
|
|
85
|
+
<Collapsible.Root bind:open={showWarnings}>
|
|
86
|
+
<div class="overflow-hidden rounded-lg border border-warning/50 bg-card">
|
|
87
|
+
<div class="px-4 py-3 flex items-center">
|
|
88
|
+
<Collapsible.Trigger
|
|
89
|
+
class="-mx-4 -my-3 gap-3 px-4 py-3 flex flex-1 items-center text-left transition-colors hover:bg-warning/5"
|
|
90
|
+
>
|
|
91
|
+
{#if showWarnings}
|
|
92
|
+
<ChevronDown class="h-4 w-4 shrink-0 text-warning" />
|
|
93
|
+
{:else}
|
|
94
|
+
<ChevronRight class="h-4 w-4 shrink-0 text-warning" />
|
|
95
|
+
{/if}
|
|
96
|
+
<TriangleAlert class="h-4 w-4 shrink-0 text-warning" />
|
|
97
|
+
<span class="text-sm font-medium text-warning">
|
|
98
|
+
{warnings.length === 1 ? '1 Warning' : `${warnings.length} Warnings`}
|
|
99
|
+
</span>
|
|
100
|
+
</Collapsible.Trigger>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<Collapsible.Content class="space-y-0">
|
|
104
|
+
<div class="max-h-60 px-4 py-3 overflow-y-auto border-t border-warning/50 bg-card">
|
|
105
|
+
<ul class="space-y-2">
|
|
106
|
+
{#each groupedWarnings as { message, count } (message)}
|
|
107
|
+
<li class="gap-2 text-sm flex text-muted-foreground">
|
|
108
|
+
<span class="shrink-0">•</span>
|
|
109
|
+
<span class="flex-1">
|
|
110
|
+
{message}
|
|
111
|
+
{#if count > 1}
|
|
112
|
+
<span class="ml-1 font-medium text-warning/70">×{count}</span>
|
|
113
|
+
{/if}
|
|
114
|
+
</span>
|
|
115
|
+
</li>
|
|
116
|
+
{/each}
|
|
117
|
+
</ul>
|
|
118
|
+
</div>
|
|
119
|
+
</Collapsible.Content>
|
|
120
|
+
</div>
|
|
121
|
+
</Collapsible.Root>
|
|
122
|
+
{/if}
|
|
123
|
+
</div>
|
|
124
|
+
</Dialog.Content>
|
|
125
|
+
</Dialog.Root>
|