From a001d9341284df8ea1ecae15e23e13bed8b5c99f Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Wed, 1 Mar 2023 16:18:57 +0100 Subject: [PATCH] Added Derive Seed node (#1611) * Added Random Seed node * Added migration * Rebrand to Derive Seed * Made seeds their own datatype * Added test for migration --- .../external_stable_diffusion/img2img.py | 11 +- .../external_stable_diffusion/inpainting.py | 11 +- .../external_stable_diffusion/outpainting.py | 11 +- .../external_stable_diffusion/txt2img.py | 11 +- backend/src/nodes/nodes/image/create_noise.py | 32 +- .../src/nodes/nodes/image_filter/add_noise.py | 19 +- .../src/nodes/nodes/utility/derive_seed.py | 68 ++ .../src/nodes/nodes/utility/random_number.py | 27 +- .../nodes/properties/inputs/generic_inputs.py | 31 + .../properties/outputs/generic_outputs.py | 9 + backend/src/nodes/utils/seed.py | 32 + src/common/migrations.ts | 112 ++++ src/common/types/chainner-scope.ts | 2 + src/common/types/function.ts | 108 ++-- .../__snapshots__/SaveFile.test.ts.snap | 588 ++++++++++++++++++ tests/data/add noise with seed edge.chn | 1 + tests/data/rnd.chn | 1 + 17 files changed, 950 insertions(+), 124 deletions(-) create mode 100644 backend/src/nodes/nodes/utility/derive_seed.py create mode 100644 backend/src/nodes/utils/seed.py create mode 100644 tests/data/add noise with seed edge.chn create mode 100644 tests/data/rnd.chn diff --git a/backend/src/nodes/nodes/external_stable_diffusion/img2img.py b/backend/src/nodes/nodes/external_stable_diffusion/img2img.py index 3b87261c9..8db16c645 100644 --- a/backend/src/nodes/nodes/external_stable_diffusion/img2img.py +++ b/backend/src/nodes/nodes/external_stable_diffusion/img2img.py @@ -23,11 +23,12 @@ BoolInput, EnumInput, ImageInput, - NumberInput, + SeedInput, SliderInput, TextAreaInput, ) from ...properties.outputs import ImageOutput +from ...utils.seed import Seed from ...utils.utils import get_h_w_c from . import category as ExternalStableDiffusionCategory @@ -52,9 +53,7 @@ def __init__(self): controls_step=0.1, precision=2, ), - group("seed")( - NumberInput("Seed", minimum=0, default=42, maximum=4294967296) - ), + group("seed")(SeedInput()), SliderInput("Steps", minimum=1, default=20, maximum=150), EnumInput( SamplerName, @@ -115,7 +114,7 @@ def run( prompt: Optional[str], negative_prompt: Optional[str], denoising_strength: float, - seed: int, + seed: Seed, steps: int, sampler_name: SamplerName, cfg_scale: float, @@ -132,7 +131,7 @@ def run( "prompt": prompt or "", "negative_prompt": negative_prompt or "", "denoising_strength": denoising_strength, - "seed": seed, + "seed": seed.to_u32(), "steps": steps, "sampler_name": sampler_name.value, "cfg_scale": cfg_scale, diff --git a/backend/src/nodes/nodes/external_stable_diffusion/inpainting.py b/backend/src/nodes/nodes/external_stable_diffusion/inpainting.py index 47963497a..acd874e37 100644 --- a/backend/src/nodes/nodes/external_stable_diffusion/inpainting.py +++ b/backend/src/nodes/nodes/external_stable_diffusion/inpainting.py @@ -27,11 +27,12 @@ BoolInput, EnumInput, ImageInput, - NumberInput, + SeedInput, SliderInput, TextAreaInput, ) from ...properties.outputs import ImageOutput +from ...utils.seed import Seed from ...utils.utils import get_h_w_c from . import category as ExternalStableDiffusionCategory @@ -64,9 +65,7 @@ def __init__(self): controls_step=0.1, precision=2, ), - group("seed")( - NumberInput("Seed", minimum=0, default=42, maximum=4294967296) - ), + group("seed")(SeedInput()), SliderInput("Steps", minimum=1, default=20, maximum=150), EnumInput( SamplerName, @@ -148,7 +147,7 @@ def run( prompt: Optional[str], negative_prompt: Optional[str], denoising_strength: float, - seed: int, + seed: Seed, steps: int, sampler_name: SamplerName, cfg_scale: float, @@ -174,7 +173,7 @@ def run( "prompt": prompt or "", "negative_prompt": negative_prompt or "", "denoising_strength": denoising_strength, - "seed": seed, + "seed": seed.to_u32(), "steps": steps, "sampler_name": sampler_name.value, "cfg_scale": cfg_scale, diff --git a/backend/src/nodes/nodes/external_stable_diffusion/outpainting.py b/backend/src/nodes/nodes/external_stable_diffusion/outpainting.py index 229b83f21..24b4be2bc 100644 --- a/backend/src/nodes/nodes/external_stable_diffusion/outpainting.py +++ b/backend/src/nodes/nodes/external_stable_diffusion/outpainting.py @@ -27,11 +27,12 @@ BoolInput, EnumInput, ImageInput, - NumberInput, + SeedInput, SliderInput, TextAreaInput, ) from ...properties.outputs import ImageOutput +from ...utils.seed import Seed from ...utils.utils import get_h_w_c from . import category as ExternalStableDiffusionCategory @@ -61,9 +62,7 @@ def __init__(self): controls_step=0.1, precision=2, ), - group("seed")( - NumberInput("Seed", minimum=0, default=42, maximum=4294967296) - ), + group("seed")(SeedInput()), SliderInput("Steps", minimum=1, default=20, maximum=150), EnumInput( SamplerName, @@ -176,7 +175,7 @@ def run( prompt: Optional[str], negative_prompt: Optional[str], denoising_strength: float, - seed: int, + seed: Seed, steps: int, sampler_name: SamplerName, cfg_scale: float, @@ -221,7 +220,7 @@ def run( "prompt": prompt or "", "negative_prompt": negative_prompt or "", "denoising_strength": denoising_strength, - "seed": seed, + "seed": seed.to_u32(), "steps": steps, "sampler_name": sampler_name.value, "cfg_scale": cfg_scale, diff --git a/backend/src/nodes/nodes/external_stable_diffusion/txt2img.py b/backend/src/nodes/nodes/external_stable_diffusion/txt2img.py index c7ca4a8ae..cd23609f0 100644 --- a/backend/src/nodes/nodes/external_stable_diffusion/txt2img.py +++ b/backend/src/nodes/nodes/external_stable_diffusion/txt2img.py @@ -19,11 +19,12 @@ from ...properties.inputs import ( BoolInput, EnumInput, - NumberInput, + SeedInput, SliderInput, TextAreaInput, ) from ...properties.outputs import ImageOutput +from ...utils.seed import Seed from ...utils.utils import get_h_w_c from . import category as ExternalStableDiffusionCategory @@ -38,9 +39,7 @@ def __init__(self): self.inputs = [ TextAreaInput("Prompt").make_optional(), TextAreaInput("Negative Prompt").make_optional(), - group("seed")( - NumberInput("Seed", minimum=0, default=42, maximum=4294967296) - ), + group("seed")(SeedInput()), SliderInput("Steps", minimum=1, default=20, maximum=150), EnumInput( SamplerName, @@ -94,7 +93,7 @@ def run( self, prompt: Optional[str], negative_prompt: Optional[str], - seed: int, + seed: Seed, steps: int, sampler_name: SamplerName, cfg_scale: float, @@ -108,7 +107,7 @@ def run( request_data = { "prompt": prompt or "", "negative_prompt": negative_prompt or "", - "seed": seed, + "seed": seed.to_u32(), "steps": steps, "sampler_name": sampler_name.value, "cfg_scale": cfg_scale, diff --git a/backend/src/nodes/nodes/image/create_noise.py b/backend/src/nodes/nodes/image/create_noise.py index a78ab9c52..e64453c8c 100644 --- a/backend/src/nodes/nodes/image/create_noise.py +++ b/backend/src/nodes/nodes/image/create_noise.py @@ -4,16 +4,22 @@ import numpy as np -from nodes.impl.image_utils import cartesian_product -from nodes.impl.noise_functions.simplex import SimplexNoise -from nodes.impl.noise_functions.value import ValueNoise -from nodes.node_base import NodeBase, group -from nodes.node_factory import NodeFactory -from nodes.properties import expression -from nodes.properties.inputs import BoolInput, EnumInput, NumberInput, SliderInput -from nodes.properties.outputs import ImageOutput - from ...groups import conditional_group +from ...impl.image_utils import cartesian_product +from ...impl.noise_functions.simplex import SimplexNoise +from ...impl.noise_functions.value import ValueNoise +from ...node_base import NodeBase, group +from ...node_factory import NodeFactory +from ...properties import expression +from ...properties.inputs import ( + BoolInput, + EnumInput, + NumberInput, + SeedInput, + SliderInput, +) +from ...properties.outputs import ImageOutput +from ...utils.seed import Seed from . import category as ImageCategory @@ -35,9 +41,7 @@ def __init__(self): self.inputs = [ NumberInput("Width", minimum=1, unit="px", default=256), NumberInput("Height", minimum=1, unit="px", default=256), - group("seed")( - NumberInput("Seed", minimum=0, maximum=2**32 - 1, default=0), - ), + group("seed")(SeedInput()), EnumInput( NoiseMethod, default_value=NoiseMethod.SIMPLEX, @@ -129,7 +133,7 @@ def run( self, width: int, height: int, - seed: int, + seed: Seed, noise_method: NoiseMethod, scale: float, brightness: float, @@ -151,7 +155,7 @@ def run( "tile_spherical": tile_spherical, "scale": scale, "brightness": brightness, - "seed": seed, + "seed": seed.to_u32(), } generator_class = None diff --git a/backend/src/nodes/nodes/image_filter/add_noise.py b/backend/src/nodes/nodes/image_filter/add_noise.py index 623b7afe6..3821cc281 100644 --- a/backend/src/nodes/nodes/image_filter/add_noise.py +++ b/backend/src/nodes/nodes/image_filter/add_noise.py @@ -14,8 +14,9 @@ ) from ...node_base import NodeBase, group from ...node_factory import NodeFactory -from ...properties.inputs import EnumInput, ImageInput, NumberInput, SliderInput +from ...properties.inputs import EnumInput, ImageInput, SeedInput, SliderInput from ...properties.outputs import ImageOutput +from ...utils.seed import Seed from . import category as ImageFilterCategory @@ -45,9 +46,7 @@ def __init__(self): }, ), SliderInput("Amount", minimum=0, maximum=100, default=50), - group("seed")( - NumberInput("Seed", minimum=None, maximum=None, default=0), - ), + group("seed")(SeedInput()), ] self.outputs = [ ImageOutput( @@ -73,17 +72,17 @@ def run( noise_type: NoiseType, noise_color: NoiseColor, amount: int, - seed: int, + seed: Seed, ) -> np.ndarray: if noise_type == NoiseType.GAUSSIAN: - return gaussian_noise(img, amount / 100, noise_color, seed) + return gaussian_noise(img, amount / 100, noise_color, seed.value) elif noise_type == NoiseType.UNIFORM: - return uniform_noise(img, amount / 100, noise_color, seed) + return uniform_noise(img, amount / 100, noise_color, seed.value) elif noise_type == NoiseType.SALT_AND_PEPPER: - return salt_and_pepper_noise(img, amount / 100, noise_color, seed) + return salt_and_pepper_noise(img, amount / 100, noise_color, seed.value) elif noise_type == NoiseType.POISSON: - return poisson_noise(img, amount / 100, noise_color, seed) + return poisson_noise(img, amount / 100, noise_color, seed.value) elif noise_type == NoiseType.SPECKLE: - return speckle_noise(img, amount / 100, noise_color, seed) + return speckle_noise(img, amount / 100, noise_color, seed.value) else: raise ValueError(f"Unknown noise type: {noise_type}") diff --git a/backend/src/nodes/nodes/utility/derive_seed.py b/backend/src/nodes/nodes/utility/derive_seed.py new file mode 100644 index 000000000..541488436 --- /dev/null +++ b/backend/src/nodes/nodes/utility/derive_seed.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import hashlib +import struct +from typing import Union + +from ...node_base import NodeBase, group +from ...node_factory import NodeFactory +from ...properties.inputs import BaseInput, SeedInput +from ...properties.outputs import SeedOutput +from ...utils.seed import Seed +from ...utils.utils import ALPHABET +from . import category as UtilityCategory + +Source = Union[int, float, str, Seed] + + +def SourceInput(label: str): + return BaseInput( + kind="generic", + label=label, + input_type="number | string | Directory | Seed", + ).make_optional() + + +def _to_bytes(s: Source) -> bytes: + if isinstance(s, str): + return s.encode(errors="backslashreplace") + if isinstance(s, Seed): + s = s.value + + i = int(s) + if isinstance(s, int) or s == i: + return i.to_bytes(i.bit_length() // 8 + 1, byteorder="big", signed=True) + + return struct.pack("d", s) + + +@NodeFactory.register("chainner:utility:derive_seed") +class RandomNumberNode(NodeBase): + def __init__(self): + super().__init__() + self.description = "Creates a new seed from multiple sources of randomness." + self.inputs = [ + group("seed")(SeedInput(has_handle=False)), + SourceInput(f"Source A"), + group("optional-list")( + *[SourceInput(f"Source {letter}") for letter in ALPHABET[1:10]], + ), + ] + self.outputs = [ + SeedOutput(), + ] + + self.category = UtilityCategory + self.name = "Derive Seed" + self.icon = "MdCalculate" + self.sub = "Random" + + def run(self, seed: Seed, *sources: Source | None) -> Seed: + h = hashlib.sha256() + + h.update(_to_bytes(seed)) + for s in sources: + if s is not None: + h.update(_to_bytes(s)) + + return Seed.from_bytes(h.digest()) diff --git a/backend/src/nodes/nodes/utility/random_number.py b/backend/src/nodes/nodes/utility/random_number.py index c899b2f66..e81313b6d 100644 --- a/backend/src/nodes/nodes/utility/random_number.py +++ b/backend/src/nodes/nodes/utility/random_number.py @@ -1,12 +1,10 @@ -from __future__ import annotations - from random import Random -from typing import Union from ...node_base import NodeBase, group from ...node_factory import NodeFactory -from ...properties.inputs import BaseInput, NumberInput +from ...properties.inputs import NumberInput, SeedInput from ...properties.outputs import NumberOutput +from ...utils.seed import Seed from . import category as UtilityCategory @@ -27,16 +25,7 @@ def __init__(self): maximum=None, default=100, ), - group("seed")( - NumberInput( - "Seed", - minimum=0, - maximum=None, - ), - ), - BaseInput( - input_type="uint", label="Index from Iterator", kind="generic" - ).make_optional(), + group("seed")(SeedInput()), ] self.outputs = [ NumberOutput( @@ -47,11 +36,7 @@ def __init__(self): self.category = UtilityCategory self.name = "Random Number" self.icon = "MdCalculate" - self.sub = "Math" + self.sub = "Random" - def run( - self, minval: int, maxval: int, seedval: int, frameval: Union[int, None] - ) -> int: - if frameval == None: - frameval = 0 - return Random((frameval + 1) * (seedval + 1)).randint(minval, maxval) + def run(self, min_val: int, max_val: int, seed: Seed) -> int: + return Random(seed.value).randint(min_val, max_val) diff --git a/backend/src/nodes/properties/inputs/generic_inputs.py b/backend/src/nodes/properties/inputs/generic_inputs.py index bcb7f7354..3cdb18acf 100644 --- a/backend/src/nodes/properties/inputs/generic_inputs.py +++ b/backend/src/nodes/properties/inputs/generic_inputs.py @@ -9,6 +9,7 @@ from ...impl.blend import BlendMode from ...impl.dds.format import DDSFormat from ...impl.image_utils import FillColor, normalize +from ...utils.seed import Seed from ...utils.utils import ( join_pascal_case, join_space_case, @@ -17,6 +18,7 @@ ) from .. import expression from .base_input import BaseInput +from .numeric_inputs import NumberInput class UntypedOption(TypedDict): @@ -278,6 +280,35 @@ def enforce_(self, value): return value +class SeedInput(NumberInput): + def __init__(self, label: str = "Seed", has_handle: bool = True): + super().__init__( + label=label, + minimum=None, + maximum=None, + precision=0, + default=0, + ) + self.has_handle = has_handle + + self.input_type = "Seed" + self.input_conversion = None + self.input_adapt = """ + match Input { + int => Seed, + _ => never + } + """ + + def enforce(self, value) -> Seed: + if isinstance(value, Seed): + return value + return Seed(int(value)) + + def make_optional(self): + raise ValueError("SeedInput cannot be made optional") + + def IteratorInput(): """Input for showing that an iterator automatically handles the input""" return BaseInput("IteratorAuto", "Auto (Iterator)", has_handle=False) diff --git a/backend/src/nodes/properties/outputs/generic_outputs.py b/backend/src/nodes/properties/outputs/generic_outputs.py index 6ef31a3c0..5ab2e7b4f 100644 --- a/backend/src/nodes/properties/outputs/generic_outputs.py +++ b/backend/src/nodes/properties/outputs/generic_outputs.py @@ -1,5 +1,6 @@ from __future__ import annotations +from ...utils.seed import Seed from .. import expression from .base_output import BaseOutput, OutputKind @@ -40,3 +41,11 @@ def FileNameOutput(label: str = "Name", of_input: int | None = None): ) return TextOutput(label=label, output_type=output_type) + + +class SeedOutput(BaseOutput): + def __init__(self, label: str = "Seed"): + super().__init__(output_type="Seed", label=label, kind="generic") + + def validate(self, value) -> None: + assert isinstance(value, Seed) diff --git a/backend/src/nodes/utils/seed.py b/backend/src/nodes/utils/seed.py new file mode 100644 index 000000000..3ba28230a --- /dev/null +++ b/backend/src/nodes/utils/seed.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from random import Random + +_U32_MAX = 4294967296 + + +@dataclass(frozen=True) +class Seed: + value: int + """ + The value of the seed. This value may be signed and generally have any range. + """ + + @staticmethod + def from_bytes(b: bytes): + return Seed(Random(b).randint(0, _U32_MAX - 1)) + + def to_range(self, a: int, b: int) -> int: + """ + Returns the value of the seed within the given range [a,b] both ends inclusive. + + If the current seed is not within the given range, a value within the range will be derived from the current seed. + """ + if a <= self.value <= b: + return self.value + return Random(self.value).randint(a, b) + + def to_u32(self) -> int: + """ + Returns the value of the seed as a 32bit unsigned integer. + """ + return self.to_range(0, _U32_MAX - 1) diff --git a/src/common/migrations.ts b/src/common/migrations.ts index 1a0315aa1..aa59863e7 100644 --- a/src/common/migrations.ts +++ b/src/common/migrations.ts @@ -748,6 +748,116 @@ const changeColorSpaceAlpha: ModernMigration = (data) => { return data; }; +const deriveSeed: ModernMigration = (data) => { + const newNodes: N[] = []; + const newEdges: E[] = []; + + data.nodes.forEach((node) => { + if (node.data.schemaId === 'chainner:utility:random_number') { + const sourceEdge = data.edges.find((e) => e.targetHandle === `${node.id}-3`); + if (sourceEdge) { + const id = deriveUniqueId(node.id); + newNodes.push({ + data: { + schemaId: 'chainner:utility:derive_seed' as SchemaId, + inputData: { 0: node.data.inputData[2] }, + id, + }, + id, + position: { x: node.position.x - 280, y: node.position.y - 20 }, + type: 'regularNode', + selected: false, + height: 356, + width: 242, + zIndex: node.zIndex, + parentNode: node.parentNode, + }); + sourceEdge.target = id; + sourceEdge.targetHandle = `${id}-1`; + + const seedEdge = data.edges.find((e) => e.targetHandle === `${node.id}-2`); + if (seedEdge) { + sourceEdge.target = id; + sourceEdge.targetHandle = `${id}-2`; + } + + newEdges.push({ + id: deriveUniqueId(node.id + id), + source: id, + sourceHandle: `${id}-0`, + target: node.id, + targetHandle: `${node.id}-2`, + type: 'main', + animated: false, + data: {}, + zIndex: sourceEdge.zIndex, + }); + } + } + }); + data.nodes.push(...newNodes); + data.edges.push(...newEdges); + + return data; +}; + +const seedInput: ModernMigration = (data) => { + const changedInputs: Record = { + 'chainner:external_stable_diffusion:img2img': 4, + 'chainner:external_stable_diffusion:img2img_inpainting': 5, + 'chainner:external_stable_diffusion:img2img_outpainting': 4, + 'chainner:external_stable_diffusion:txt2img': 2, + 'chainner:image:create_noise': 2, + 'chainner:image:add_noise': 4, + }; + + const newNodes: N[] = []; + const newEdges: E[] = []; + + data.nodes.forEach((node) => { + const inputId = changedInputs[node.data.schemaId]; + if (typeof inputId === 'number') { + const seedEdge = data.edges.find((e) => e.targetHandle === `${node.id}-${inputId}`); + if (seedEdge) { + const id = deriveUniqueId(`${node.id}seedInput`); + newNodes.push({ + data: { + schemaId: 'chainner:utility:derive_seed' as SchemaId, + inputData: { 0: 0 }, + id, + }, + id, + position: { x: node.position.x - 280, y: node.position.y - 20 }, + type: 'regularNode', + selected: false, + height: 356, + width: 242, + zIndex: node.zIndex, + parentNode: node.parentNode, + }); + seedEdge.target = id; + seedEdge.targetHandle = `${id}-1`; + + newEdges.push({ + id: deriveUniqueId(node.id + id), + source: id, + sourceHandle: `${id}-0`, + target: node.id, + targetHandle: `${node.id}-${inputId}`, + type: 'main', + animated: false, + data: {}, + zIndex: seedEdge.zIndex, + }); + } + } + }); + data.nodes.push(...newNodes); + data.edges.push(...newEdges); + + return data; +}; + // ============== const versionToMigration = (version: string) => { @@ -791,6 +901,8 @@ const migrations = [ clearEdgeData, gammaCheckbox, changeColorSpaceAlpha, + deriveSeed, + seedInput, ]; export const currentMigration = migrations.length; diff --git a/src/common/types/chainner-scope.ts b/src/common/types/chainner-scope.ts index 865f08e96..bb89c3012 100644 --- a/src/common/types/chainner-scope.ts +++ b/src/common/types/chainner-scope.ts @@ -14,6 +14,8 @@ import { formatTextPattern, padCenter, padEnd, padStart, splitFilePath } from '. const code = ` struct null; +struct Seed; + struct Directory { path: string } struct AudioFile; diff --git a/src/common/types/function.ts b/src/common/types/function.ts index 2c3eacf96..6453d7ca5 100644 --- a/src/common/types/function.ts +++ b/src/common/types/function.ts @@ -203,67 +203,65 @@ const getInputDataAdapters = ( for (const input of schema.inputs) { const inputName = `${schema.name} (id: ${schema.schemaId}) > ${input.label} (id: ${input.id})`; - switch (input.kind) { - case 'number': - case 'slider': - case 'text': - case 'text-line': { - adapters.set(input.id, (value) => literal(value as never)); - break; - } + if (input.adapt != null) { + const adoptExpression = fromJson(input.adapt); + const conversionScope = getConversionScope(scope); - case 'dropdown': { - const options = new Map(); - for (const o of input.options) { - if (o.type !== undefined) { - const name = `${o.option}=${JSON.stringify(o.value)} in ${inputName}`; - - let type; - try { - type = evaluate(fromJson(o.type), scope); - } catch (error) { - throw new Error( - `Unable to evaluate type of option ${name}: ${String(error)}` - ); - } - if (type.type === 'never') { - throw new Error(`Type of ${name} cannot be 'never'.`); - } + // verify that it's a valid conversion + try { + conversionScope.assignParameter( + 'Input', + union(NumberType.instance, StringType.instance) + ); + evaluate(adoptExpression, conversionScope); + } catch (error) { + const name = `${schema.name} (id: ${schema.schemaId}) > ${input.label} (id: ${input.id})`; + throw new Error(`The conversion of input ${name} is invalid: ${String(error)}`); + } - options.set(o.value, type); - } + adapters.set(input.id, (value) => { + conversionScope.assignParameter('Input', literal(value as never)); + const result = evaluate(adoptExpression, conversionScope); + if (result.type === 'never') return undefined; + return result; + }); + } else { + switch (input.kind) { + case 'number': + case 'slider': + case 'text': + case 'text-line': { + adapters.set(input.id, (value) => literal(value as never)); + break; } - adapters.set(input.id, (value) => options.get(value)); - break; - } - default: { - if (input.adapt != null) { - const adoptExpression = fromJson(input.adapt); - const conversionScope = getConversionScope(scope); - - // verify that it's a valid conversion - try { - conversionScope.assignParameter( - 'Input', - union(NumberType.instance, StringType.instance) - ); - evaluate(adoptExpression, conversionScope); - } catch (error) { - const name = `${schema.name} (id: ${schema.schemaId}) > ${input.label} (id: ${input.id})`; - throw new Error( - `The conversion of input ${name} is invalid: ${String(error)}` - ); + case 'dropdown': { + const options = new Map(); + for (const o of input.options) { + if (o.type !== undefined) { + const name = `${o.option}=${JSON.stringify(o.value)} in ${inputName}`; + + let type; + try { + type = evaluate(fromJson(o.type), scope); + } catch (error) { + throw new Error( + `Unable to evaluate type of option ${name}: ${String(error)}` + ); + } + if (type.type === 'never') { + throw new Error(`Type of ${name} cannot be 'never'.`); + } + + options.set(o.value, type); + } } - - adapters.set(input.id, (value) => { - conversionScope.assignParameter('Input', literal(value as never)); - const result = evaluate(adoptExpression, conversionScope); - if (result.type === 'never') return undefined; - return result; - }); + adapters.set(input.id, (value) => options.get(value)); + break; } - break; + + default: + break; } } } diff --git a/tests/common/__snapshots__/SaveFile.test.ts.snap b/tests/common/__snapshots__/SaveFile.test.ts.snap index e21b67bd1..9f1074832 100644 --- a/tests/common/__snapshots__/SaveFile.test.ts.snap +++ b/tests/common/__snapshots__/SaveFile.test.ts.snap @@ -1,5 +1,172 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Read save file add noise with seed edge.chn 1`] = ` +Object { + "edges": Array [ + Object { + "animated": false, + "data": Object {}, + "id": "01c37d90-c15a-44a6-95af-fa105b9f7caf", + "source": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "sourceHandle": "4b44b642-8003-41d7-9fed-6d7bac837b1c-0", + "target": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "targetHandle": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f-1", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "51c5608b-3ac4-4062-ae29-d06a6af47979", + "source": "7bd9efef-3506-42cb-942a-062f3e410085", + "sourceHandle": "7bd9efef-3506-42cb-942a-062f3e410085-0", + "target": "545c6463-b978-4987-a3c3-a44a774184be", + "targetHandle": "545c6463-b978-4987-a3c3-a44a774184be-0", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "7ad71b84-1fc7-4d06-a729-aa0927fa9c6b", + "source": "545c6463-b978-4987-a3c3-a44a774184be", + "sourceHandle": "545c6463-b978-4987-a3c3-a44a774184be-0", + "target": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc", + "targetHandle": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc-0", + "type": "main", + "zIndex": 69, + }, + Object { + "animated": false, + "data": Object {}, + "id": "e4a21d62-cc17-42ea-85c3-5ab8013767b2", + "source": "7bd9efef-3506-42cb-942a-062f3e410085", + "sourceHandle": "7bd9efef-3506-42cb-942a-062f3e410085-2", + "target": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "targetHandle": "4b44b642-8003-41d7-9fed-6d7bac837b1c-0", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "161e3e49-745d-58fe-aae4-e0bdefa50dff", + "source": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "sourceHandle": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f-0", + "target": "545c6463-b978-4987-a3c3-a44a774184be", + "targetHandle": "545c6463-b978-4987-a3c3-a44a774184be-4", + "type": "main", + "zIndex": 49, + }, + ], + "nodes": Array [ + Object { + "data": Object { + "id": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "inputData": Object { + "0": "", + }, + "schemaId": "chainner:utility:text_length", + }, + "height": 164, + "id": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "position": Object { + "x": 496, + "y": 800, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "545c6463-b978-4987-a3c3-a44a774184be", + "inputData": Object { + "1": "gaussian", + "2": "rgb", + "3": 50, + "4": 0, + }, + "schemaId": "chainner:image:add_noise", + }, + "height": 356, + "id": "545c6463-b978-4987-a3c3-a44a774184be", + "position": Object { + "x": 800, + "y": 544, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "7bd9efef-3506-42cb-942a-062f3e410085", + "inputData": Object { + "0": "C:\\\\Users\\\\micha\\\\Desktop\\\\face.png", + }, + "schemaId": "chainner:image:load", + }, + "height": 420, + "id": "7bd9efef-3506-42cb-942a-062f3e410085", + "position": Object { + "x": 192, + "y": 480, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc", + "inputData": Object {}, + "schemaId": "chainner:image:view", + }, + "height": 332, + "id": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc", + "position": Object { + "x": 1104, + "y": 544, + }, + "selected": true, + "type": "regularNode", + "width": 240, + "zIndex": 70, + }, + Object { + "data": Object { + "id": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "inputData": Object { + "0": 0, + }, + "schemaId": "chainner:utility:derive_seed", + }, + "height": 356, + "id": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "parentNode": undefined, + "position": Object { + "x": 520, + "y": 524, + }, + "selected": false, + "type": "regularNode", + "width": 242, + "zIndex": 50, + }, + ], + "tamperedWith": false, + "viewport": Object { + "x": -172.3368816369316, + "y": -148.94747837726254, + "zoom": 1.1486983549970347, + }, +} +`; + exports[`Read save file big ol test.chn 1`] = ` Object { "edges": Array [ @@ -4647,6 +4814,131 @@ Object { } `; +exports[`Read save file rnd.chn 1`] = ` +Object { + "edges": Array [ + Object { + "animated": false, + "data": Object {}, + "id": "96b4c334-274b-434f-9eba-f3c91330e34b", + "source": "8097ee60-8658-49ed-a206-7687f612fc3c", + "sourceHandle": "8097ee60-8658-49ed-a206-7687f612fc3c-0", + "target": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "targetHandle": "7e4ee660-b958-5ac7-9914-7887f3c49eff-1", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "b111966e-2872-4596-9702-87c6e04a3233", + "source": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "sourceHandle": "5f7b26fa-7032-4849-891d-9286669d2cfe-0", + "target": "9b373116-cab0-4df2-8517-4a65c8d9bb55", + "targetHandle": "9b373116-cab0-4df2-8517-4a65c8d9bb55-0", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "857649e8-1b7c-5f5b-9d8e-0a4ee1f3e7e8", + "source": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "sourceHandle": "7e4ee660-b958-5ac7-9914-7887f3c49eff-0", + "target": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "targetHandle": "5f7b26fa-7032-4849-891d-9286669d2cfe-2", + "type": "main", + "zIndex": 49, + }, + ], + "nodes": Array [ + Object { + "data": Object { + "id": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "inputData": Object { + "0": 0, + "1": 100, + "2": 123, + }, + "schemaId": "chainner:utility:random_number", + }, + "height": 308, + "id": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "position": Object { + "x": 80, + "y": 720, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "8097ee60-8658-49ed-a206-7687f612fc3c", + "inputData": Object { + "0": 42, + }, + "schemaId": "chainner:utility:number", + }, + "height": 164, + "id": "8097ee60-8658-49ed-a206-7687f612fc3c", + "position": Object { + "x": -240, + "y": 864, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "9b373116-cab0-4df2-8517-4a65c8d9bb55", + "inputData": Object {}, + "schemaId": "chainner:utility:copy_to_clipboard", + }, + "height": 124, + "id": "9b373116-cab0-4df2-8517-4a65c8d9bb55", + "position": Object { + "x": 384, + "y": 896, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "inputData": Object { + "0": 123, + }, + "schemaId": "chainner:utility:derive_seed", + }, + "height": 356, + "id": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "parentNode": undefined, + "position": Object { + "x": -200, + "y": 700, + }, + "selected": false, + "type": "regularNode", + "width": 242, + "zIndex": 50, + }, + ], + "tamperedWith": false, + "viewport": Object { + "x": 298.9783568154796, + "y": 184.89290185654272, + "zoom": 0.6597539553864473, + }, +} +`; + exports[`Read save file text-pattern.chn 1`] = ` Object { "edges": Array [ @@ -4904,6 +5196,175 @@ Object { } `; +exports[`Write save file add noise with seed edge.chn 1`] = ` +Object { + "checksum": "5893bba761593cdd2c35cb69dff2a6ae", + "content": Object { + "edges": Array [ + Object { + "animated": false, + "data": Object {}, + "id": "01c37d90-c15a-44a6-95af-fa105b9f7caf", + "source": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "sourceHandle": "4b44b642-8003-41d7-9fed-6d7bac837b1c-0", + "target": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "targetHandle": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f-1", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "51c5608b-3ac4-4062-ae29-d06a6af47979", + "source": "7bd9efef-3506-42cb-942a-062f3e410085", + "sourceHandle": "7bd9efef-3506-42cb-942a-062f3e410085-0", + "target": "545c6463-b978-4987-a3c3-a44a774184be", + "targetHandle": "545c6463-b978-4987-a3c3-a44a774184be-0", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "7ad71b84-1fc7-4d06-a729-aa0927fa9c6b", + "source": "545c6463-b978-4987-a3c3-a44a774184be", + "sourceHandle": "545c6463-b978-4987-a3c3-a44a774184be-0", + "target": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc", + "targetHandle": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc-0", + "type": "main", + "zIndex": 69, + }, + Object { + "animated": false, + "data": Object {}, + "id": "e4a21d62-cc17-42ea-85c3-5ab8013767b2", + "source": "7bd9efef-3506-42cb-942a-062f3e410085", + "sourceHandle": "7bd9efef-3506-42cb-942a-062f3e410085-2", + "target": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "targetHandle": "4b44b642-8003-41d7-9fed-6d7bac837b1c-0", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "161e3e49-745d-58fe-aae4-e0bdefa50dff", + "source": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "sourceHandle": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f-0", + "target": "545c6463-b978-4987-a3c3-a44a774184be", + "targetHandle": "545c6463-b978-4987-a3c3-a44a774184be-4", + "type": "main", + "zIndex": 49, + }, + ], + "nodes": Array [ + Object { + "data": Object { + "id": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "inputData": Object { + "0": "", + }, + "schemaId": "chainner:utility:text_length", + }, + "height": 164, + "id": "4b44b642-8003-41d7-9fed-6d7bac837b1c", + "position": Object { + "x": 496, + "y": 800, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "545c6463-b978-4987-a3c3-a44a774184be", + "inputData": Object { + "1": "gaussian", + "2": "rgb", + "3": 50, + "4": 0, + }, + "schemaId": "chainner:image:add_noise", + }, + "height": 356, + "id": "545c6463-b978-4987-a3c3-a44a774184be", + "position": Object { + "x": 800, + "y": 544, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "7bd9efef-3506-42cb-942a-062f3e410085", + "inputData": Object { + "0": "C:\\\\Users\\\\micha\\\\Desktop\\\\face.png", + }, + "schemaId": "chainner:image:load", + }, + "height": 420, + "id": "7bd9efef-3506-42cb-942a-062f3e410085", + "position": Object { + "x": 192, + "y": 480, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc", + "inputData": Object {}, + "schemaId": "chainner:image:view", + }, + "height": 332, + "id": "e67d0c33-3f14-40c7-8bce-f6c67cfee6bc", + "position": Object { + "x": 1104, + "y": 544, + }, + "selected": true, + "type": "regularNode", + "width": 240, + "zIndex": 70, + }, + Object { + "data": Object { + "id": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "inputData": Object { + "0": 0, + }, + "schemaId": "chainner:utility:derive_seed", + }, + "height": 356, + "id": "fbc19ef9-0b32-52a8-94f4-ef1be1a9063f", + "position": Object { + "x": 520, + "y": 524, + }, + "selected": false, + "type": "regularNode", + "width": 242, + "zIndex": 50, + }, + ], + "viewport": Object { + "x": -172.3368816369316, + "y": -148.94747837726254, + "zoom": 1.1486983549970347, + }, + }, + "version": "0.0.0-test", +} +`; + exports[`Write save file big ol test.chn 1`] = ` Object { "checksum": "f74ad3bcaed84b5b46366781974d9667", @@ -9439,6 +9900,133 @@ Object { } `; +exports[`Write save file rnd.chn 1`] = ` +Object { + "checksum": "12beb05cab258628f98e7cf064373eec", + "content": Object { + "edges": Array [ + Object { + "animated": false, + "data": Object {}, + "id": "96b4c334-274b-434f-9eba-f3c91330e34b", + "source": "8097ee60-8658-49ed-a206-7687f612fc3c", + "sourceHandle": "8097ee60-8658-49ed-a206-7687f612fc3c-0", + "target": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "targetHandle": "7e4ee660-b958-5ac7-9914-7887f3c49eff-1", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "b111966e-2872-4596-9702-87c6e04a3233", + "source": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "sourceHandle": "5f7b26fa-7032-4849-891d-9286669d2cfe-0", + "target": "9b373116-cab0-4df2-8517-4a65c8d9bb55", + "targetHandle": "9b373116-cab0-4df2-8517-4a65c8d9bb55-0", + "type": "main", + "zIndex": 49, + }, + Object { + "animated": false, + "data": Object {}, + "id": "857649e8-1b7c-5f5b-9d8e-0a4ee1f3e7e8", + "source": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "sourceHandle": "7e4ee660-b958-5ac7-9914-7887f3c49eff-0", + "target": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "targetHandle": "5f7b26fa-7032-4849-891d-9286669d2cfe-2", + "type": "main", + "zIndex": 49, + }, + ], + "nodes": Array [ + Object { + "data": Object { + "id": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "inputData": Object { + "0": 0, + "1": 100, + "2": 123, + }, + "schemaId": "chainner:utility:random_number", + }, + "height": 308, + "id": "5f7b26fa-7032-4849-891d-9286669d2cfe", + "position": Object { + "x": 80, + "y": 720, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "8097ee60-8658-49ed-a206-7687f612fc3c", + "inputData": Object { + "0": 42, + }, + "schemaId": "chainner:utility:number", + }, + "height": 164, + "id": "8097ee60-8658-49ed-a206-7687f612fc3c", + "position": Object { + "x": -240, + "y": 864, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "9b373116-cab0-4df2-8517-4a65c8d9bb55", + "inputData": Object {}, + "schemaId": "chainner:utility:copy_to_clipboard", + }, + "height": 124, + "id": "9b373116-cab0-4df2-8517-4a65c8d9bb55", + "position": Object { + "x": 384, + "y": 896, + }, + "selected": false, + "type": "regularNode", + "width": 240, + "zIndex": 50, + }, + Object { + "data": Object { + "id": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "inputData": Object { + "0": 123, + }, + "schemaId": "chainner:utility:derive_seed", + }, + "height": 356, + "id": "7e4ee660-b958-5ac7-9914-7887f3c49eff", + "position": Object { + "x": -200, + "y": 700, + }, + "selected": false, + "type": "regularNode", + "width": 242, + "zIndex": 50, + }, + ], + "viewport": Object { + "x": 298.9783568154796, + "y": 184.89290185654272, + "zoom": 0.6597539553864473, + }, + }, + "version": "0.0.0-test", +} +`; + exports[`Write save file text-pattern.chn 1`] = ` Object { "checksum": "c0dff178466fea6b688fcb5459d05f79", diff --git a/tests/data/add noise with seed edge.chn b/tests/data/add noise with seed edge.chn new file mode 100644 index 000000000..d5ab39bea --- /dev/null +++ b/tests/data/add noise with seed edge.chn @@ -0,0 +1 @@ +{"version":"0.18.1","content":{"nodes":[{"data":{"schemaId":"chainner:utility:text_length","inputData":{"0":""},"id":"4b44b642-8003-41d7-9fed-6d7bac837b1c"},"id":"4b44b642-8003-41d7-9fed-6d7bac837b1c","position":{"x":496,"y":800},"type":"regularNode","selected":false,"height":164,"width":240,"zIndex":50},{"data":{"schemaId":"chainner:image:add_noise","inputData":{"1":"gaussian","2":"rgb","3":50,"4":0},"id":"545c6463-b978-4987-a3c3-a44a774184be"},"id":"545c6463-b978-4987-a3c3-a44a774184be","position":{"x":800,"y":544},"type":"regularNode","selected":false,"height":356,"width":240,"zIndex":50},{"data":{"schemaId":"chainner:image:load","inputData":{"0":"C:\\Users\\micha\\Desktop\\face.png"},"id":"7bd9efef-3506-42cb-942a-062f3e410085"},"id":"7bd9efef-3506-42cb-942a-062f3e410085","position":{"x":192,"y":480},"type":"regularNode","selected":false,"height":420,"width":240,"zIndex":50},{"data":{"schemaId":"chainner:image:view","inputData":{},"id":"e67d0c33-3f14-40c7-8bce-f6c67cfee6bc"},"id":"e67d0c33-3f14-40c7-8bce-f6c67cfee6bc","position":{"x":1104,"y":544},"type":"regularNode","selected":true,"height":332,"width":240,"zIndex":70}],"edges":[{"id":"01c37d90-c15a-44a6-95af-fa105b9f7caf","sourceHandle":"4b44b642-8003-41d7-9fed-6d7bac837b1c-0","targetHandle":"545c6463-b978-4987-a3c3-a44a774184be-4","source":"4b44b642-8003-41d7-9fed-6d7bac837b1c","target":"545c6463-b978-4987-a3c3-a44a774184be","type":"main","animated":false,"data":{},"zIndex":49},{"id":"51c5608b-3ac4-4062-ae29-d06a6af47979","sourceHandle":"7bd9efef-3506-42cb-942a-062f3e410085-0","targetHandle":"545c6463-b978-4987-a3c3-a44a774184be-0","source":"7bd9efef-3506-42cb-942a-062f3e410085","target":"545c6463-b978-4987-a3c3-a44a774184be","type":"main","animated":false,"data":{},"zIndex":49},{"id":"7ad71b84-1fc7-4d06-a729-aa0927fa9c6b","sourceHandle":"545c6463-b978-4987-a3c3-a44a774184be-0","targetHandle":"e67d0c33-3f14-40c7-8bce-f6c67cfee6bc-0","source":"545c6463-b978-4987-a3c3-a44a774184be","target":"e67d0c33-3f14-40c7-8bce-f6c67cfee6bc","type":"main","animated":false,"data":{},"zIndex":69},{"id":"e4a21d62-cc17-42ea-85c3-5ab8013767b2","sourceHandle":"7bd9efef-3506-42cb-942a-062f3e410085-2","targetHandle":"4b44b642-8003-41d7-9fed-6d7bac837b1c-0","source":"7bd9efef-3506-42cb-942a-062f3e410085","target":"4b44b642-8003-41d7-9fed-6d7bac837b1c","type":"main","animated":false,"data":{},"zIndex":49}],"viewport":{"x":-172.3368816369316,"y":-148.94747837726254,"zoom":1.1486983549970347}},"timestamp":"2023-03-01T14:33:03.483Z","checksum":"e32199899572250707a378f32a0f8a43","migration":26} \ No newline at end of file diff --git a/tests/data/rnd.chn b/tests/data/rnd.chn new file mode 100644 index 000000000..a72050735 --- /dev/null +++ b/tests/data/rnd.chn @@ -0,0 +1 @@ +{"version":"0.18.1","content":{"nodes":[{"data":{"schemaId":"chainner:utility:random_number","inputData":{"0":0,"1":100,"2":123},"id":"5f7b26fa-7032-4849-891d-9286669d2cfe"},"id":"5f7b26fa-7032-4849-891d-9286669d2cfe","position":{"x":80,"y":720},"type":"regularNode","selected":false,"height":308,"width":240,"zIndex":50},{"data":{"schemaId":"chainner:utility:number","inputData":{"0":42},"id":"8097ee60-8658-49ed-a206-7687f612fc3c"},"id":"8097ee60-8658-49ed-a206-7687f612fc3c","position":{"x":-240,"y":864},"type":"regularNode","selected":false,"height":164,"width":240,"zIndex":50},{"data":{"schemaId":"chainner:utility:copy_to_clipboard","inputData":{},"id":"9b373116-cab0-4df2-8517-4a65c8d9bb55"},"id":"9b373116-cab0-4df2-8517-4a65c8d9bb55","position":{"x":384,"y":896},"type":"regularNode","selected":false,"height":124,"width":240,"zIndex":50}],"edges":[{"id":"96b4c334-274b-434f-9eba-f3c91330e34b","sourceHandle":"8097ee60-8658-49ed-a206-7687f612fc3c-0","targetHandle":"5f7b26fa-7032-4849-891d-9286669d2cfe-3","source":"8097ee60-8658-49ed-a206-7687f612fc3c","target":"5f7b26fa-7032-4849-891d-9286669d2cfe","type":"main","animated":false,"data":{},"zIndex":49},{"id":"b111966e-2872-4596-9702-87c6e04a3233","sourceHandle":"5f7b26fa-7032-4849-891d-9286669d2cfe-0","targetHandle":"9b373116-cab0-4df2-8517-4a65c8d9bb55-0","source":"5f7b26fa-7032-4849-891d-9286669d2cfe","target":"9b373116-cab0-4df2-8517-4a65c8d9bb55","type":"main","animated":false,"data":{},"zIndex":49}],"viewport":{"x":298.9783568154796,"y":184.89290185654272,"zoom":0.6597539553864473}},"timestamp":"2023-02-28T18:58:03.293Z","checksum":"06bb8ce3f8dba9ecb07fe805bd3565f5","migration":26} \ No newline at end of file