feat: add active node settings

This commit is contained in:
max_richter 2024-04-23 19:14:27 +02:00
parent 6ea4afa012
commit 198a868fc6
14 changed files with 293 additions and 148 deletions

View File

@ -5,6 +5,7 @@ import EventEmitter from "./helpers/EventEmitter.js";
import throttle from "./helpers/throttle.js"; import throttle from "./helpers/throttle.js";
import { createLogger } from "./helpers/index.js"; import { createLogger } from "./helpers/index.js";
import type { NodeInput } from "@nodes/types"; import type { NodeInput } from "@nodes/types";
import { fastHashString } from "@nodes/utils";
const logger = createLogger("graph-manager"); const logger = createLogger("graph-manager");
@ -70,7 +71,13 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
private lastSettingsHash = 0;
setSettings(settings: Record<string, unknown>) { setSettings(settings: Record<string, unknown>) {
let hash = fastHashString(JSON.stringify(settings));
if (hash === this.lastSettingsHash) return;
this.lastSettingsHash = hash;
this.settings = settings; this.settings = settings;
this.save(); this.save();
this.execute(); this.execute();

View File

@ -39,10 +39,11 @@
const edges = graph.edges; const edges = graph.edges;
let wrapper: HTMLDivElement; let wrapper: HTMLDivElement;
let rect: DOMRect;
$: rect = $: rect =
wrapper && width wrapper && width
? wrapper.getBoundingClientRect() ? wrapper.getBoundingClientRect()
: { x: 0, y: 0, width: 0, height: 0 }; : ({ x: 0, y: 0, width: 0, height: 0 } as DOMRect);
let camera: OrthographicCamera; let camera: OrthographicCamera;
const minZoom = 1; const minZoom = 1;
@ -121,10 +122,13 @@
const height = const height =
5 + 5 +
10 * 10 *
Object.keys(node.inputs) Object.keys(node.inputs).filter(
.filter((i) => i !== "seed") (p) =>
.filter((p) => node?.inputs && !("setting" in node?.inputs?.[p])) p !== "seed" &&
.length; node?.inputs &&
!("setting" in node?.inputs?.[p]) &&
node.inputs[p].hidden !== true,
).length;
nodeHeightCache[nodeTypeId] = height; nodeHeightCache[nodeTypeId] = height;
return height; return height;
} }
@ -756,7 +760,8 @@
clickedNodeId === -1 && clickedNodeId === -1 &&
!boxSelection && !boxSelection &&
cameraDown[0] === cameraPosition[0] && cameraDown[0] === cameraPosition[0] &&
cameraDown[1] === cameraPosition[1] cameraDown[1] === cameraPosition[1] &&
isBodyFocused()
) { ) {
$activeNodeId = -1; $activeNodeId = -1;
$selectedNodes?.clear(); $selectedNodes?.clear();
@ -817,7 +822,6 @@
function handleDragEnter(e: DragEvent) { function handleDragEnter(e: DragEvent) {
e.preventDefault(); e.preventDefault();
isDragging = true; isDragging = true;
console.log(e);
} }
function handlerDragOver(e: DragEvent) { function handlerDragOver(e: DragEvent) {
@ -840,7 +844,7 @@
}); });
</script> </script>
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} /> <svelte:window on:mousemove={handleMouseMove} />
<div <div
on:wheel={handleMouseScroll} on:wheel={handleMouseScroll}
@ -851,6 +855,7 @@
tabindex="0" tabindex="0"
bind:clientWidth={width} bind:clientWidth={width}
bind:clientHeight={height} bind:clientHeight={height}
on:mouseup={handleMouseUp}
on:dragenter={handleDragEnter} on:dragenter={handleDragEnter}
on:dragover={handlerDragOver} on:dragover={handlerDragOver}
on:drop={handleDrop} on:drop={handleDrop}

View File

