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.
Files changed (56) hide show
  1. flowgraph/__init__.py +9 -0
  2. flowgraph/__main__.py +6 -0
  3. flowgraph/adapters/__init__.py +141 -0
  4. flowgraph/adapters/controls.py +96 -0
  5. flowgraph/adapters/cosapp.py +195 -0
  6. flowgraph/adapters/data_types.py +59 -0
  7. flowgraph/adapters/documentation.py +45 -0
  8. flowgraph/adapters/field_ops.py +121 -0
  9. flowgraph/adapters/file_conversion.py +393 -0
  10. flowgraph/adapters/files.py +191 -0
  11. flowgraph/adapters/filters.py +173 -0
  12. flowgraph/adapters/image_tools.py +394 -0
  13. flowgraph/adapters/mesh_creation_tools.py +431 -0
  14. flowgraph/adapters/mesh_generations.py +58 -0
  15. flowgraph/adapters/mesh_ops.py +362 -0
  16. flowgraph/adapters/paraview.py +39 -0
  17. flowgraph/adapters/plaid_bridge.py +205 -0
  18. flowgraph/adapters/readers.py +215 -0
  19. flowgraph/adapters/remote_files.py +118 -0
  20. flowgraph/adapters/simple_sources.py +199 -0
  21. flowgraph/adapters/sinks.py +80 -0
  22. flowgraph/adapters/user_code.py +196 -0
  23. flowgraph/adapters/workflow.py +176 -0
  24. flowgraph/adapters/workflow_interfaces.py +78 -0
  25. flowgraph/adapters/writers.py +159 -0
  26. flowgraph/application/__init__.py +1 -0
  27. flowgraph/application/node_registry.py +14 -0
  28. flowgraph/application/port_conversions.py +63 -0
  29. flowgraph/application/wasm_execution.py +132 -0
  30. flowgraph/application/workflow_cli.py +417 -0
  31. flowgraph/application/workflow_core.py +1193 -0
  32. flowgraph/application/workflow_history.py +93 -0
  33. flowgraph/application/workflow_io.py +393 -0
  34. flowgraph/application/workflow_layout.py +170 -0
  35. flowgraph/application/workflow_web_catalog.py +143 -0
  36. flowgraph/application/workflows.py +7 -0
  37. flowgraph/core_cli.py +81 -0
  38. flowgraph/domain/__init__.py +5 -0
  39. flowgraph/domain/mesh_document.py +91 -0
  40. flowgraph/examples/AllFilters.py +19 -0
  41. flowgraph/examples/convert_format.py +21 -0
  42. flowgraph/resources.py +29 -0
  43. flowgraph/testdata/AllFilters.json +390 -0
  44. flowgraph/testdata/FlowGraph.h5 +0 -0
  45. flowgraph/testdata/FlowGraph.stl +0 -0
  46. flowgraph/testdata/FlowGraph.xdmf +22 -0
  47. flowgraph/testdata/SimpleReadMesh.json +70 -0
  48. flowgraph/testdata/SpherePoints.csv +83 -0
  49. flowgraph/testdata/muscat-muscat-mesh-conversion.json +101 -0
  50. flowgraph/version.py +3 -0
  51. flowgraph-0.1.2.dist-info/METADATA +150 -0
  52. flowgraph-0.1.2.dist-info/RECORD +56 -0
  53. flowgraph-0.1.2.dist-info/WHEEL +5 -0
  54. flowgraph-0.1.2.dist-info/entry_points.txt +2 -0
  55. flowgraph-0.1.2.dist-info/licenses/LICENSE +29 -0
  56. 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)