@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
|
@@ -1,33 +1,26 @@
|
|
|
1
1
|
import { getContext, onMount, onDestroy } from 'svelte';
|
|
2
2
|
import { FOOTER_CONTEXT_KEY } from '../contexts/footerContext.svelte';
|
|
3
3
|
/**
|
|
4
|
-
* Register a footer item
|
|
5
|
-
* The `getProps` function is called on every render, so returning reactive state
|
|
6
|
-
* (e.g. `() => ({ status: myState.status })`) will keep the footer in sync.
|
|
4
|
+
* Register a footer item for the lifetime of the calling component.
|
|
7
5
|
*
|
|
8
|
-
* Registration is deferred to onMount (client-side only) to avoid SSR context errors
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param component - Svelte component to render
|
|
12
|
-
* @param getProps - Function returning current props (called reactively)
|
|
13
|
-
* @param position - Where to display ('left' or 'right'), defaults to 'left'
|
|
14
|
-
* @param priority - Higher values render first within their position
|
|
15
|
-
* @param onClick - Optional click handler (e.g. open a popup/dialog)
|
|
6
|
+
* Registration is deferred to onMount (client-side only) to avoid SSR context errors;
|
|
7
|
+
* it's unregistered on destroy. A no-op when no footer context is present (e.g. a
|
|
8
|
+
* component rendered outside the root layout) or no component is supplied.
|
|
16
9
|
*/
|
|
17
|
-
export function useFooterItem(
|
|
10
|
+
export function useFooterItem(config) {
|
|
18
11
|
onMount(() => {
|
|
19
12
|
const store = getContext(FOOTER_CONTEXT_KEY);
|
|
20
|
-
if (!store || !component)
|
|
13
|
+
if (!store || !config.component)
|
|
21
14
|
return;
|
|
22
|
-
store.register(
|
|
15
|
+
store.register(config);
|
|
23
16
|
});
|
|
24
17
|
onDestroy(() => {
|
|
25
18
|
try {
|
|
26
19
|
const store = getContext(FOOTER_CONTEXT_KEY);
|
|
27
|
-
store?.unregister(id);
|
|
20
|
+
store?.unregister(config.id);
|
|
28
21
|
}
|
|
29
22
|
catch {
|
|
30
|
-
// Context may not exist during SSR cleanup — safe to ignore
|
|
23
|
+
// Context may not exist during SSR cleanup — safe to ignore.
|
|
31
24
|
}
|
|
32
25
|
});
|
|
33
26
|
}
|
|
@@ -30,8 +30,11 @@ export function createComputeThrottle(computeFn, options = {}) {
|
|
|
30
30
|
await computeFn(values, signal);
|
|
31
31
|
}
|
|
32
32
|
catch (err) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// AbortError is expected (timeout or cancel). Non-abort errors must be
|
|
34
|
+
// handled inside computeFn — re-throwing here would produce an unhandled
|
|
35
|
+
// rejection because executeCompute is always called fire-and-forget.
|
|
36
|
+
if (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {
|
|
37
|
+
console.error('[computeThrottle] unhandled error in computeFn:', err);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
finally {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { UISchema } from '@selvajs/schemas';
|
|
2
|
+
import type { SolveFn, SolveResult } from '../types/solveFn';
|
|
3
|
+
/**
|
|
4
|
+
* The transport behind a Solve Session. Knows how to start and cancel a solve and
|
|
5
|
+
* reports its in-flight state. It does NOT return outputs — those come back via the
|
|
6
|
+
* session's report() so push transports (WebSocket) fit without contortion.
|
|
7
|
+
*/
|
|
8
|
+
export interface SolveDriver {
|
|
9
|
+
solve(values: Record<string, unknown>): void;
|
|
10
|
+
cancel(): void;
|
|
11
|
+
readonly isSolving: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SolveSession {
|
|
14
|
+
readonly values: Record<string, unknown>;
|
|
15
|
+
readonly error: string;
|
|
16
|
+
readonly computeErrors: string[];
|
|
17
|
+
readonly computeWarnings: string[];
|
|
18
|
+
readonly meshes: unknown[];
|
|
19
|
+
readonly hasPendingChanges: boolean;
|
|
20
|
+
readonly hasNeverSolved: boolean;
|
|
21
|
+
readonly isSolving: boolean;
|
|
22
|
+
/** Records a value change and dispatches a solve unless the schema is manual-solve. */
|
|
23
|
+
setValue(id: string, value: unknown): void;
|
|
24
|
+
/** Explicit "calculate" — dispatches a solve with the current values. */
|
|
25
|
+
solve(): void;
|
|
26
|
+
/** Merges incoming values, then solves (auto) or marks dirty (manual). */
|
|
27
|
+
loadValues(incoming: Record<string, unknown>): void;
|
|
28
|
+
/** Re-seed for a new active definition: rebuild values, clear outputs, gate the solve. */
|
|
29
|
+
rebuild(schema: UISchema, scopeKey: string): void;
|
|
30
|
+
/** Feed a completed solve result back into the session (called by the driver/host). */
|
|
31
|
+
report(result: SolveResult): void;
|
|
32
|
+
/** Report a solve failure (transport/solver error) — surfaces in `error`. */
|
|
33
|
+
reportError(message: string): void;
|
|
34
|
+
}
|
|
35
|
+
export interface SolveSessionArgs {
|
|
36
|
+
schema: UISchema;
|
|
37
|
+
scopeKey: string;
|
|
38
|
+
driver: SolveDriver;
|
|
39
|
+
}
|
|
40
|
+
export declare function createSolveSession(args: SolveSessionArgs): SolveSession;
|
|
41
|
+
/** The slice of a SolveSession a driver feeds completed/failed solves back into. */
|
|
42
|
+
export interface SolveReporter {
|
|
43
|
+
report(result: SolveResult): void;
|
|
44
|
+
reportError(message: string): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Request/response Solve Driver: wraps createComputeThrottle around a SolveFn and feeds
|
|
48
|
+
* the resolved result back through the reporter. One solve in flight at a time; the
|
|
49
|
+
* latest triggered values win. Used by ComputeApp (Rhino.Compute over HTTP).
|
|
50
|
+
*
|
|
51
|
+
* Because the session and driver reference each other, the host passes the reporter
|
|
52
|
+
* lazily (`() => session`) so it can construct the session with the driver in hand.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createRequestResponseDriver(onSolve: SolveFn, getReporter: () => SolveReporter, options?: {
|
|
55
|
+
timeout?: number;
|
|
56
|
+
}): SolveDriver;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Reactive Solve Session (see CONTEXT.md): the $state-backed shell over the pure
|
|
2
|
+
// transition logic in solve-session-core.ts. It owns the live values/flags, delegates
|
|
3
|
+
// every transition to the core, and drives solves through a transport-agnostic
|
|
4
|
+
// SolveDriver. A completed solve re-enters via report().
|
|
5
|
+
import { readExternalValue } from '../external/storage';
|
|
6
|
+
import { createComputeThrottle } from './computeThrottle.svelte';
|
|
7
|
+
import { buildInitialValues, makeInitialFlags, applyValueChange, applySolveResult } from './solve-session-core';
|
|
8
|
+
export function createSolveSession(args) {
|
|
9
|
+
let currentSchema = args.schema;
|
|
10
|
+
const flags = makeInitialFlags(currentSchema?.instanceSolve);
|
|
11
|
+
const state = $state({
|
|
12
|
+
values: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),
|
|
13
|
+
error: '',
|
|
14
|
+
computeErrors: [],
|
|
15
|
+
computeWarnings: [],
|
|
16
|
+
meshes: [],
|
|
17
|
+
pendingValues: {},
|
|
18
|
+
hasPendingChanges: flags.hasPendingChanges,
|
|
19
|
+
hasNeverSolved: flags.hasNeverSolved
|
|
20
|
+
});
|
|
21
|
+
function dispatch() {
|
|
22
|
+
args.driver.solve($state.snapshot(state.values));
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
get values() {
|
|
26
|
+
return state.values;
|
|
27
|
+
},
|
|
28
|
+
get error() {
|
|
29
|
+
return state.error;
|
|
30
|
+
},
|
|
31
|
+
get computeErrors() {
|
|
32
|
+
return state.computeErrors;
|
|
33
|
+
},
|
|
34
|
+
get computeWarnings() {
|
|
35
|
+
return state.computeWarnings;
|
|
36
|
+
},
|
|
37
|
+
get meshes() {
|
|
38
|
+
return state.meshes;
|
|
39
|
+
},
|
|
40
|
+
get hasPendingChanges() {
|
|
41
|
+
return state.hasPendingChanges;
|
|
42
|
+
},
|
|
43
|
+
get hasNeverSolved() {
|
|
44
|
+
return state.hasNeverSolved;
|
|
45
|
+
},
|
|
46
|
+
get isSolving() {
|
|
47
|
+
return args.driver.isSolving;
|
|
48
|
+
},
|
|
49
|
+
setValue(id, value) {
|
|
50
|
+
const { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);
|
|
51
|
+
if (shouldSolve)
|
|
52
|
+
dispatch();
|
|
53
|
+
},
|
|
54
|
+
solve() {
|
|
55
|
+
dispatch();
|
|
56
|
+
},
|
|
57
|
+
loadValues(incoming) {
|
|
58
|
+
Object.assign(state.values, incoming);
|
|
59
|
+
if (currentSchema?.instanceSolve !== false) {
|
|
60
|
+
dispatch();
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
state.hasPendingChanges = true;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
rebuild(schema, scopeKey) {
|
|
67
|
+
currentSchema = schema;
|
|
68
|
+
state.meshes = [];
|
|
69
|
+
state.error = '';
|
|
70
|
+
state.computeErrors = [];
|
|
71
|
+
state.computeWarnings = [];
|
|
72
|
+
state.pendingValues = {};
|
|
73
|
+
state.values = buildInitialValues(schema, scopeKey, readExternalValue);
|
|
74
|
+
const f = makeInitialFlags(schema?.instanceSolve);
|
|
75
|
+
state.hasPendingChanges = f.hasPendingChanges;
|
|
76
|
+
state.hasNeverSolved = f.hasNeverSolved;
|
|
77
|
+
if (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {
|
|
78
|
+
dispatch();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
report(result) {
|
|
82
|
+
applySolveResult(state, result);
|
|
83
|
+
},
|
|
84
|
+
reportError(message) {
|
|
85
|
+
state.error = message;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Request/response Solve Driver: wraps createComputeThrottle around a SolveFn and feeds
|
|
91
|
+
* the resolved result back through the reporter. One solve in flight at a time; the
|
|
92
|
+
* latest triggered values win. Used by ComputeApp (Rhino.Compute over HTTP).
|
|
93
|
+
*
|
|
94
|
+
* Because the session and driver reference each other, the host passes the reporter
|
|
95
|
+
* lazily (`() => session`) so it can construct the session with the driver in hand.
|
|
96
|
+
*/
|
|
97
|
+
export function createRequestResponseDriver(onSolve, getReporter, options = {}) {
|
|
98
|
+
const throttle = createComputeThrottle(async (values, signal) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await onSolve(values, signal);
|
|
101
|
+
if (signal.aborted)
|
|
102
|
+
return;
|
|
103
|
+
getReporter().report(result);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (signal.aborted)
|
|
107
|
+
return;
|
|
108
|
+
getReporter().reportError(err instanceof Error ? err.message : String(err));
|
|
109
|
+
}
|
|
110
|
+
}, options);
|
|
111
|
+
return {
|
|
112
|
+
solve(values) {
|
|
113
|
+
throttle.trigger(values);
|
|
114
|
+
},
|
|
115
|
+
cancel() {
|
|
116
|
+
throttle.cancel();
|
|
117
|
+
},
|
|
118
|
+
get isSolving() {
|
|
119
|
+
return throttle.isComputing;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { UISchema } from '@selvajs/schemas';
|
|
2
|
+
import { type ExternalValueRef } from '../external/storage';
|
|
3
|
+
import type { SolveResult } from '../types/solveFn';
|
|
4
|
+
export interface SolveSessionState {
|
|
5
|
+
values: Record<string, unknown>;
|
|
6
|
+
error: string;
|
|
7
|
+
computeErrors: string[];
|
|
8
|
+
computeWarnings: string[];
|
|
9
|
+
meshes: unknown[];
|
|
10
|
+
/** Values changed since the last solve, in manual (instanceSolve === false) mode. */
|
|
11
|
+
pendingValues: Record<string, unknown>;
|
|
12
|
+
hasPendingChanges: boolean;
|
|
13
|
+
hasNeverSolved: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Reads a previously produced client-sourced value, or undefined if absent. */
|
|
16
|
+
export type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Builds the initial `values` map for a schema. Non-client inputs get their declared
|
|
19
|
+
* default (falling back to the paramType default); client-sourced inputs are hydrated
|
|
20
|
+
* from `read` and left absent when no stored value exists so the missing-inputs panel
|
|
21
|
+
* can detect them. Outputs are seeded to null.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildInitialValues(schema: UISchema, scopeKey: string, read: ExternalReader): Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Initial lifecycle flags. Manual-solve schemas (instanceSolve === false) start dirty
|
|
26
|
+
* so the user must explicitly calculate; auto-solve schemas start clean.
|
|
27
|
+
*/
|
|
28
|
+
export declare function makeInitialFlags(instanceSolve: boolean | undefined): {
|
|
29
|
+
hasPendingChanges: boolean;
|
|
30
|
+
hasNeverSolved: boolean;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Records a single value change and decides whether it should dispatch a solve now.
|
|
34
|
+
* Auto-solve mode dispatches immediately; manual mode defers (records the pending value
|
|
35
|
+
* and raises the dirty flag) and never dispatches.
|
|
36
|
+
*/
|
|
37
|
+
export declare function applyValueChange(state: SolveSessionState, id: string, value: unknown, instanceSolve: boolean | undefined): {
|
|
38
|
+
state: SolveSessionState;
|
|
39
|
+
shouldSolve: boolean;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Merges a reported solve result into the state and clears the post-solve lifecycle
|
|
43
|
+
* flags. Missing result arrays are treated as empty.
|
|
44
|
+
*/
|
|
45
|
+
export declare function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Pure, framework-free transition logic for a Solve Session (see CONTEXT.md).
|
|
2
|
+
//
|
|
3
|
+
// This module holds the value/lifecycle state machine with no Svelte runes and no
|
|
4
|
+
// transport: how values are seeded (including client-sourced hydration), when a value
|
|
5
|
+
// change should dispatch a solve vs. defer it, and what a reported solve result does to
|
|
6
|
+
// the flags. The reactive wrapper in createSolveSession.svelte.ts is a thin shell over
|
|
7
|
+
// these functions; everything testable lives here.
|
|
8
|
+
import { getDefaultValue } from '../schema/defaults';
|
|
9
|
+
import { getExternalInputs } from '../external/storage';
|
|
10
|
+
/**
|
|
11
|
+
* Builds the initial `values` map for a schema. Non-client inputs get their declared
|
|
12
|
+
* default (falling back to the paramType default); client-sourced inputs are hydrated
|
|
13
|
+
* from `read` and left absent when no stored value exists so the missing-inputs panel
|
|
14
|
+
* can detect them. Outputs are seeded to null.
|
|
15
|
+
*/
|
|
16
|
+
export function buildInitialValues(schema, scopeKey, read) {
|
|
17
|
+
const clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));
|
|
18
|
+
const values = {};
|
|
19
|
+
for (const input of schema.inputs) {
|
|
20
|
+
if (clientSet.has(input.id)) {
|
|
21
|
+
const stored = read({ scopeKey, inputId: input.id });
|
|
22
|
+
if (stored !== undefined)
|
|
23
|
+
values[input.id] = stored;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
values[input.id] = input.default ?? getDefaultValue(input.paramType);
|
|
27
|
+
}
|
|
28
|
+
for (const output of schema.outputs) {
|
|
29
|
+
values[output.id] = null;
|
|
30
|
+
}
|
|
31
|
+
return values;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Initial lifecycle flags. Manual-solve schemas (instanceSolve === false) start dirty
|
|
35
|
+
* so the user must explicitly calculate; auto-solve schemas start clean.
|
|
36
|
+
*/
|
|
37
|
+
export function makeInitialFlags(instanceSolve) {
|
|
38
|
+
const manual = instanceSolve === false;
|
|
39
|
+
return { hasPendingChanges: manual, hasNeverSolved: manual };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Records a single value change and decides whether it should dispatch a solve now.
|
|
43
|
+
* Auto-solve mode dispatches immediately; manual mode defers (records the pending value
|
|
44
|
+
* and raises the dirty flag) and never dispatches.
|
|
45
|
+
*/
|
|
46
|
+
export function applyValueChange(state, id, value, instanceSolve) {
|
|
47
|
+
state.values[id] = value;
|
|
48
|
+
if (instanceSolve === false) {
|
|
49
|
+
state.pendingValues[id] = value;
|
|
50
|
+
state.hasPendingChanges = true;
|
|
51
|
+
return { state, shouldSolve: false };
|
|
52
|
+
}
|
|
53
|
+
return { state, shouldSolve: true };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Merges a reported solve result into the state and clears the post-solve lifecycle
|
|
57
|
+
* flags. Missing result arrays are treated as empty.
|
|
58
|
+
*/
|
|
59
|
+
export function applySolveResult(state, result) {
|
|
60
|
+
state.error = '';
|
|
61
|
+
state.computeErrors = result.errors ?? [];
|
|
62
|
+
state.computeWarnings = result.warnings ?? [];
|
|
63
|
+
state.meshes = result.meshes ?? [];
|
|
64
|
+
Object.assign(state.values, result.outputs);
|
|
65
|
+
state.pendingValues = {};
|
|
66
|
+
state.hasPendingChanges = false;
|
|
67
|
+
state.hasNeverSolved = false;
|
|
68
|
+
return state;
|
|
69
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,8 +6,6 @@ export declare const APP_DEFAULTS: {
|
|
|
6
6
|
TIMEOUTS: {
|
|
7
7
|
/** Duration to show solving state (ms) */
|
|
8
8
|
SOLVE_STATE_DURATION: number;
|
|
9
|
-
/** Parameter export callback delay (ms) */
|
|
10
|
-
PARAM_EXPORT_DELAY: number;
|
|
11
9
|
/** Drawer open/close animation duration (ms) — matches the CSS transition. */
|
|
12
10
|
DRAWER_ANIMATION_MS: number;
|
|
13
11
|
};
|
package/dist/constants.js
CHANGED
|
@@ -8,8 +8,6 @@ export const APP_DEFAULTS = {
|
|
|
8
8
|
TIMEOUTS: {
|
|
9
9
|
/** Duration to show solving state (ms) */
|
|
10
10
|
SOLVE_STATE_DURATION: 3500,
|
|
11
|
-
/** Parameter export callback delay (ms) */
|
|
12
|
-
PARAM_EXPORT_DELAY: 100,
|
|
13
11
|
/** Drawer open/close animation duration (ms) — matches the CSS transition. */
|
|
14
12
|
DRAWER_ANIMATION_MS: 350
|
|
15
13
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export interface ClientSlotArgs {
|
|
3
|
+
/** Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id). */
|
|
4
|
+
inputId: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
/** Author-set label from the schema, passed through untouched. May be undefined. */
|
|
7
|
+
slotLabel?: string;
|
|
8
|
+
/** The current value held for this input (e.g. the prefilled JSON), if any. */
|
|
9
|
+
value: unknown;
|
|
10
|
+
}
|
|
11
|
+
export type ClientSlot = Snippet<[ClientSlotArgs]>;
|
|
12
|
+
export declare function setClientSlot(slot: ClientSlot | undefined): void;
|
|
13
|
+
export declare function getClientSlot(): ClientSlot | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const CLIENT_SLOT_CONTEXT_KEY = Symbol('client-slot-context');
|
|
3
|
+
export function setClientSlot(slot) {
|
|
4
|
+
setContext(CLIENT_SLOT_CONTEXT_KEY, slot);
|
|
5
|
+
}
|
|
6
|
+
export function getClientSlot() {
|
|
7
|
+
return getContext(CLIENT_SLOT_CONTEXT_KEY);
|
|
8
|
+
}
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { type Component } from 'svelte';
|
|
1
2
|
import { SvelteMap } from 'svelte/reactivity';
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A footer item registered by a descendant of the root layout. `component` + `position`
|
|
5
|
+
* are fixed at registration; the renderer re-invokes `getProps` reactively, so return
|
|
6
|
+
* reactive state from it (e.g. `() => ({ status: myState.status })`) to keep the footer
|
|
7
|
+
* in sync.
|
|
8
|
+
*
|
|
9
|
+
* Generic over the component's props `P` so registration is type-checked at the call
|
|
10
|
+
* site. The store holds a heterogeneous mix of items, so its map widens `P` to
|
|
11
|
+
* `Record<string, unknown>` at the boundary.
|
|
12
|
+
*/
|
|
13
|
+
export interface FooterItem<P extends Record<string, unknown> = Record<string, unknown>> {
|
|
3
14
|
id: string;
|
|
4
|
-
component:
|
|
5
|
-
/** Called on every render — return reactive state to keep footer in sync */
|
|
6
|
-
getProps: () =>
|
|
15
|
+
component: Component<P>;
|
|
16
|
+
/** Called on every render — return reactive state to keep footer in sync. */
|
|
17
|
+
getProps: () => P;
|
|
7
18
|
position: 'left' | 'right';
|
|
8
19
|
priority: number;
|
|
9
20
|
onClick?: () => void;
|
|
10
21
|
}
|
|
11
22
|
export interface FooterStore {
|
|
12
23
|
items: SvelteMap<string, FooterItem>;
|
|
13
|
-
|
|
24
|
+
/** Register (or replace) a footer item. `position`/`priority` default to 'left'/0. */
|
|
25
|
+
register<P extends Record<string, unknown>>(item: Omit<FooterItem<P>, 'position' | 'priority'> & Partial<Pick<FooterItem<P>, 'position' | 'priority'>>): void;
|
|
14
26
|
unregister(id: string): void;
|
|
15
27
|
}
|
|
16
28
|
export declare const FOOTER_CONTEXT_KEY: unique symbol;
|
|
@@ -5,8 +5,16 @@ export function initializeFooterContext() {
|
|
|
5
5
|
const items = new SvelteMap();
|
|
6
6
|
const store = {
|
|
7
7
|
items,
|
|
8
|
-
register(
|
|
9
|
-
|
|
8
|
+
register(item) {
|
|
9
|
+
const { id, component, getProps, position = 'left', priority = 0, onClick } = item;
|
|
10
|
+
items.set(id, {
|
|
11
|
+
id,
|
|
12
|
+
component: component,
|
|
13
|
+
getProps: getProps,
|
|
14
|
+
position,
|
|
15
|
+
priority,
|
|
16
|
+
onClick
|
|
17
|
+
});
|
|
10
18
|
},
|
|
11
19
|
unregister(id) {
|
|
12
20
|
items.delete(id);
|
package/dist/external/storage.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// definition guid in selva/library, etc.
|
|
8
8
|
//
|
|
9
9
|
// inputId is the Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id).
|
|
10
|
+
import { getInputItems } from '@selvajs/schemas';
|
|
10
11
|
const STORAGE_PREFIX = 'external';
|
|
11
12
|
function makeKey(scopeKey, inputId) {
|
|
12
13
|
return `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;
|
|
@@ -43,28 +44,11 @@ export function clearExternalValue(ref) {
|
|
|
43
44
|
return;
|
|
44
45
|
sessionStorage.removeItem(makeKey(scopeKey, inputId));
|
|
45
46
|
}
|
|
46
|
-
function* walkLayoutItems(schema) {
|
|
47
|
-
const groups = schema.layout.type === 'tabbed'
|
|
48
|
-
? schema.layout.tabs.flatMap((t) => t.groups)
|
|
49
|
-
: schema.layout.groups;
|
|
50
|
-
for (const group of groups) {
|
|
51
|
-
for (const item of group.items) {
|
|
52
|
-
yield item;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
47
|
export function getExternalInputs(schema) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
continue;
|
|
64
|
-
result.push({
|
|
65
|
-
paramId: item.paramId,
|
|
66
|
-
displayName: item.displayName ?? item.paramId
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
48
|
+
return getInputItems(schema)
|
|
49
|
+
.filter((item) => item.source?.kind === 'client')
|
|
50
|
+
.map((item) => ({
|
|
51
|
+
paramId: item.paramId,
|
|
52
|
+
displayName: item.displayName ?? item.paramId
|
|
53
|
+
}));
|
|
70
54
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ export * from './components/primitives';
|
|
|
6
6
|
export { default as StateDisplay } from './components/primitives/StateDisplay.svelte';
|
|
7
7
|
export { default as Viewer } from './components/viewer/Viewer.svelte';
|
|
8
8
|
export * from './schema/defaults';
|
|
9
|
+
export * from './schema/traversal';
|
|
9
10
|
export * from './compute/solving.svelte';
|
|
10
11
|
export * from './external/storage';
|
|
11
12
|
export * from './contexts/footerContext.svelte';
|
|
13
|
+
export * from './contexts/clientSlotContext.svelte';
|
|
12
14
|
export * from './composables/useFooterItem.svelte';
|
|
13
15
|
export * from './utils';
|
|
14
16
|
export { randomId } from './utils/randomId';
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,13 @@ export { default as StateDisplay } from './components/primitives/StateDisplay.sv
|
|
|
12
12
|
export { default as Viewer } from './components/viewer/Viewer.svelte';
|
|
13
13
|
// Utilities
|
|
14
14
|
export * from './schema/defaults';
|
|
15
|
+
export * from './schema/traversal';
|
|
15
16
|
export * from './compute/solving.svelte';
|
|
16
17
|
// External-input transit storage (used by routes that wire pre-step producers)
|
|
17
18
|
export * from './external/storage';
|
|
18
19
|
// Contexts & Composables
|
|
19
20
|
export * from './contexts/footerContext.svelte';
|
|
21
|
+
export * from './contexts/clientSlotContext.svelte';
|
|
20
22
|
export * from './composables/useFooterItem.svelte';
|
|
21
23
|
// Utils (cn function)
|
|
22
24
|
export * from './utils';
|
|
@@ -10,6 +10,30 @@ export declare function validateSavedState(savedState: ParameterPreset, currentS
|
|
|
10
10
|
issues: ValidationIssueMessage[];
|
|
11
11
|
canLoad: boolean;
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The subset of a preset's parameters that can safely be applied to the current schema:
|
|
15
|
+
* every param that still exists as an input. A param that no longer exists is dropped
|
|
16
|
+
* (it's the same condition `validateSavedState` flags as an error), so this derives its
|
|
17
|
+
* own "loadable" rule from the schema rather than re-reading validation issues — no
|
|
18
|
+
* coupling to how sentinel ids are encoded.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractLoadableValues(savedState: ParameterPreset, currentSchema: UISchema): Record<string, unknown>;
|
|
21
|
+
/** The full outcome of loading a preset: what to apply, plus the diagnostics to surface. */
|
|
22
|
+
export interface PresetLoadResult {
|
|
23
|
+
/** Values safe to apply now (params that still exist). */
|
|
24
|
+
values: Record<string, unknown>;
|
|
25
|
+
/** All validation issues (errors + warnings) for the load dialog. */
|
|
26
|
+
issues: ValidationIssueMessage[];
|
|
27
|
+
/** No issues at all — load silently. */
|
|
28
|
+
isValid: boolean;
|
|
29
|
+
/** No blocking errors — load is allowed (warnings are fine). */
|
|
30
|
+
canLoad: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Single entry point for applying a preset: validates against the current schema and, in
|
|
34
|
+
* the same pass, computes the loadable values. Callers get one object instead of
|
|
35
|
+
* threading a validation result back into a separate extraction step.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadPreset(savedState: ParameterPreset, currentSchema: UISchema): PresetLoadResult;
|
|
14
38
|
export declare function exportStateAsJson(savedState: ParameterPreset): void;
|
|
15
39
|
export declare function importStateFromJson(file: File): Promise<ParameterPreset>;
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import { APP_DEFAULTS } from '../constants';
|
|
2
1
|
import { randomId } from '../utils/randomId';
|
|
3
|
-
|
|
4
|
-
if (!schema.layout)
|
|
5
|
-
return [];
|
|
6
|
-
if (schema.layout.type === 'tabbed')
|
|
7
|
-
return schema.layout.tabs.flatMap((t) => t.groups ?? []);
|
|
8
|
-
if (schema.layout.type === 'flat')
|
|
9
|
-
return schema.layout.groups ?? [];
|
|
10
|
-
return [];
|
|
11
|
-
}
|
|
2
|
+
import { getGroups } from '@selvajs/schemas';
|
|
12
3
|
export function createSavedState(schema, currentValues, metadata) {
|
|
13
4
|
const parameters = [];
|
|
14
5
|
for (const group of getGroups(schema)) {
|
|
@@ -83,34 +74,46 @@ export function validateSavedState(savedState, currentSchema) {
|
|
|
83
74
|
const hasErrors = issues.some((i) => i.severity === 'error');
|
|
84
75
|
return { isValid: issues.length === 0, issues, canLoad: !hasErrors };
|
|
85
76
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The subset of a preset's parameters that can safely be applied to the current schema:
|
|
79
|
+
* every param that still exists as an input. A param that no longer exists is dropped
|
|
80
|
+
* (it's the same condition `validateSavedState` flags as an error), so this derives its
|
|
81
|
+
* own "loadable" rule from the schema rather than re-reading validation issues — no
|
|
82
|
+
* coupling to how sentinel ids are encoded.
|
|
83
|
+
*/
|
|
84
|
+
export function extractLoadableValues(savedState, currentSchema) {
|
|
90
85
|
const values = {};
|
|
91
86
|
for (const paramState of savedState.parameters) {
|
|
92
87
|
const exists = currentSchema.inputs.some((i) => i.id === paramState.paramId);
|
|
93
|
-
if (
|
|
88
|
+
if (exists) {
|
|
94
89
|
values[paramState.paramId] = paramState.value;
|
|
95
90
|
}
|
|
96
91
|
}
|
|
97
92
|
return values;
|
|
98
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Single entry point for applying a preset: validates against the current schema and, in
|
|
96
|
+
* the same pass, computes the loadable values. Callers get one object instead of
|
|
97
|
+
* threading a validation result back into a separate extraction step.
|
|
98
|
+
*/
|
|
99
|
+
export function loadPreset(savedState, currentSchema) {
|
|
100
|
+
const validation = validateSavedState(savedState, currentSchema);
|
|
101
|
+
return {
|
|
102
|
+
values: extractLoadableValues(savedState, currentSchema),
|
|
103
|
+
issues: validation.issues,
|
|
104
|
+
isValid: validation.isValid,
|
|
105
|
+
canLoad: validation.canLoad
|
|
106
|
+
};
|
|
107
|
+
}
|
|
99
108
|
export function exportStateAsJson(savedState) {
|
|
100
109
|
const blob = new Blob([JSON.stringify(savedState, null, 2)], { type: 'application/json' });
|
|
101
|
-
const
|
|
110
|
+
const safeName = savedState.name.replace(/[^a-z0-9]/gi, '_');
|
|
111
|
+
const date = savedState.timestamp.split('T')[0].replace(/-/g, '_');
|
|
102
112
|
const link = document.createElement('a');
|
|
103
|
-
link.href =
|
|
104
|
-
link.download = `${
|
|
105
|
-
link.
|
|
106
|
-
|
|
107
|
-
setTimeout(() => {
|
|
108
|
-
link.click();
|
|
109
|
-
setTimeout(() => {
|
|
110
|
-
document.body.removeChild(link);
|
|
111
|
-
URL.revokeObjectURL(url);
|
|
112
|
-
}, APP_DEFAULTS.TIMEOUTS.PARAM_EXPORT_DELAY);
|
|
113
|
-
}, 0);
|
|
113
|
+
link.href = URL.createObjectURL(blob);
|
|
114
|
+
link.download = `${safeName}_${date}.sps`;
|
|
115
|
+
link.click();
|
|
116
|
+
URL.revokeObjectURL(link.href);
|
|
114
117
|
}
|
|
115
118
|
export async function importStateFromJson(file) {
|
|
116
119
|
const state = JSON.parse(await file.text());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getGroups, getLayoutItems, getInputItems, type InputLayoutItem } from '@selvajs/schemas';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Schema layout traversal now lives in @selvajs/schemas (next to the types it walks) so
|
|
2
|
+
// any package depending on the schema can traverse it without pulling in @selvajs/ui.
|
|
3
|
+
// Re-exported here to keep it part of @selvajs/ui's published surface for existing
|
|
4
|
+
// consumers.
|
|
5
|
+
export { getGroups, getLayoutItems, getInputItems } from '@selvajs/schemas';
|