@ -1,17 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { Graph, NodeRegistry } from "@nodes/types"; import type { Graph, Node, NodeRegistry } from "@nodes/types";
import GraphEl from "./Graph.svelte"; import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.js"; import { GraphManager } from "../graph-manager.js";
import { createEventDispatcher, setContext } from "svelte"; import { createEventDispatcher, setContext } from "svelte";
import type { Writable } from "svelte/store"; import { type Writable } from "svelte/store";
import { debounce } from "$lib/helpers"; import { debounce } from "$lib/helpers";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import { activeNodeId } from "./stores";
export let registry: NodeRegistry; export let registry: NodeRegistry;
export let graph: Graph; export let graph: Graph;
export let settings: Writable<Record<string, any>> | undefined; export let settings: Writable<Record<string, any>> | undefined;
export const manager = new GraphManager(registry); export const manager = new GraphManager(registry);
export let activeNode: Node | undefined;
$: if ($activeNodeId !== -1) {
activeNode = manager.getNode($activeNodeId);
} else {
activeNode = undefined;
}
export const status = manager.status; export const status = manager.status;

View File

@ -3,6 +3,7 @@
import NodeHeader from "./NodeHeader.svelte"; import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte"; import NodeParameter from "./NodeParameter.svelte";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import Page from "../../../routes/+page.svelte";
export let isActive = false; export let isActive = false;
export let isSelected = false; export let isSelected = false;
export let inView = true; export let inView = true;
@ -17,9 +18,10 @@
const type = node?.tmp?.type; const type = node?.tmp?.type;
const parameters = Object.entries(type?.inputs || {}) const parameters = Object.entries(type?.inputs || {}).filter(
.filter((p) => p[1].type !== "seed") (p) =>
.filter((p) => !("setting" in p[1])); p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true,
);
const updateNodePosition = const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition"); getContext<(n: Node) => void>("updateNodePosition");

View File

