@selvajs/ui 4.3.0 → 4.5.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.
Files changed (80) hide show
  1. package/dist/components/compute/AppLayout.svelte +3 -3
  2. package/dist/components/compute/AppLayout.svelte.d.ts +2 -2
  3. package/dist/components/compute/ComputeApp.svelte +47 -140
  4. package/dist/components/compute/ComputeApp.svelte.d.ts +0 -16
  5. package/dist/components/compute/ComputeMessagesDialog.svelte +125 -0
  6. package/dist/components/compute/ComputeMessagesDialog.svelte.d.ts +10 -0
  7. package/dist/components/compute/ParameterPresetManager.svelte +25 -33
  8. package/dist/components/layout/PageFooter.svelte +22 -144
  9. package/dist/components/preview/Group.svelte +6 -3
  10. package/dist/components/preview/ImageOutput.svelte +1 -13
  11. package/dist/components/preview/OutputDisplay.svelte +6 -6
  12. package/dist/components/preview/TabLayout.svelte +6 -5
  13. package/dist/components/preview/inputs/FileInput.svelte +10 -19
  14. package/dist/composables/useFooterItem.svelte.d.ts +10 -12
  15. package/dist/composables/useFooterItem.svelte.js +9 -16
  16. package/dist/compute/computeThrottle.svelte.js +5 -2
  17. package/dist/compute/createSolveSession.svelte.d.ts +56 -0
  18. package/dist/compute/createSolveSession.svelte.js +122 -0
  19. package/dist/compute/solve-session-core.d.ts +45 -0
  20. package/dist/compute/solve-session-core.js +69 -0
  21. package/dist/constants.d.ts +0 -2
  22. package/dist/constants.js +0 -2
  23. package/dist/contexts/footerContext.svelte.d.ts +17 -5
  24. package/dist/contexts/footerContext.svelte.js +10 -2
  25. package/dist/external/storage.js +7 -23
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +5 -0
  28. package/dist/schema/param-exporter.d.ts +25 -1
  29. package/dist/schema/param-exporter.js +30 -27
  30. package/dist/schema/traversal.d.ts +1 -0
  31. package/dist/schema/traversal.js +5 -0
  32. package/dist/schema/visibility-rules.d.ts +12 -0
  33. package/dist/schema/visibility-rules.js +20 -0
  34. package/dist/types/solveFn.d.ts +1 -2
  35. package/dist/utils/file-download.d.ts +5 -17
  36. package/dist/utils/file-download.js +12 -19
  37. package/dist/utils/randomId.js +3 -19
  38. package/package.json +10 -26
  39. package/src/lib/components/compute/AppLayout.svelte +3 -3
  40. package/src/lib/components/compute/ComputeApp.svelte +47 -140
  41. package/src/lib/components/compute/ComputeMessagesDialog.svelte +125 -0
  42. package/src/lib/components/compute/ParameterPresetManager.svelte +25 -33
  43. package/src/lib/components/layout/PageFooter.svelte +22 -144
  44. package/src/lib/components/preview/Group.svelte +6 -3
  45. package/src/lib/components/preview/ImageOutput.svelte +1 -13
  46. package/src/lib/components/preview/OutputDisplay.svelte +6 -6
  47. package/src/lib/components/preview/TabLayout.svelte +6 -5
  48. package/src/lib/components/preview/inputs/FileInput.svelte +10 -19
  49. package/src/lib/composables/useFooterItem.svelte.ts +25 -25
  50. package/src/lib/compute/computeThrottle.svelte.ts +5 -2
  51. package/src/lib/compute/computeThrottle.test.ts +106 -0
  52. package/src/lib/compute/createSolveSession.svelte.ts +188 -0
  53. package/src/lib/compute/solve-session-core.test.ts +153 -0
  54. package/src/lib/compute/solve-session-core.ts +102 -0
  55. package/src/lib/constants.ts +0 -2
  56. package/src/lib/contexts/footerContext.svelte.ts +29 -14
  57. package/src/lib/external/storage.test.ts +99 -0
  58. package/src/lib/external/storage.ts +6 -22
  59. package/src/lib/index.ts +14 -1
  60. package/src/lib/schema/param-exporter.test.ts +136 -0
  61. package/src/lib/schema/param-exporter.ts +44 -30
  62. package/src/lib/schema/traversal.test.ts +92 -0
  63. package/src/lib/schema/traversal.ts +5 -0
  64. package/src/lib/schema/visibility-rules.test.ts +173 -0
  65. package/src/lib/schema/visibility-rules.ts +25 -0
  66. package/src/lib/types/solveFn.ts +1 -1
  67. package/src/lib/utils/file-download.ts +13 -20
  68. package/src/lib/utils/randomId.ts +3 -19
  69. package/dist/components/compute/ComputeMessages.svelte +0 -169
  70. package/dist/components/compute/ComputeMessages.svelte.d.ts +0 -7
  71. package/dist/components/compute/index.d.ts +0 -5
  72. package/dist/components/compute/index.js +0 -5
  73. package/dist/components/preview/index.d.ts +0 -3
  74. package/dist/components/preview/index.js +0 -4
  75. package/dist/components/viewer/index.d.ts +0 -3
  76. package/dist/components/viewer/index.js +0 -3
  77. package/src/lib/components/compute/ComputeMessages.svelte +0 -169
  78. package/src/lib/components/compute/index.ts +0 -5
  79. package/src/lib/components/preview/index.ts +0 -4
  80. package/src/lib/components/viewer/index.ts +0 -3
