combine save and save layers
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.execution_context import emit_warning, emit_file_download
|
||||
@@ -11,9 +12,15 @@ from backend.exporters import (
|
||||
resolve_path,
|
||||
type_name_for_value,
|
||||
)
|
||||
from backend.nodes.helpers import _MAX_SAVE_FIELDS
|
||||
|
||||
DOWNLOAD_DIR = Path(tempfile.gettempdir()) / "tono-downloads"
|
||||
|
||||
# Source types that expand into a layer stack (i.e., the Save node grows
|
||||
# extra field_N inputs). Any other type (FLOAT, LINE, MESH, …) is a single
|
||||
# value; no stacking UI is shown.
|
||||
_STACKABLE_SOURCE_TYPES: tuple[str, ...] = ("DATA_FIELD", "IMAGE", "ANNOTATION_SOURCE")
|
||||
|
||||
|
||||
def _choices_by_source_type() -> dict[str, list[str]]:
|
||||
"""Build the format dropdown's source-type map from the exporter registry.
|
||||
@@ -39,6 +46,43 @@ class Save:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
choices = _choices_by_source_type()
|
||||
|
||||
optional: dict[str, Any] = {
|
||||
"plot_title": ("STRING", {
|
||||
"default": "",
|
||||
"placeholder": "plot title (optional)",
|
||||
"label": "title",
|
||||
"show_when_source_type": {"value": ["LINE"]},
|
||||
}),
|
||||
# Name widget for the primary (value) layer. Only surfaces once
|
||||
# the stack grows beyond one layer, so single-value saves stay
|
||||
# clutter-free.
|
||||
"primary_name": ("STRING", {
|
||||
"default": "",
|
||||
"placeholder": "name",
|
||||
"show_when_input_visible": "field_0",
|
||||
"inline_with_input": "value",
|
||||
"hide_label": True,
|
||||
}),
|
||||
}
|
||||
# Extra layer sockets for stackable source types. The frontend
|
||||
# progressive-reveal block keys off `field_N` and only shows slot N
|
||||
# once slot N-1 is connected; we further gate every slot on `value`
|
||||
# being a stackable source type via `show_when_source_type`.
|
||||
for i in range(_MAX_SAVE_FIELDS):
|
||||
optional[f"field_{i}"] = ("DATA_FIELD", {
|
||||
"label": f"layer {i + 2}", # primary is layer 1
|
||||
"accepted_types": ["IMAGE", "ANNOTATION_SOURCE"],
|
||||
"show_when_source_type": {"value": list(_STACKABLE_SOURCE_TYPES)},
|
||||
})
|
||||
optional[f"layer_name_{i}"] = ("STRING", {
|
||||
"default": "",
|
||||
"placeholder": "name",
|
||||
"show_when_input_visible": f"field_{i}",
|
||||
"inline_with_input": f"field_{i}",
|
||||
"hide_label": True,
|
||||
})
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"filename": ("STRING", {
|
||||
@@ -64,14 +108,7 @@ class Save:
|
||||
"source_type_input": "value",
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"plot_title": ("STRING", {
|
||||
"default": "",
|
||||
"placeholder": "plot title (optional)",
|
||||
"label": "title",
|
||||
"show_when_source_type": {"value": ["LINE"]},
|
||||
}),
|
||||
},
|
||||
"optional": optional,
|
||||
}
|
||||
|
||||
OUTPUTS = ()
|
||||
@@ -80,12 +117,18 @@ class Save:
|
||||
OUTPUT_NODE = True
|
||||
MANUAL_TRIGGER = True
|
||||
DESCRIPTION = (
|
||||
"Save a single graph value to disk. Supports fields, images, lines, tables, scalars, "
|
||||
"and 3D meshes. Use 'GWY' or 'TIFF (data)' for DataFields you want to re-open later "
|
||||
"with their physical units preserved."
|
||||
"Save one or more graph values to disk. A single value works for every type "
|
||||
"(fields, images, lines, tables, scalars, meshes). For DataFields and Images, "
|
||||
"additional layer slots appear as you connect each one, letting you write "
|
||||
"multi-channel TIFF, NPZ, GWY, or HDF5 stacks from a single node. "
|
||||
"Use 'GWY' or 'TIFF (data)' when you need to re-open the result with its "
|
||||
"physical units preserved."
|
||||
)
|
||||
|
||||
KEYWORDS = ("export", "write", "download", "png", "tiff", "csv", "json", "npz", "obj", "stl", "gwy")
|
||||
KEYWORDS = (
|
||||
"export", "write", "download", "png", "tiff", "csv", "json", "npz",
|
||||
"obj", "stl", "gwy", "hdf5", "layers", "stack", "channels",
|
||||
)
|
||||
|
||||
def save(
|
||||
self,
|
||||
@@ -93,12 +136,62 @@ class Save:
|
||||
format: str,
|
||||
value,
|
||||
plot_title: str = "",
|
||||
primary_name: str = "",
|
||||
**kwargs,
|
||||
):
|
||||
type_name = type_name_for_value(value)
|
||||
module, spec = get_exporter(type_name, format)
|
||||
path = resolve_path(filename, spec, DOWNLOAD_DIR)
|
||||
module.save(path, value, format, plot_title=plot_title)
|
||||
|
||||
extra_layers, layer_names = self._collect_extra_layers(
|
||||
type_name, primary_name, kwargs,
|
||||
)
|
||||
|
||||
module.save(
|
||||
path,
|
||||
value,
|
||||
format,
|
||||
plot_title=plot_title,
|
||||
extra_layers=extra_layers,
|
||||
layer_names=layer_names,
|
||||
)
|
||||
|
||||
emit_warning(f"Saved to {path.name}")
|
||||
emit_file_download(str(path))
|
||||
return ()
|
||||
|
||||
def _collect_extra_layers(
|
||||
self,
|
||||
type_name: str,
|
||||
primary_name: str,
|
||||
kwargs: dict[str, Any],
|
||||
) -> tuple[list[Any], list[str]]:
|
||||
"""Pull field_N + layer_name_N from kwargs into parallel lists.
|
||||
|
||||
Only applies when the primary value is a stackable source type; for
|
||||
anything else (LINE, FLOAT, MESH_MODEL, tables) any stray field_N
|
||||
kwargs are ignored — the frontend hides those sockets in that case
|
||||
and the backend treats it as a single-value save.
|
||||
"""
|
||||
if type_name not in _STACKABLE_SOURCE_TYPES:
|
||||
return [], []
|
||||
|
||||
extras: list[Any] = []
|
||||
extra_names: list[str] = []
|
||||
# Preserve the on-node order: iterate field_0, field_1, …, stopping at
|
||||
# the first hole. An unconnected slot in the middle would be a UI bug,
|
||||
# but bailing early keeps the saved stack matching what the user sees.
|
||||
for i in range(_MAX_SAVE_FIELDS):
|
||||
layer = kwargs.get(f"field_{i}")
|
||||
if layer is None:
|
||||
break
|
||||
extras.append(layer)
|
||||
extra_names.append(str(kwargs.get(f"layer_name_{i}", "") or "").strip())
|
||||
|
||||
if not extras:
|
||||
return [], []
|
||||
|
||||
# Full names list starts with the primary's name (empty → exporter
|
||||
# substitutes path.stem) and then each extra in order.
|
||||
names = [str(primary_name or "").strip(), *extra_names]
|
||||
return extras, names
|
||||
|
||||
Reference in New Issue
Block a user