@ -0,0 +1,71 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "./NestedSettings.svelte";
import { writable } from "svelte/store";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
function filterInputs(inputs: Record<string, NodeInput>) {
return Object.fromEntries(
Object.entries(inputs).filter(([key, value]) => {
return value.hidden === true;
}),
);
}
function createStore(
props: Node["props"],
inputs: Record<string, NodeInput>,
) {
const store = {};
Object.keys(inputs).forEach((key) => {
store[key] = props[key] || inputs[key].value;
});
console.log({ store, props });
return writable(store);
}
export let manager: GraphManager;
export let node: Node;
let nodeDefinition: Record<string, NodeInput> | undefined;
$: nodeDefinition = node?.tmp?.type
? filterInputs(node.tmp.type.inputs)
: undefined;
$: store = node ? createStore(node.props, nodeDefinition) : undefined;
function updateNode() {
if (!node || !$store) return;
Object.keys($store).forEach((_key: string) => {
node.props = node.props || {};
const key = _key as keyof typeof $store;
let needsUpdate = false;
console.log({ key });
if (
node &&
$store &&
key in node.props &&
node.props[key] !== $store[key]
) {
needsUpdate = true;
node.props[key] = $store[key];
}
if (needsUpdate) {
manager.execute();
}
});
}
$: if (store && $store) {
updateNode();
}
</script>
{#if node}
{#if nodeDefinition && store && Object.keys(nodeDefinition).length > 0}
<NestedSettings id="activeNodeSettings" settings={nodeDefinition} {store} />
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{:else}
<p class="mx-4">No active node</p>
{/if}

View File

@ -10,6 +10,8 @@
{ {
icon: string; icon: string;
id: string; id: string;
hidden?: boolean;
props?: Record<string, unknown>;
component?: typeof SvelteComponent<{}, {}, {}>; component?: typeof SvelteComponent<{}, {}, {}>;
definition: Record<string, NodeInput>; definition: Record<string, NodeInput>;
settings: Writable<Record<string, unknown>>; settings: Writable<Record<string, unknown>>;
@ -22,7 +24,7 @@
); );
$: keys = panels $: keys = panels
? (Object.keys(panels) as unknown as (keyof typeof panels)[]).filter( ? (Object.keys(panels) as unknown as (keyof typeof panels)[]).filter(
(key) => !!panels[key]?.id, (key) => !!panels[key]?.id && panels[key]?.hidden !== true,
) )
: []; : [];
@ -79,7 +81,7 @@
{/each} {/each}
</div> </div>
<div class="content"> <div class="content">
{#if $activePanel && panels[$activePanel]} {#if $activePanel && panels[$activePanel] && panels[$activePanel].hidden !== true}
<h1 class="m-0 p-4">{panels[$activePanel].id}</h1> <h1 class="m-0 p-4">{panels[$activePanel].id}</h1>
{#key $activePanel} {#key $activePanel}
{#if panels[$activePanel]?.component} {#if panels[$activePanel]?.component}

View File

@ -4,11 +4,11 @@
import { MemoryRuntimeExecutor } from "$lib/runtime-executor"; import { MemoryRuntimeExecutor } from "$lib/runtime-executor";
import { RemoteNodeRegistry } from "$lib/node-registry-client"; import { RemoteNodeRegistry } from "$lib/node-registry-client";
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph } from "@nodes/types"; import type { Graph, Node } from "@nodes/types";
import Viewer from "$lib/result-viewer/Viewer.svelte"; import Viewer from "$lib/result-viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte"; import Settings from "$lib/settings/Settings.svelte";
import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings"; import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Readable, type Writable } from "svelte/store";
import Keymap from "$lib/settings/Keymap.svelte"; import Keymap from "$lib/settings/Keymap.svelte";
import type { createKeyMap } from "$lib/helpers/createKeyMap"; import type { createKeyMap } from "$lib/helpers/createKeyMap";
import NodeStore from "$lib/node-store/NodeStore.svelte"; import NodeStore from "$lib/node-store/NodeStore.svelte";
@ -17,7 +17,7 @@
import { decodeNestedArray, encodeNestedArray } from "@nodes/utils"; import { decodeNestedArray, encodeNestedArray } from "@nodes/utils";
import type { PerspectiveCamera, Vector3 } from "three"; import type { PerspectiveCamera, Vector3 } from "three";
import type { OrbitControls } from "three/examples/jsm/Addons.js"; import type { OrbitControls } from "three/examples/jsm/Addons.js";
import GraphView from "$lib/graph-interface/graph/GraphView.svelte"; import ActiveNode from "$lib/settings/ActiveNode.svelte";
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry("");
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry); const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
@ -29,6 +29,7 @@
let viewerCamera: PerspectiveCamera; let viewerCamera: PerspectiveCamera;
let viewerControls: OrbitControls; let viewerControls: OrbitControls;
let viewerCenter: Vector3; let viewerCenter: Vector3;
let activeNode: Node | undefined;
let graph = localStorage.getItem("graph") let graph = localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
@ -65,6 +66,12 @@
shortcuts: {}, shortcuts: {},
nodeStore: {}, nodeStore: {},
graph: {}, graph: {},
activeNode: {
id: "Active Node",
icon: "i-tabler-adjustments",
props: { node: undefined, manager },
component: ActiveNode,
},
}; };
$: if (keymap) { $: if (keymap) {
@ -79,6 +86,7 @@
} }
$: if (manager) { $: if (manager) {
settings.activeNode.props.manager = manager;
settings.nodeStore = { settings.nodeStore = {
id: "Node Store", id: "Node Store",
icon: "i-tabler-database", icon: "i-tabler-database",
@ -88,6 +96,14 @@
settings = settings; settings = settings;
} }
$: if (activeNode) {
settings.activeNode.props.node = activeNode;
settings = settings;
} else {
settings.activeNode.props.node = undefined;
settings = settings;
}
function handleSettings( function handleSettings(
ev: CustomEvent<{ ev: CustomEvent<{
values: Record<string, unknown>; values: Record<string, unknown>;
@ -107,7 +123,7 @@
}; };
settings.graph = { settings.graph = {
icon: "i-tabler-chart-bar", icon: "i-tabler-git-fork",
id: "graph", id: "graph",
settings: writable(ev.detail.values), settings: writable(ev.detail.values),
definition: { definition: {
@ -139,6 +155,7 @@
{#key graph} {#key graph}
<GraphInterface <GraphInterface
bind:manager bind:manager
bind:activeNode
registry={nodeRegistry} registry={nodeRegistry}
{graph} {graph}
bind:keymap bind:keymap

View File

@ -17,14 +17,14 @@
"min": 0.1, "min": 0.1,
"max": 100 "max": 100
}, },
"seed": {
"type": "seed"
},
"fixBottom": { "fixBottom": {
"type": "float", "type": "float",
"hidden": true, "hidden": true,
"min": 0, "min": 0,
"max": 1 "max": 1
},
"seed": {
"type": "seed"
} }
} }
} }

View File

@ -7,6 +7,10 @@ use wasm_bindgen::prelude::*;
include_definition_file!("src/input.json"); include_definition_file!("src/input.json");
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + t * (b - a)
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook(); set_panic_hook();
@ -16,10 +20,23 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let plants = get_args(args[0]); let plants = get_args(args[0]);
let scale = evaluate_float(args[1]); let scale = evaluate_float(args[1]);
let strength = evaluate_float(args[2]); let strength = evaluate_float(args[2]);
let seed = args[3][0]; let _fix_bottom = evaluate_float(args[3]);
let fix_bottom = if _fix_bottom.is_finite() {
_fix_bottom
} else {
0.0
};
let seed = args[4][0];
let hasher = PermutationTable::new(seed as u32); let hasher = PermutationTable::new(seed as u32);
log!("scale: {}, strength: {}, seed: {}", scale, strength, seed); log!(
"scale: {}, strength: {}, fix_bottom: {}, seed: {}",
scale,
strength,
fix_bottom,
seed
);
let output: Vec<Vec<i32>> = plants let output: Vec<Vec<i32>> = plants
.iter() .iter()
@ -33,8 +50,14 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let a = i as f64 / (points - 1) as f64; let a = i as f64 / (points - 1) as f64;
let px = Vector2::new(0.0, a * scale as f64); let px = Vector2::new(0.0, a * scale as f64);
let pz = Vector2::new(a * scale as f64, 0.0); let pz = Vector2::new(a * scale as f64, 0.0);
let nx = open_simplex_2d(px, &hasher) as f32 * strength * 0.1 * a as f32; let nx = open_simplex_2d(px, &hasher) as f32
let nz = open_simplex_2d(pz, &hasher) as f32 * strength * 0.1 * a as f32; * strength
* 0.1
* lerp(1.0, a as f32, fix_bottom);
let nz = open_simplex_2d(pz, &hasher) as f32
* strength
* 0.1
* lerp(1.0, a as f32, fix_bottom);
plant[3 + i * 4] = encode_float(decode_float(plant[3 + i * 4]) + nx); plant[3 + i * 4] = encode_float(decode_float(plant[3 + i * 4]) + nx);
plant[5 + i * 4] = encode_float(decode_float(plant[5 + i * 4]) + nz); plant[5 + i * 4] = encode_float(decode_float(plant[5 + i * 4]) + nz);
} }

View File

@ -3,10 +3,7 @@
"outputs": [], "outputs": [],
"inputs": { "inputs": {
"input": { "input": {
"type": [ "type": "plant",
"plant",
"model"
],
"external": true "external": true
}, },
"resolution_circle": { "resolution_circle": {

View File

@ -49,8 +49,12 @@ pub fn include_definition_file(input: TokenStream) -> TokenStream {
}); });
// Optionally, validate that the content is valid JSON // Optionally, validate that the content is valid JSON
let _: NodeDefinition = serde_json::from_str(&json_content) let _: NodeDefinition = serde_json::from_str(&json_content).unwrap_or_else(|err| {
.unwrap_or_else(|err| panic!("JSON file contains invalid JSON: {}", err)); panic!(
"JSON file contains invalid JSON: \n {} \n {}",
json_content, err
)
});
// Generate the function that returns the JSON string // Generate the function that returns the JSON string
let expanded = quote! { let expanded = quote! {

View File

@ -7,6 +7,7 @@ const DefaultOptionsSchema = z.object({
label: z.string().optional(), label: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
accepts: z.array(z.string()).optional(), accepts: z.array(z.string()).optional(),
hidden: z.boolean().optional(),
}); });
@ -38,6 +39,7 @@ export const NodeInputBooleanSchema = z.object({
export const NodeInputSelectSchema = z.object({ export const NodeInputSelectSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("select"), type: z.literal("select"),
options: z.array(z.string()).optional(),
value: z.number().optional(), value: z.number().optional(),
}); });
@ -75,4 +77,4 @@ export const NodeInputSchema = z.union([
NodeInputPlantSchema NodeInputPlantSchema
]); ]);
export type NodeInput = z.infer<typeof InputSchema>; export type NodeInput = z.infer<typeof NodeInputSchema>;

View File

@ -1,135 +1,142 @@
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize)]
pub struct DefaultOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub internal: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub setting: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accepts: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hidden: Option<bool>,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputFloat {
#[serde(flatten)]
pub default_options: DefaultOptions,
#[serde(skip_serializing_if = "Option::is_none")]
pub element: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step: Option<f64>,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputInteger {
#[serde(flatten)]
pub default_options: DefaultOptions,
#[serde(skip_serializing_if = "Option::is_none")]
pub element: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputBoolean {
#[serde(flatten)]
pub default_options: DefaultOptions,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<bool>,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputSelect {
#[serde(flatten)]
pub default_options: DefaultOptions,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<HashMap<String, Value>>,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputSeed {
#[serde(flatten)]
pub default_options: DefaultOptions,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<i32>,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputVec3 {
#[serde(flatten)]
pub default_options: DefaultOptions,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<Vec<f64>>,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputModel {
#[serde(flatten)]
pub default_options: DefaultOptions,
}
#[derive(Serialize, Deserialize)]
pub struct NodeInputPlant {
#[serde(flatten)]
pub default_options: DefaultOptions,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum InputTypes { pub enum NodeInput {
float(NodeInputFloat), float(NodeInputFloat),
integer(NodeInputInteger), integer(NodeInputInteger),
boolean(NodeInputBoolean), boolean(NodeInputBoolean),
select(NodeInputSelect), select(NodeInputSelect),
seed(NodeInputSeed), seed(NodeInputSeed),
vec3(NodeInputVec3),
model(NodeInputModel), model(NodeInputModel),
plant(NodeInputPlant), plant(NodeInputPlant),
vec3(NodeInputVec3),
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize)]
pub struct NodeInputVec3 {
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<Vec<f64>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInputFloat {
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step: Option<f64>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInputInteger {
#[serde(skip_serializing_if = "Option::is_none")]
pub element: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<i64>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInputBoolean {
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInputSelect {
pub options: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<usize>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInputSeed {
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<usize>,
}
// Assuming similar structure as other NodeInput types for Model and Plant
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInputModel {
// Model-specific fields can be added here
}
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInputPlant {
// Plant-specific fields can be added here
}
#[derive(Serialize, Deserialize, Debug)]
pub struct DefaultOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub internal: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub setting: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<serde_json::Value>, // To handle both String and false
#[serde(skip_serializing_if = "Option::is_none")]
pub hidden: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum NodeDefinitionOrArray {
Single(InputTypes),
Multiple(Vec<String>),
}
#[derive(Debug, Serialize)]
pub struct NodeInput {
pub types: Vec<String>,
pub options: DefaultOptions,
}
impl<'de> Deserialize<'de> for NodeInput {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw_input: Value = Deserialize::deserialize(deserializer)?;
let options: DefaultOptions =
DefaultOptions::deserialize(&raw_input).map_err(serde::de::Error::custom)?; // Maps deserialization errors appropriately
let types: Vec<String> = match raw_input.get("type") {
Some(Value::String(single_type)) => vec![single_type.clone()],
Some(Value::Array(types)) => types
.iter()
.map(|t| t.as_str().unwrap_or("").to_owned())
.collect(),
_ => return Err(serde::de::Error::custom("Invalid or missing 'type' field")),
};
Ok(NodeInput { types, options })
}
}
#[derive(Deserialize, Debug, Serialize)]
pub struct NodeDefinition { pub struct NodeDefinition {
pub id: String, pub id: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<HashMap<String, NodeInput>>, pub inputs: Option<HashMap<String, NodeInput>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Vec<String>>, pub outputs: Option<Vec<String>>,
} }

View File

@ -42,6 +42,8 @@
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
inputEl.focus();
isMouseDown = true; isMouseDown = true;
downV = value; downV = value;