@@ -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
+ }
@@ -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
  },
@@ -1,16 +1,28 @@
1
+ import { type Component } from 'svelte';
1
2
  import { SvelteMap } from 'svelte/reactivity';
2
- export interface FooterItem {
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: any;
5
- /** Called on every render — return reactive state to keep footer in sync */
6
- getProps: () => Record<string, any>;
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
- register(id: string, component: any, getProps: () => Record<string, any>, position?: 'left' | 'right', priority?: number, onClick?: () => void): void;
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(id, component, getProps, position = 'left', priority = 0, onClick) {
9
- items.set(id, { id, component, getProps, position, priority, onClick });
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);
@@ -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
- const result = [];
58
- for (const item of walkLayoutItems(schema)) {
59
- if (item.type !== 'input')
60
- continue;
61
- const source = item.source;
62
- if (source?.kind !== 'client')
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,7 +6,9 @@ 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';
11
+ export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveDriver, type SolveReporter } from './compute/createSolveSession.svelte';
10
12
  export * from './external/storage';
11
13
  export * from './contexts/footerContext.svelte';
12
14
  export * from './contexts/clientSlotContext.svelte';
@@ -14,5 +16,5 @@ export * from './composables/useFooterItem.svelte';
14
16
  export * from './utils';
15
17
  export { randomId } from './utils/randomId';
16
18
  export type { ActionButton } from './types/actionButton';
17
- export type { SolveFn } from './types/solveFn';
19
+ export type { SolveFn, SolveResult } from './types/solveFn';
18
20
  export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
package/dist/index.js CHANGED
@@ -12,7 +12,12 @@ 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';
17
+ // Solve Session seam (transport-agnostic value/lifecycle state machine + its driver
18
+ // interface). Exported so transports outside this package — e.g. plugin-ui's WebSocket
19
+ // driver — can satisfy SolveDriver and drive a session. See CONTEXT.md.
20
+ export { createSolveSession, createRequestResponseDriver } from './compute/createSolveSession.svelte';
16
21
  // External-input transit storage (used by routes that wire pre-step producers)
17
22
  export * from './external/storage';
18
23
  // Contexts & Composables
@@ -10,6 +10,30 @@ export declare function validateSavedState(savedState: ParameterPreset, currentS
10
10
  issues: ValidationIssueMessage[];
11
11
  canLoad: boolean;
12
12
  };
13
- export declare function extractLoadableValues(savedState: ParameterPreset, currentSchema: UISchema, validation: ReturnType<typeof validateSavedState>): Record<string, unknown>;
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
- function getGroups(schema) {
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
- export function extractLoadableValues(savedState, currentSchema, validation) {
87
- const errorParamIds = new Set(validation.issues
88
- .filter((i) => i.severity === 'error' && !i.paramId.startsWith('__'))
89
- .map((i) => i.paramId));
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 (!errorParamIds.has(paramState.paramId) && exists) {
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 url = URL.createObjectURL(blob);
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 = url;
104
- link.download = `${savedState.name.replace(/[^a-z0-9]/gi, '_')}_${savedState.timestamp.split('T')[0].replace(/-/g, '_')}.sps`;
105
- link.style.display = 'none';
106
- document.body.appendChild(link);
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';
@@ -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;
@@ -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
- * Format file size for display
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;