flowgraph 0.1.2__py3-none-any.whl
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.
- flowgraph/__init__.py +9 -0
- flowgraph/__main__.py +6 -0
- flowgraph/adapters/__init__.py +141 -0
- flowgraph/adapters/controls.py +96 -0
- flowgraph/adapters/cosapp.py +195 -0
- flowgraph/adapters/data_types.py +59 -0
- flowgraph/adapters/documentation.py +45 -0
- flowgraph/adapters/field_ops.py +121 -0
- flowgraph/adapters/file_conversion.py +393 -0
- flowgraph/adapters/files.py +191 -0
- flowgraph/adapters/filters.py +173 -0
- flowgraph/adapters/image_tools.py +394 -0
- flowgraph/adapters/mesh_creation_tools.py +431 -0
- flowgraph/adapters/mesh_generations.py +58 -0
- flowgraph/adapters/mesh_ops.py +362 -0
- flowgraph/adapters/paraview.py +39 -0
- flowgraph/adapters/plaid_bridge.py +205 -0
- flowgraph/adapters/readers.py +215 -0
- flowgraph/adapters/remote_files.py +118 -0
- flowgraph/adapters/simple_sources.py +199 -0
- flowgraph/adapters/sinks.py +80 -0
- flowgraph/adapters/user_code.py +196 -0
- flowgraph/adapters/workflow.py +176 -0
- flowgraph/adapters/workflow_interfaces.py +78 -0
- flowgraph/adapters/writers.py +159 -0
- flowgraph/application/__init__.py +1 -0
- flowgraph/application/node_registry.py +14 -0
- flowgraph/application/port_conversions.py +63 -0
- flowgraph/application/wasm_execution.py +132 -0
- flowgraph/application/workflow_cli.py +417 -0
- flowgraph/application/workflow_core.py +1193 -0
- flowgraph/application/workflow_history.py +93 -0
- flowgraph/application/workflow_io.py +393 -0
- flowgraph/application/workflow_layout.py +170 -0
- flowgraph/application/workflow_web_catalog.py +143 -0
- flowgraph/application/workflows.py +7 -0
- flowgraph/core_cli.py +81 -0
- flowgraph/domain/__init__.py +5 -0
- flowgraph/domain/mesh_document.py +91 -0
- flowgraph/examples/AllFilters.py +19 -0
- flowgraph/examples/convert_format.py +21 -0
- flowgraph/resources.py +29 -0
- flowgraph/testdata/AllFilters.json +390 -0
- flowgraph/testdata/FlowGraph.h5 +0 -0
- flowgraph/testdata/FlowGraph.stl +0 -0
- flowgraph/testdata/FlowGraph.xdmf +22 -0
- flowgraph/testdata/SimpleReadMesh.json +70 -0
- flowgraph/testdata/SpherePoints.csv +83 -0
- flowgraph/testdata/muscat-muscat-mesh-conversion.json +101 -0
- flowgraph/version.py +3 -0
- flowgraph-0.1.2.dist-info/METADATA +150 -0
- flowgraph-0.1.2.dist-info/RECORD +56 -0
- flowgraph-0.1.2.dist-info/WHEEL +5 -0
- flowgraph-0.1.2.dist-info/entry_points.txt +2 -0
- flowgraph-0.1.2.dist-info/licenses/LICENSE +29 -0
- flowgraph-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Workflow nodes that read external data into FlowGraph domain objects."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from flowgraph.application.workflow_core import (
|
|
9
|
+
NodeDefinition,
|
|
10
|
+
ParameterDefinition,
|
|
11
|
+
PortDefinition,
|
|
12
|
+
PortDirection,
|
|
13
|
+
)
|
|
14
|
+
from flowgraph.domain.mesh_document import MeshDocument
|
|
15
|
+
|
|
16
|
+
from .data_types import MESH_DOCUMENT, STRING, TABLE_DOCUMENT, ParameterKind
|
|
17
|
+
|
|
18
|
+
LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
"""Boundary between a source file and FlowGraph's canonical Muscat document."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MeshLoadError(RuntimeError):
|
|
25
|
+
"""Raised when a selected mesh cannot be loaded by a reader adapter."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _validate_source(
|
|
29
|
+
inputs: Mapping[str, Any], parameters: Mapping[str, Any], reader_name: str
|
|
30
|
+
) -> Path:
|
|
31
|
+
"""Return a usable source path or raise a reader-specific load error."""
|
|
32
|
+
configured_path = inputs.get("path", parameters.get("path", ""))
|
|
33
|
+
source = Path(configured_path).expanduser()
|
|
34
|
+
|
|
35
|
+
LOGGER.debug("Validating requested mesh path: %s", source)
|
|
36
|
+
if not source.is_file():
|
|
37
|
+
LOGGER.warning("Rejected missing/non-file mesh path: %s", source)
|
|
38
|
+
raise MeshLoadError(f"Mesh file does not exist or is not a file: {source}")
|
|
39
|
+
if not source.suffix:
|
|
40
|
+
LOGGER.warning("Rejected extensionless mesh path: %s", source)
|
|
41
|
+
raise MeshLoadError(
|
|
42
|
+
f"Mesh file has no extension; {reader_name} cannot select a reader: {source}"
|
|
43
|
+
)
|
|
44
|
+
return source
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load_muscat(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
48
|
+
"""Load *source* with Muscat's extension-aware universal mesh reader.
|
|
49
|
+
|
|
50
|
+
The returned :class:`~flowgraph.domain.mesh_document.MeshDocument` is the
|
|
51
|
+
canonical editable model. The caller can display its summary immediately;
|
|
52
|
+
VTK conversion remains a separate, derived projection.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
source = _validate_source(inputs, parameters, "Muscat")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
from Muscat.IO.IOFactory import CreateReader, InitAllReaders
|
|
59
|
+
|
|
60
|
+
LOGGER.debug("Initialising Muscat readers for %s", source)
|
|
61
|
+
InitAllReaders()
|
|
62
|
+
reader = CreateReader(source.suffix)
|
|
63
|
+
LOGGER.info("Loading mesh with Muscat reader %s: %s", type(reader).__name__, source)
|
|
64
|
+
reader.SetFileName(str(source))
|
|
65
|
+
mesh = reader.Read()
|
|
66
|
+
except Exception as error:
|
|
67
|
+
LOGGER.exception("Muscat failed while loading %s", source)
|
|
68
|
+
raise MeshLoadError(f"Muscat could not load '{source}': {error}") from error
|
|
69
|
+
|
|
70
|
+
LOGGER.info(
|
|
71
|
+
"Muscat loaded %s (%d nodes, %d elements)",
|
|
72
|
+
source,
|
|
73
|
+
mesh.GetNumberOfNodes(),
|
|
74
|
+
mesh.GetNumberOfElements(),
|
|
75
|
+
)
|
|
76
|
+
return {"mesh": MeshDocument(mesh=mesh)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load_meshio(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
80
|
+
"""Load *source* with meshio and convert it through Muscat's bridge."""
|
|
81
|
+
source = _validate_source(inputs, parameters, "meshio")
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
import meshio
|
|
85
|
+
from Muscat.Bridges.MeshIOBridge import MeshIOToMesh
|
|
86
|
+
|
|
87
|
+
LOGGER.info("Loading mesh with meshio: %s", source)
|
|
88
|
+
mesh = MeshIOToMesh(meshio.read(source))
|
|
89
|
+
except Exception as error:
|
|
90
|
+
LOGGER.exception("meshio failed while loading %s", source)
|
|
91
|
+
raise MeshLoadError(f"meshio could not load '{source}': {error}") from error
|
|
92
|
+
|
|
93
|
+
LOGGER.info(
|
|
94
|
+
"meshio loaded %s through the Muscat bridge (%d nodes, %d elements)",
|
|
95
|
+
source,
|
|
96
|
+
mesh.GetNumberOfNodes(),
|
|
97
|
+
mesh.GetNumberOfElements(),
|
|
98
|
+
)
|
|
99
|
+
return {"mesh": MeshDocument(mesh=mesh)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_meshlane(inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
103
|
+
"""Load *source* with the optional MeshLane adapter."""
|
|
104
|
+
source = _validate_source(inputs, parameters, "MeshLane")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
import meshlane
|
|
108
|
+
except ImportError as error:
|
|
109
|
+
raise MeshLoadError(
|
|
110
|
+
"MeshLane is not installed; install FlowGraph with the 'meshlane' extra"
|
|
111
|
+
) from error
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
from Muscat.Bridges.MeshlaneBridge import MeshlaneToMesh
|
|
115
|
+
except ImportError as error:
|
|
116
|
+
raise MeshLoadError(
|
|
117
|
+
"This Muscat installation has no MeshLane bridge; install FlowGraph with "
|
|
118
|
+
"the 'meshlane' extra"
|
|
119
|
+
) from error
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
LOGGER.info("Loading mesh with MeshLane: %s", source)
|
|
123
|
+
mesh = MeshlaneToMesh(meshlane.read(source))
|
|
124
|
+
except Exception as error:
|
|
125
|
+
LOGGER.exception("MeshLane failed while loading %s", source)
|
|
126
|
+
raise MeshLoadError(f"MeshLane could not load '{source}': {error}") from error
|
|
127
|
+
|
|
128
|
+
LOGGER.info(
|
|
129
|
+
"MeshLane loaded %s through the Muscat bridge (%d nodes, %d elements)",
|
|
130
|
+
source,
|
|
131
|
+
mesh.GetNumberOfNodes(),
|
|
132
|
+
mesh.GetNumberOfElements(),
|
|
133
|
+
)
|
|
134
|
+
return {"mesh": MeshDocument(mesh=mesh)}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def load_csv(inputs: Mapping[str, Any], _parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Load a CSV file and return a dictionary where the keys are column names and the values are numpy arrays.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
file_path (str): The path to the CSV file.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
dict: A dictionary where the keys are column names and the values are numpy arrays.
|
|
146
|
+
"""
|
|
147
|
+
import pandas as pd
|
|
148
|
+
|
|
149
|
+
# Read the CSV file
|
|
150
|
+
source = _validate_source(inputs, {}, "MeshLane")
|
|
151
|
+
df = pd.read_csv(source)
|
|
152
|
+
|
|
153
|
+
# Convert the DataFrame to a dictionary
|
|
154
|
+
data_dict = {col: df[col].to_numpy() for col in df.columns}
|
|
155
|
+
|
|
156
|
+
# Return the dictionary
|
|
157
|
+
return {"table": data_dict}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
LOAD_MUSCAT = NodeDefinition(
|
|
161
|
+
id="load-muscat",
|
|
162
|
+
icon="/__flowgraph_ui/LOAD_MUSCAT.svg",
|
|
163
|
+
label="Read Mesh (Muscat)",
|
|
164
|
+
description="Loads a file path into the canonical mesh document.",
|
|
165
|
+
ports=(PortDefinition("mesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh"),),
|
|
166
|
+
executor=_load_muscat,
|
|
167
|
+
parameters=(
|
|
168
|
+
ParameterDefinition(
|
|
169
|
+
"path", ParameterKind.FILE, "File path", "", "/path/to/mesh", port=True
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
LOAD_MESHIO = NodeDefinition(
|
|
176
|
+
id="load-meshio",
|
|
177
|
+
icon="/__flowgraph_ui/LOAD_MESHIO.svg",
|
|
178
|
+
label="Read Mesh (MeshIO)",
|
|
179
|
+
description="Loads a file with meshio through Muscat's meshio bridge.",
|
|
180
|
+
ports=(PortDefinition("mesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh"),),
|
|
181
|
+
executor=_load_meshio,
|
|
182
|
+
parameters=(
|
|
183
|
+
ParameterDefinition(
|
|
184
|
+
"path", ParameterKind.FILE, "File path", "", "/path/to/mesh", port=True
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
LOAD_MESHLANE = NodeDefinition(
|
|
191
|
+
id="load-meshlane",
|
|
192
|
+
icon="/__flowgraph_ui/LOAD_MESHLANE.svg",
|
|
193
|
+
label="Read Mesh (MeshLane)",
|
|
194
|
+
description="Loads a file with the optional MeshLane adapter.",
|
|
195
|
+
ports=(PortDefinition("mesh", PortDirection.OUTPUT, MESH_DOCUMENT, "Mesh"),),
|
|
196
|
+
executor=_load_meshlane,
|
|
197
|
+
parameters=(
|
|
198
|
+
ParameterDefinition(
|
|
199
|
+
"path", ParameterKind.FILE, "File path", "", "/path/to/mesh", port=True
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
LOAD_CSV = NodeDefinition(
|
|
206
|
+
id="load-csv",
|
|
207
|
+
icon="mdi-file-table-outline",
|
|
208
|
+
label="Read Table (Pandas)",
|
|
209
|
+
description="Loads a csv file with the optional Panda adapter.",
|
|
210
|
+
ports=(
|
|
211
|
+
PortDefinition("path", PortDirection.INPUT, STRING, "File path"),
|
|
212
|
+
PortDefinition("table", PortDirection.OUTPUT, TABLE_DOCUMENT, "Table"),
|
|
213
|
+
),
|
|
214
|
+
executor=load_csv,
|
|
215
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Workflow nodes for downloading remote files to the local temporary directory."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.error import HTTPError
|
|
11
|
+
from urllib.parse import unquote, urlparse
|
|
12
|
+
from urllib.request import urlopen
|
|
13
|
+
|
|
14
|
+
from flowgraph.adapters.data_types import STRING, ParameterKind
|
|
15
|
+
from flowgraph.application.workflow_core import (
|
|
16
|
+
NodeDefinition,
|
|
17
|
+
ParameterDefinition,
|
|
18
|
+
PortDefinition,
|
|
19
|
+
PortDirection,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
IS_PYODIDE = sys.platform == "emscripten" or "pyodide" in sys.modules
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RemoteFileDownloadError(RuntimeError):
|
|
26
|
+
"""Raised when a remote file cannot be downloaded to a temporary file."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _download_desktop_content(url: str) -> bytes:
|
|
30
|
+
"""Download URL content with CPython's blocking standard-library HTTP client."""
|
|
31
|
+
try:
|
|
32
|
+
with urlopen(url) as response:
|
|
33
|
+
return response.read()
|
|
34
|
+
except HTTPError as error:
|
|
35
|
+
raise RemoteFileDownloadError(f"Failed to fetch {url}: HTTP {error.code}") from error
|
|
36
|
+
except OSError as error:
|
|
37
|
+
raise RemoteFileDownloadError(f"Failed to fetch {url}: {error}") from error
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def download_content(url: str) -> bytes:
|
|
41
|
+
"""Download URL content in desktop Python or Pyodide.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
url:
|
|
46
|
+
HTTP(S) URL to download.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
bytes
|
|
51
|
+
The complete response body.
|
|
52
|
+
|
|
53
|
+
Raises
|
|
54
|
+
------
|
|
55
|
+
RemoteFileDownloadError
|
|
56
|
+
If the request cannot be completed successfully.
|
|
57
|
+
"""
|
|
58
|
+
if IS_PYODIDE:
|
|
59
|
+
from pyodide.http import pyfetch
|
|
60
|
+
|
|
61
|
+
response = await pyfetch(url)
|
|
62
|
+
if not response.ok:
|
|
63
|
+
raise RemoteFileDownloadError(f"Failed to fetch {url}: HTTP {response.status}")
|
|
64
|
+
return await response.bytes()
|
|
65
|
+
|
|
66
|
+
return await asyncio.to_thread(_download_desktop_content, url)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _url_suffix(url: str) -> str:
|
|
70
|
+
"""Return a safe filename suffix inferred from a URL path, if one exists."""
|
|
71
|
+
suffix = Path(unquote(urlparse(url).path)).suffix
|
|
72
|
+
return suffix if suffix and len(suffix) <= 20 else ".bin"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _download_url(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
76
|
+
"""Download the configured URL to a persistent file in the system temporary directory."""
|
|
77
|
+
url = parameters.get("url", "")
|
|
78
|
+
if not isinstance(url, str) or not url.strip():
|
|
79
|
+
raise ValueError("The download URL must be non-empty text")
|
|
80
|
+
if urlparse(url).scheme not in {"http", "https"}:
|
|
81
|
+
raise ValueError("The download URL must use HTTP or HTTPS")
|
|
82
|
+
descriptor, filename = tempfile.mkstemp(prefix="flowgraph-download-", suffix=_url_suffix(url))
|
|
83
|
+
destination = Path(filename)
|
|
84
|
+
try:
|
|
85
|
+
os.close(descriptor)
|
|
86
|
+
if IS_PYODIDE:
|
|
87
|
+
from pyodide.ffi import can_run_sync, run_sync
|
|
88
|
+
|
|
89
|
+
if not can_run_sync():
|
|
90
|
+
raise RemoteFileDownloadError(
|
|
91
|
+
"Downloading URLs in Pyodide requires JavaScript Promise Integration (JSPI)"
|
|
92
|
+
)
|
|
93
|
+
content = run_sync(download_content(url))
|
|
94
|
+
else:
|
|
95
|
+
content = _download_desktop_content(url)
|
|
96
|
+
with destination.open("wb") as output:
|
|
97
|
+
output.write(content)
|
|
98
|
+
except Exception:
|
|
99
|
+
destination.unlink(missing_ok=True)
|
|
100
|
+
raise
|
|
101
|
+
return {"path": str(destination)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
DOWNLOAD_URL = NodeDefinition(
|
|
105
|
+
id="download-url",
|
|
106
|
+
icon="mdi-cloud-upload-outline",
|
|
107
|
+
label="Download URL",
|
|
108
|
+
description="Downloads an HTTP(S) URL to a temporary file and outputs its local path.",
|
|
109
|
+
ports=(PortDefinition("path", PortDirection.OUTPUT, STRING, "File path"),),
|
|
110
|
+
executor=_download_url,
|
|
111
|
+
parameters=(
|
|
112
|
+
ParameterDefinition("url", ParameterKind.TEXT, "URL", "", "https://example.com/file.ext"),
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
AVAILABLE_NODES = (DOWNLOAD_URL,)
|
|
117
|
+
|
|
118
|
+
__all__ = ["AVAILABLE_NODES", "DOWNLOAD_URL", "RemoteFileDownloadError", "download_content"]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Simple workflow sources that produce values without reading external data."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import math
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from flowgraph.application.workflow_core import (
|
|
9
|
+
NodeDefinition,
|
|
10
|
+
ParameterDefinition,
|
|
11
|
+
PortDefinition,
|
|
12
|
+
PortDirection,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .data_types import FLOAT, INTEGER, LIST_FLOAT, LIST_INT, LIST_STR, STRING, VEC3D, ParameterKind
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _set_string(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
19
|
+
"""Output the configured string value, including an empty string."""
|
|
20
|
+
value = parameters.get("value", "")
|
|
21
|
+
if not isinstance(value, str):
|
|
22
|
+
raise TypeError("The configured string value must be text")
|
|
23
|
+
return {"value": value}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SET_STRING = NodeDefinition(
|
|
27
|
+
id="set-string",
|
|
28
|
+
icon="mdi-text-box-outline",
|
|
29
|
+
label="String Input",
|
|
30
|
+
description="Outputs a configured text value.",
|
|
31
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, STRING, "Value"),),
|
|
32
|
+
executor=_set_string,
|
|
33
|
+
parameters=(ParameterDefinition("value", ParameterKind.TEXT, "Value", "", "Enter text"),),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _set_int(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
38
|
+
"""Output the configured integer value without accepting booleans."""
|
|
39
|
+
value = parameters.get("value", 0)
|
|
40
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
41
|
+
raise TypeError("The configured integer value must be an integer")
|
|
42
|
+
return {"value": value}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
SET_INT = NodeDefinition(
|
|
46
|
+
id="set-int",
|
|
47
|
+
icon="mdi-numeric-1-box-outline",
|
|
48
|
+
label="Integer Input",
|
|
49
|
+
description="Outputs a configured integer value.",
|
|
50
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, INTEGER, "Value"),),
|
|
51
|
+
executor=_set_int,
|
|
52
|
+
parameters=(ParameterDefinition("value", ParameterKind.INTEGER, "Value", 0, "0"),),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _set_float(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
57
|
+
"""Output the configured finite floating-point value."""
|
|
58
|
+
value = parameters.get("value", 0.0)
|
|
59
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
60
|
+
raise TypeError("The configured floating-point value must be numeric")
|
|
61
|
+
normalized = float(value)
|
|
62
|
+
if not math.isfinite(normalized):
|
|
63
|
+
raise ValueError("The configured floating-point value must be finite")
|
|
64
|
+
return {"value": normalized}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
SET_FLOAT = NodeDefinition(
|
|
68
|
+
id="set-float",
|
|
69
|
+
icon="mdi-decimal-comma",
|
|
70
|
+
label="Float Input",
|
|
71
|
+
description="Outputs a configured floating-point value.",
|
|
72
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, FLOAT, "Value"),),
|
|
73
|
+
executor=_set_float,
|
|
74
|
+
parameters=(ParameterDefinition("value", ParameterKind.FLOAT, "Value", 0.0, "0.0"),),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _set_vec3d(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
79
|
+
"""Output a configured finite three-dimensional vector."""
|
|
80
|
+
components = tuple(parameters.get(axis, 0.0) for axis in ("x", "y", "z"))
|
|
81
|
+
if any(isinstance(value, bool) or not isinstance(value, (int, float)) for value in components):
|
|
82
|
+
raise TypeError("The configured vector components must be numeric")
|
|
83
|
+
|
|
84
|
+
vector = [float(value) for value in components]
|
|
85
|
+
if not all(math.isfinite(value) for value in vector):
|
|
86
|
+
raise ValueError("The configured vector components must be finite")
|
|
87
|
+
return {"value": list(vector)}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
SET_VEC3D = NodeDefinition(
|
|
91
|
+
id="set-vec3d",
|
|
92
|
+
icon="mdi-axis-arrow",
|
|
93
|
+
label="3D Vector Input",
|
|
94
|
+
description="Outputs a configured three-dimensional vector.",
|
|
95
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, VEC3D, "Value"),),
|
|
96
|
+
executor=_set_vec3d,
|
|
97
|
+
parameters=(
|
|
98
|
+
ParameterDefinition("x", ParameterKind.FLOAT, "X", 0.0, "0.0"),
|
|
99
|
+
ParameterDefinition("y", ParameterKind.FLOAT, "Y", 0.0, "0.0"),
|
|
100
|
+
ParameterDefinition("z", ParameterKind.FLOAT, "Z", 0.0, "0.0"),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_list_parameter(parameters: Mapping[str, Any]) -> list[Any]:
|
|
106
|
+
"""Parse a list parameter supplied by the UI or by a workflow file."""
|
|
107
|
+
configured = parameters.get("value", "[]")
|
|
108
|
+
try:
|
|
109
|
+
value = ast.literal_eval(configured) if isinstance(configured, str) else configured
|
|
110
|
+
except (SyntaxError, TypeError, ValueError):
|
|
111
|
+
raise TypeError(f"Unable to convert {configured!r} to a list") from None
|
|
112
|
+
if not isinstance(value, list):
|
|
113
|
+
raise TypeError(f"The configured value must be a list, not {type(value).__name__}")
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _set_str_list(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
118
|
+
"""Output a configured list of strings."""
|
|
119
|
+
value = _parse_list_parameter(parameters)
|
|
120
|
+
if any(not isinstance(item, str) for item in value):
|
|
121
|
+
raise TypeError("The configured string list must contain only text")
|
|
122
|
+
return {"value": value}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
SET_LIST_STR = NodeDefinition(
|
|
126
|
+
id="set-list[str]",
|
|
127
|
+
icon="mdi-format-list-bulleted-square",
|
|
128
|
+
label="String List Input",
|
|
129
|
+
description="Outputs a configured list of str.",
|
|
130
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, LIST_STR, "Value"),),
|
|
131
|
+
executor=_set_str_list,
|
|
132
|
+
parameters=(
|
|
133
|
+
ParameterDefinition("value", ParameterKind.LIST_STR, "Value", [], "['a', 'b', 'c']"),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _set_int_list(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
139
|
+
"""Output a configured list of integers."""
|
|
140
|
+
value = _parse_list_parameter(parameters)
|
|
141
|
+
if any(isinstance(item, bool) or not isinstance(item, int) for item in value):
|
|
142
|
+
raise TypeError("The configured integer list must contain only integers")
|
|
143
|
+
return {"value": value}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
SET_LIST_INT = NodeDefinition(
|
|
147
|
+
id="set-list[int]",
|
|
148
|
+
icon="mdi-format-list-numbered",
|
|
149
|
+
label="Integer List Input",
|
|
150
|
+
description="Outputs a configured list of int.",
|
|
151
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, LIST_INT, "Value"),),
|
|
152
|
+
executor=_set_int_list,
|
|
153
|
+
parameters=(ParameterDefinition("value", ParameterKind.LIST_INT, "Value", [], "[1, 2, 3]"),),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _set_float_list(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
158
|
+
"""Output a configured finite list of floating-point values."""
|
|
159
|
+
value = _parse_list_parameter(parameters)
|
|
160
|
+
if any(isinstance(item, bool) or not isinstance(item, (int, float)) for item in value):
|
|
161
|
+
raise TypeError("The configured float list must contain only numbers")
|
|
162
|
+
normalized = [float(item) for item in value]
|
|
163
|
+
if any(not math.isfinite(item) for item in normalized):
|
|
164
|
+
raise ValueError("The configured float list must contain only finite numbers")
|
|
165
|
+
return {"value": normalized}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
SET_LIST_FLOAT = NodeDefinition(
|
|
169
|
+
id="set-list[float]",
|
|
170
|
+
icon="mdi-format-list-numbered-rtl",
|
|
171
|
+
label="Float List Input",
|
|
172
|
+
description="Outputs a configured list of float.",
|
|
173
|
+
ports=(PortDefinition("value", PortDirection.OUTPUT, LIST_FLOAT, "Value"),),
|
|
174
|
+
executor=_set_float_list,
|
|
175
|
+
parameters=(
|
|
176
|
+
ParameterDefinition("value", ParameterKind.LIST_FLOAT, "Value", [], "[1.0, 2.0, 3.0]"),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _select_file(_inputs: Mapping[str, Any], parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
182
|
+
"""Output the configured path; selecting the path itself is a UI concern."""
|
|
183
|
+
path = parameters.get("path", "")
|
|
184
|
+
if not isinstance(path, str):
|
|
185
|
+
raise TypeError("The selected file path parameter must be text")
|
|
186
|
+
if len(path) == 0:
|
|
187
|
+
raise ValueError("Path can't be empty")
|
|
188
|
+
return {"path": path}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
AVAILABLE_NODES = (
|
|
192
|
+
SET_STRING,
|
|
193
|
+
SET_INT,
|
|
194
|
+
SET_FLOAT,
|
|
195
|
+
SET_VEC3D,
|
|
196
|
+
SET_LIST_STR,
|
|
197
|
+
SET_LIST_INT,
|
|
198
|
+
SET_LIST_FLOAT,
|
|
199
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from flowgraph.adapters.data_types import ANY, BOOLEAN, IMAGE, MESH_DOCUMENT, STRING
|
|
5
|
+
from flowgraph.application.workflow_core import (
|
|
6
|
+
NodeDefinition,
|
|
7
|
+
ParameterDefinition,
|
|
8
|
+
ParameterOption,
|
|
9
|
+
PortDefinition,
|
|
10
|
+
PortDirection,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _consume_mesh(_inputs: Mapping[str, Any], _parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
15
|
+
"""Terminate a mesh branch; the application renders this resolved input."""
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
TO_3D_VIEW = NodeDefinition(
|
|
20
|
+
id="mesh-sink",
|
|
21
|
+
icon="mdi-cube-scan",
|
|
22
|
+
label="To 3D View",
|
|
23
|
+
description="Displays an input mesh document in the 3D view.",
|
|
24
|
+
ports=(PortDefinition("mesh", PortDirection.INPUT, MESH_DOCUMENT, "Mesh"),),
|
|
25
|
+
executor=_consume_mesh,
|
|
26
|
+
parameters=(
|
|
27
|
+
ParameterDefinition(
|
|
28
|
+
"color_field",
|
|
29
|
+
STRING,
|
|
30
|
+
"Color field",
|
|
31
|
+
"",
|
|
32
|
+
"Scalar field name (empty for solid color)",
|
|
33
|
+
options=(ParameterOption("", "Solid color"),),
|
|
34
|
+
port=False,
|
|
35
|
+
),
|
|
36
|
+
ParameterDefinition(
|
|
37
|
+
"show_representation",
|
|
38
|
+
BOOLEAN,
|
|
39
|
+
"Show representation",
|
|
40
|
+
True,
|
|
41
|
+
port=False,
|
|
42
|
+
),
|
|
43
|
+
ParameterDefinition(
|
|
44
|
+
"show_axis_grid",
|
|
45
|
+
BOOLEAN,
|
|
46
|
+
"Show axis grid",
|
|
47
|
+
False,
|
|
48
|
+
port=False,
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
SHOW_IMAGE = NodeDefinition(
|
|
55
|
+
id="local-view",
|
|
56
|
+
icon="mdi-image-outline",
|
|
57
|
+
label="Show Image",
|
|
58
|
+
description="Displays an image in a viewport within this node.",
|
|
59
|
+
ports=(PortDefinition("input", PortDirection.INPUT, IMAGE, "Image"),),
|
|
60
|
+
executor=_consume_mesh,
|
|
61
|
+
presentation="local-view",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _consume_value(_inputs: Mapping[str, Any], _parameters: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
66
|
+
"""Terminate a value branch; the application renders the resolved input."""
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
DISPLAY_VALUE = NodeDefinition(
|
|
71
|
+
id="show-value",
|
|
72
|
+
icon="mdi-text-box-outline",
|
|
73
|
+
label="Show Value",
|
|
74
|
+
description="Displays the string representation of any input value on the workflow.",
|
|
75
|
+
ports=(PortDefinition("value", PortDirection.INPUT, ANY, "Value"),),
|
|
76
|
+
executor=_consume_value,
|
|
77
|
+
presentation="value",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
AVAILABLE_NODES = (TO_3D_VIEW, SHOW_IMAGE, DISPLAY_VALUE)
|