feat: expose graph still workflow in editor
This commit is contained in:
@@ -11,6 +11,7 @@ WorkflowPresetType = str
|
|||||||
|
|
||||||
_PRESET_TYPES = {
|
_PRESET_TYPES = {
|
||||||
"still",
|
"still",
|
||||||
|
"still_graph",
|
||||||
"turntable",
|
"turntable",
|
||||||
"multi_angle",
|
"multi_angle",
|
||||||
"still_with_exports",
|
"still_with_exports",
|
||||||
@@ -18,6 +19,8 @@ _PRESET_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_EXECUTION_MODES = {"legacy", "graph", "shadow"}
|
_EXECUTION_MODES = {"legacy", "graph", "shadow"}
|
||||||
|
_WORKFLOW_BLUEPRINTS = {"cad_intake", "order_rendering"}
|
||||||
|
_WORKFLOW_STARTERS = {"cad_file", "order_line"}
|
||||||
|
|
||||||
_NODE_TYPE_TO_STEP: dict[str, str] = {
|
_NODE_TYPE_TO_STEP: dict[str, str] = {
|
||||||
"inputNode": StepName.RESOLVE_STEP_PATH.value,
|
"inputNode": StepName.RESOLVE_STEP_PATH.value,
|
||||||
@@ -29,7 +32,6 @@ _NODE_TYPE_TO_STEP: dict[str, str] = {
|
|||||||
"outputNode": StepName.OUTPUT_SAVE.value,
|
"outputNode": StepName.OUTPUT_SAVE.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _make_node(
|
def _make_node(
|
||||||
node_id: str,
|
node_id: str,
|
||||||
step: StepName,
|
step: StepName,
|
||||||
@@ -91,6 +93,32 @@ def build_preset_workflow_config(
|
|||||||
{"from": "template", "to": "render"},
|
{"from": "template", "to": "render"},
|
||||||
{"from": "render", "to": "output"},
|
{"from": "render", "to": "output"},
|
||||||
]
|
]
|
||||||
|
elif preset_type == "still_graph":
|
||||||
|
nodes = [
|
||||||
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 100, label="Order Line Setup"),
|
||||||
|
_make_node("populate_materials", StepName.AUTO_POPULATE_MATERIALS, 220, 100, label="Auto Populate Materials"),
|
||||||
|
_make_node("template", StepName.RESOLVE_TEMPLATE, 440, 100, label="Resolve Template"),
|
||||||
|
_make_node("resolve_materials", StepName.MATERIAL_MAP_RESOLVE, 660, 100, label="Resolve Material Map"),
|
||||||
|
_make_node(
|
||||||
|
"render",
|
||||||
|
StepName.BLENDER_STILL,
|
||||||
|
880,
|
||||||
|
100,
|
||||||
|
params=render_params,
|
||||||
|
node_type="renderNode",
|
||||||
|
label="Still Render",
|
||||||
|
),
|
||||||
|
_make_node("output", StepName.OUTPUT_SAVE, 1100, 70, label="Save Output"),
|
||||||
|
_make_node("notify", StepName.NOTIFY, 1100, 160, label="Notify Result"),
|
||||||
|
]
|
||||||
|
edges = [
|
||||||
|
{"from": "setup", "to": "populate_materials"},
|
||||||
|
{"from": "populate_materials", "to": "template"},
|
||||||
|
{"from": "template", "to": "resolve_materials"},
|
||||||
|
{"from": "resolve_materials", "to": "render"},
|
||||||
|
{"from": "render", "to": "output"},
|
||||||
|
{"from": "render", "to": "notify"},
|
||||||
|
]
|
||||||
elif preset_type == "turntable":
|
elif preset_type == "turntable":
|
||||||
nodes = [
|
nodes = [
|
||||||
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 100, label="Order Line Setup"),
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 100, label="Order Line Setup"),
|
||||||
@@ -173,7 +201,162 @@ def build_preset_workflow_config(
|
|||||||
"edges": edges,
|
"edges": edges,
|
||||||
"ui": {
|
"ui": {
|
||||||
"preset": preset_type,
|
"preset": preset_type,
|
||||||
|
"execution_mode": "graph" if preset_type == "still_graph" else "legacy",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_workflow_blueprint_config(blueprint: str) -> dict[str, Any]:
|
||||||
|
if blueprint not in _WORKFLOW_BLUEPRINTS:
|
||||||
|
raise ValueError(f"Unknown workflow blueprint: {blueprint!r}")
|
||||||
|
|
||||||
|
if blueprint == "cad_intake":
|
||||||
|
nodes = [
|
||||||
|
_make_node("resolve_step", StepName.RESOLVE_STEP_PATH, 0, 180, label="Resolve STEP Path"),
|
||||||
|
_make_node("extract_objects", StepName.OCC_OBJECT_EXTRACT, 220, 180, label="Extract STEP Objects"),
|
||||||
|
_make_node("export_glb", StepName.OCC_GLB_EXPORT, 440, 180, label="Export GLB"),
|
||||||
|
_make_node("stl_cache", StepName.STL_CACHE_GENERATE, 660, 300, label="Generate STL Cache"),
|
||||||
|
_make_node(
|
||||||
|
"blender_thumb",
|
||||||
|
StepName.BLENDER_RENDER,
|
||||||
|
880,
|
||||||
|
120,
|
||||||
|
params={"render_engine": "cycles", "samples": 64, "width": 512, "height": 512},
|
||||||
|
node_type="renderNode",
|
||||||
|
label="Render Thumbnail (Blender)",
|
||||||
|
),
|
||||||
|
_make_node(
|
||||||
|
"threejs_thumb",
|
||||||
|
StepName.THREEJS_RENDER,
|
||||||
|
880,
|
||||||
|
320,
|
||||||
|
params={"width": 512, "height": 512, "transparent_bg": True},
|
||||||
|
node_type="renderNode",
|
||||||
|
label="Render Thumbnail (Three.js)",
|
||||||
|
),
|
||||||
|
_make_node("save_blender_thumb", StepName.THUMBNAIL_SAVE, 1100, 120, label="Save Blender Thumbnail"),
|
||||||
|
_make_node("save_threejs_thumb", StepName.THUMBNAIL_SAVE, 1100, 320, label="Save Three.js Thumbnail"),
|
||||||
|
]
|
||||||
|
edges = [
|
||||||
|
{"from": "resolve_step", "to": "extract_objects"},
|
||||||
|
{"from": "extract_objects", "to": "export_glb"},
|
||||||
|
{"from": "export_glb", "to": "stl_cache"},
|
||||||
|
{"from": "export_glb", "to": "blender_thumb"},
|
||||||
|
{"from": "export_glb", "to": "threejs_thumb"},
|
||||||
|
{"from": "blender_thumb", "to": "save_blender_thumb"},
|
||||||
|
{"from": "threejs_thumb", "to": "save_threejs_thumb"},
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
nodes = [
|
||||||
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 220, label="Order Line Setup"),
|
||||||
|
_make_node("bbox", StepName.GLB_BBOX, 220, 80, label="Compute Bounding Box"),
|
||||||
|
_make_node("resolve_materials", StepName.MATERIAL_MAP_RESOLVE, 440, 80, label="Resolve Material Map"),
|
||||||
|
_make_node("populate_materials", StepName.AUTO_POPULATE_MATERIALS, 660, 80, label="Auto Populate Materials"),
|
||||||
|
_make_node("template", StepName.RESOLVE_TEMPLATE, 880, 220, label="Resolve Template"),
|
||||||
|
_make_node(
|
||||||
|
"still_render",
|
||||||
|
StepName.BLENDER_STILL,
|
||||||
|
1120,
|
||||||
|
80,
|
||||||
|
params={"rotation_z": 0},
|
||||||
|
node_type="renderNode",
|
||||||
|
label="Render Still",
|
||||||
|
),
|
||||||
|
_make_node(
|
||||||
|
"turntable_render",
|
||||||
|
StepName.BLENDER_TURNTABLE,
|
||||||
|
1120,
|
||||||
|
220,
|
||||||
|
params={"fps": 24, "duration_s": 5},
|
||||||
|
node_type="renderFramesNode",
|
||||||
|
label="Render Turntable",
|
||||||
|
),
|
||||||
|
_make_node("blend_export", StepName.EXPORT_BLEND, 1120, 360, label="Export Blend"),
|
||||||
|
_make_node("save_still", StepName.OUTPUT_SAVE, 1360, 80, label="Save Still Output"),
|
||||||
|
_make_node("save_turntable", StepName.OUTPUT_SAVE, 1360, 220, label="Save Turntable Output"),
|
||||||
|
_make_node("notify_still", StepName.NOTIFY, 1600, 80, label="Notify Still Result"),
|
||||||
|
_make_node("notify_turntable", StepName.NOTIFY, 1600, 220, label="Notify Turntable Result"),
|
||||||
|
_make_node("notify_export", StepName.NOTIFY, 1360, 360, label="Notify Blend Export"),
|
||||||
|
]
|
||||||
|
edges = [
|
||||||
|
{"from": "setup", "to": "bbox"},
|
||||||
|
{"from": "bbox", "to": "resolve_materials"},
|
||||||
|
{"from": "resolve_materials", "to": "populate_materials"},
|
||||||
|
{"from": "populate_materials", "to": "template"},
|
||||||
|
{"from": "template", "to": "still_render"},
|
||||||
|
{"from": "template", "to": "turntable_render"},
|
||||||
|
{"from": "template", "to": "blend_export"},
|
||||||
|
{"from": "still_render", "to": "save_still"},
|
||||||
|
{"from": "turntable_render", "to": "save_turntable"},
|
||||||
|
{"from": "save_still", "to": "notify_still"},
|
||||||
|
{"from": "save_turntable", "to": "notify_turntable"},
|
||||||
|
{"from": "blend_export", "to": "notify_export"},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"nodes": nodes,
|
||||||
|
"edges": edges,
|
||||||
|
"ui": {
|
||||||
|
"preset": "custom",
|
||||||
"execution_mode": "legacy",
|
"execution_mode": "legacy",
|
||||||
|
"blueprint": blueprint,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_starter_workflow_config(family: str = "order_line") -> dict[str, Any]:
|
||||||
|
if family not in _WORKFLOW_STARTERS:
|
||||||
|
raise ValueError(f"Unknown workflow starter family: {family!r}")
|
||||||
|
|
||||||
|
if family == "cad_file":
|
||||||
|
nodes = [
|
||||||
|
_make_node("resolve_step", StepName.RESOLVE_STEP_PATH, 120, 140, label="Resolve STEP Path"),
|
||||||
|
]
|
||||||
|
blueprint = "starter_cad_intake"
|
||||||
|
else:
|
||||||
|
nodes = [
|
||||||
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 120, 140, label="Order Line Setup"),
|
||||||
|
]
|
||||||
|
blueprint = "starter_order_rendering"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"nodes": nodes,
|
||||||
|
"edges": [],
|
||||||
|
"ui": {
|
||||||
|
"preset": "custom",
|
||||||
|
"execution_mode": "legacy",
|
||||||
|
"blueprint": blueprint,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_legacy_custom_render_fallback_config(params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
render_params = _resolution_to_dimensions(params or {})
|
||||||
|
render_params.setdefault("use_custom_render_settings", True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"nodes": [
|
||||||
|
_make_node("setup", StepName.ORDER_LINE_SETUP, 0, 140, label="Order Line Setup"),
|
||||||
|
_make_node(
|
||||||
|
"render",
|
||||||
|
StepName.BLENDER_STILL,
|
||||||
|
240,
|
||||||
|
140,
|
||||||
|
params=render_params,
|
||||||
|
node_type="renderNode",
|
||||||
|
label="Still Render",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"from": "setup", "to": "render"},
|
||||||
|
],
|
||||||
|
"ui": {
|
||||||
|
"preset": "custom",
|
||||||
|
"execution_mode": "legacy",
|
||||||
|
"blueprint": "starter_order_rendering",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +364,18 @@ def build_preset_workflow_config(
|
|||||||
def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
|
def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
|
||||||
legacy_nodes = raw.get("nodes") or []
|
legacy_nodes = raw.get("nodes") or []
|
||||||
legacy_edges = raw.get("edges") or []
|
legacy_edges = raw.get("edges") or []
|
||||||
|
raw_ui = raw.get("ui")
|
||||||
|
|
||||||
|
if not legacy_nodes:
|
||||||
|
canonical = _build_legacy_custom_render_fallback_config(raw.get("params") or {})
|
||||||
|
if isinstance(raw_ui, dict):
|
||||||
|
merged_ui = dict(canonical.get("ui") or {})
|
||||||
|
merged_ui.update(raw_ui)
|
||||||
|
if merged_ui.get("execution_mode") not in _EXECUTION_MODES:
|
||||||
|
merged_ui["execution_mode"] = "legacy"
|
||||||
|
canonical["ui"] = merged_ui
|
||||||
|
return canonical
|
||||||
|
|
||||||
nodes: list[dict[str, Any]] = []
|
nodes: list[dict[str, Any]] = []
|
||||||
for legacy_node in legacy_nodes:
|
for legacy_node in legacy_nodes:
|
||||||
data = legacy_node.get("data") or {}
|
data = legacy_node.get("data") or {}
|
||||||
@@ -213,7 +408,7 @@ def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
canonical = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"nodes": nodes,
|
"nodes": nodes,
|
||||||
"edges": edges,
|
"edges": edges,
|
||||||
@@ -222,6 +417,13 @@ def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"execution_mode": "legacy",
|
"execution_mode": "legacy",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if isinstance(raw_ui, dict):
|
||||||
|
merged_ui = dict(canonical.get("ui") or {})
|
||||||
|
merged_ui.update(raw_ui)
|
||||||
|
if merged_ui.get("execution_mode") not in _EXECUTION_MODES:
|
||||||
|
merged_ui["execution_mode"] = "legacy"
|
||||||
|
canonical["ui"] = merged_ui
|
||||||
|
return canonical
|
||||||
|
|
||||||
|
|
||||||
def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
|
def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -242,11 +444,29 @@ def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
|
|||||||
if workflow_type in _PRESET_TYPES:
|
if workflow_type in _PRESET_TYPES:
|
||||||
if workflow_type == "custom":
|
if workflow_type == "custom":
|
||||||
return _canonicalize_legacy_custom_config(raw)
|
return _canonicalize_legacy_custom_config(raw)
|
||||||
return build_preset_workflow_config(workflow_type, raw.get("params") or {})
|
canonical = build_preset_workflow_config(workflow_type, raw.get("params") or {})
|
||||||
|
raw_ui = raw.get("ui")
|
||||||
|
if isinstance(raw_ui, dict):
|
||||||
|
merged_ui = dict(canonical.get("ui") or {})
|
||||||
|
merged_ui.update(raw_ui)
|
||||||
|
if merged_ui.get("execution_mode") not in _EXECUTION_MODES:
|
||||||
|
merged_ui["execution_mode"] = "legacy"
|
||||||
|
canonical["ui"] = merged_ui
|
||||||
|
return canonical
|
||||||
|
|
||||||
raise ValueError("Unsupported workflow config format")
|
raise ValueError("Unsupported workflow config format")
|
||||||
|
|
||||||
|
|
||||||
|
def workflow_config_requires_canonicalization(raw: dict[str, Any]) -> bool:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if "version" not in raw or "nodes" not in raw:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return raw != canonicalize_workflow_config(raw)
|
||||||
|
|
||||||
|
|
||||||
def get_workflow_preset_type(config: dict[str, Any]) -> str | None:
|
def get_workflow_preset_type(config: dict[str, Any]) -> str | None:
|
||||||
canonical = canonicalize_workflow_config(config)
|
canonical = canonicalize_workflow_config(config)
|
||||||
ui = canonical.get("ui") or {}
|
ui = canonical.get("ui") or {}
|
||||||
@@ -273,7 +493,7 @@ def extract_runtime_workflow(config: dict[str, Any]) -> tuple[str | None, dict[s
|
|||||||
|
|
||||||
nodes = canonical.get("nodes") or []
|
nodes = canonical.get("nodes") or []
|
||||||
|
|
||||||
if preset in {"still", "still_with_exports"}:
|
if preset in {"still", "still_graph", "still_with_exports"}:
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node.get("step") == StepName.BLENDER_STILL.value:
|
if node.get("step") == StepName.BLENDER_STILL.value:
|
||||||
return preset, _resolution_to_dimensions(node.get("params") or {})
|
return preset, _resolution_to_dimensions(node.get("params") or {})
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
from app.domains.rendering.workflow_config_utils import (
|
from app.domains.rendering.workflow_config_utils import (
|
||||||
build_preset_workflow_config,
|
build_preset_workflow_config,
|
||||||
|
build_workflow_blueprint_config,
|
||||||
|
build_starter_workflow_config,
|
||||||
canonicalize_workflow_config,
|
canonicalize_workflow_config,
|
||||||
extract_runtime_workflow,
|
extract_runtime_workflow,
|
||||||
|
get_workflow_execution_mode,
|
||||||
|
workflow_config_requires_canonicalization,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,20 +29,56 @@ def test_build_preset_workflow_config_creates_canonical_dag():
|
|||||||
assert render_node["params"]["height"] == 1080
|
assert render_node["params"]["height"] == 1080
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_preset_workflow_config_creates_graph_still_variant():
|
||||||
|
config = build_preset_workflow_config(
|
||||||
|
"still_graph",
|
||||||
|
{"render_engine": "cycles", "samples": 128, "resolution": [1600, 900]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config["version"] == 1
|
||||||
|
assert config["ui"]["preset"] == "still_graph"
|
||||||
|
assert config["ui"]["execution_mode"] == "graph"
|
||||||
|
assert [node["step"] for node in config["nodes"]] == [
|
||||||
|
"order_line_setup",
|
||||||
|
"auto_populate_materials",
|
||||||
|
"resolve_template",
|
||||||
|
"material_map_resolve",
|
||||||
|
"blender_still",
|
||||||
|
"output_save",
|
||||||
|
"notify",
|
||||||
|
]
|
||||||
|
render_node = next(node for node in config["nodes"] if node["step"] == "blender_still")
|
||||||
|
assert render_node["params"]["width"] == 1600
|
||||||
|
assert render_node["params"]["height"] == 900
|
||||||
|
assert render_node["params"]["samples"] == 128
|
||||||
|
|
||||||
|
|
||||||
def test_canonicalize_workflow_config_migrates_legacy_preset():
|
def test_canonicalize_workflow_config_migrates_legacy_preset():
|
||||||
legacy = {
|
legacy = {
|
||||||
"type": "turntable",
|
"type": "turntable",
|
||||||
"params": {"render_engine": "cycles", "samples": 64, "fps": 24},
|
"params": {"render_engine": "cycles", "samples": 64, "fps": 24},
|
||||||
|
"ui": {"execution_mode": "shadow", "label": "Legacy Turntable"},
|
||||||
}
|
}
|
||||||
|
|
||||||
canonical = canonicalize_workflow_config(legacy)
|
canonical = canonicalize_workflow_config(legacy)
|
||||||
|
|
||||||
assert canonical["version"] == 1
|
assert canonical["version"] == 1
|
||||||
assert canonical["ui"]["preset"] == "turntable"
|
assert canonical["ui"]["preset"] == "turntable"
|
||||||
assert canonical["ui"]["execution_mode"] == "legacy"
|
assert canonical["ui"]["execution_mode"] == "shadow"
|
||||||
|
assert canonical["ui"]["label"] == "Legacy Turntable"
|
||||||
assert any(node["step"] == "blender_turntable" for node in canonical["nodes"])
|
assert any(node["step"] == "blender_turntable" for node in canonical["nodes"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_workflow_execution_mode_uses_legacy_preset_ui_override():
|
||||||
|
legacy = {
|
||||||
|
"type": "still",
|
||||||
|
"params": {"width": 1024, "height": 768},
|
||||||
|
"ui": {"execution_mode": "graph"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert get_workflow_execution_mode(legacy) == "graph"
|
||||||
|
|
||||||
|
|
||||||
def test_extract_runtime_workflow_uses_canonical_render_node_params():
|
def test_extract_runtime_workflow_uses_canonical_render_node_params():
|
||||||
config = build_preset_workflow_config(
|
config = build_preset_workflow_config(
|
||||||
"multi_angle",
|
"multi_angle",
|
||||||
@@ -82,6 +122,34 @@ def test_canonicalize_legacy_custom_config_preserves_edges():
|
|||||||
assert canonical["edges"] == [{"id": "e1", "from": "input", "to": "render"}]
|
assert canonical["edges"] == [{"id": "e1", "from": "input", "to": "render"}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_canonicalize_legacy_custom_config_without_nodes_builds_render_fallback_graph():
|
||||||
|
legacy = {
|
||||||
|
"type": "custom",
|
||||||
|
"params": {
|
||||||
|
"samples": 256,
|
||||||
|
"resolution": [2048, 2048],
|
||||||
|
"render_engine": "cycles",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical = canonicalize_workflow_config(legacy)
|
||||||
|
|
||||||
|
assert canonical["version"] == 1
|
||||||
|
assert canonical["ui"]["preset"] == "custom"
|
||||||
|
assert canonical["ui"]["execution_mode"] == "legacy"
|
||||||
|
assert [node["step"] for node in canonical["nodes"]] == [
|
||||||
|
"order_line_setup",
|
||||||
|
"blender_still",
|
||||||
|
]
|
||||||
|
assert canonical["edges"] == [{"from": "setup", "to": "render"}]
|
||||||
|
render_node = next(node for node in canonical["nodes"] if node["step"] == "blender_still")
|
||||||
|
assert render_node["params"]["render_engine"] == "cycles"
|
||||||
|
assert render_node["params"]["samples"] == 256
|
||||||
|
assert render_node["params"]["width"] == 2048
|
||||||
|
assert render_node["params"]["height"] == 2048
|
||||||
|
assert render_node["params"]["use_custom_render_settings"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_extract_runtime_workflow_converts_resolution_to_dimensions():
|
def test_extract_runtime_workflow_converts_resolution_to_dimensions():
|
||||||
config = build_preset_workflow_config(
|
config = build_preset_workflow_config(
|
||||||
"turntable",
|
"turntable",
|
||||||
@@ -96,6 +164,41 @@ def test_extract_runtime_workflow_converts_resolution_to_dimensions():
|
|||||||
assert "resolution" not in params
|
assert "resolution" not in params
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_preset_workflow_config_keeps_render_overrides_opt_in_disabled_by_default():
|
||||||
|
config = build_preset_workflow_config(
|
||||||
|
"still_with_exports",
|
||||||
|
{"width": 640, "height": 640, "samples": 32},
|
||||||
|
)
|
||||||
|
|
||||||
|
render_node = next(node for node in config["nodes"] if node["step"] == "blender_still")
|
||||||
|
|
||||||
|
assert "use_custom_render_settings" not in render_node["params"]
|
||||||
|
assert render_node["params"]["width"] == 640
|
||||||
|
assert render_node["params"]["height"] == 640
|
||||||
|
assert render_node["params"]["samples"] == 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_canonicalize_workflow_config_preserves_unset_override_intent_for_saved_preset_configs():
|
||||||
|
canonical = canonicalize_workflow_config(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"nodes": [
|
||||||
|
{"id": "setup", "step": "order_line_setup", "params": {}},
|
||||||
|
{
|
||||||
|
"id": "render",
|
||||||
|
"step": "blender_still",
|
||||||
|
"params": {"width": 1024, "height": 768},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"edges": [{"from": "setup", "to": "render"}],
|
||||||
|
"ui": {"preset": "still_with_exports", "execution_mode": "graph"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
render_node = next(node for node in canonical["nodes"] if node["step"] == "blender_still")
|
||||||
|
assert "use_custom_render_settings" not in render_node["params"]
|
||||||
|
|
||||||
|
|
||||||
def test_canonicalize_workflow_config_defaults_execution_mode_for_canonical_configs():
|
def test_canonicalize_workflow_config_defaults_execution_mode_for_canonical_configs():
|
||||||
canonical = canonicalize_workflow_config(
|
canonical = canonicalize_workflow_config(
|
||||||
{
|
{
|
||||||
@@ -110,3 +213,68 @@ def test_canonicalize_workflow_config_defaults_execution_mode_for_canonical_conf
|
|||||||
|
|
||||||
assert canonical["ui"]["preset"] == "custom"
|
assert canonical["ui"]["preset"] == "custom"
|
||||||
assert canonical["ui"]["execution_mode"] == "legacy"
|
assert canonical["ui"]["execution_mode"] == "legacy"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_config_requires_canonicalization_for_legacy_payloads():
|
||||||
|
assert workflow_config_requires_canonicalization(
|
||||||
|
{
|
||||||
|
"type": "still",
|
||||||
|
"params": {"width": 1024, "height": 768},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_config_requires_canonicalization_skips_already_canonical_configs():
|
||||||
|
config = build_preset_workflow_config("still", {"width": 1024, "height": 768})
|
||||||
|
|
||||||
|
assert workflow_config_requires_canonicalization(config) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_workflow_blueprint_config_creates_cad_intake_family_graph():
|
||||||
|
config = build_workflow_blueprint_config("cad_intake")
|
||||||
|
|
||||||
|
assert config["version"] == 1
|
||||||
|
assert config["ui"]["preset"] == "custom"
|
||||||
|
assert config["ui"]["blueprint"] == "cad_intake"
|
||||||
|
assert [node["step"] for node in config["nodes"]] == [
|
||||||
|
"resolve_step_path",
|
||||||
|
"occ_object_extract",
|
||||||
|
"occ_glb_export",
|
||||||
|
"stl_cache_generate",
|
||||||
|
"blender_render",
|
||||||
|
"threejs_render",
|
||||||
|
"thumbnail_save",
|
||||||
|
"thumbnail_save",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_workflow_blueprint_config_creates_order_rendering_family_graph():
|
||||||
|
config = build_workflow_blueprint_config("order_rendering")
|
||||||
|
|
||||||
|
assert config["version"] == 1
|
||||||
|
assert config["ui"]["preset"] == "custom"
|
||||||
|
assert config["ui"]["blueprint"] == "order_rendering"
|
||||||
|
assert any(node["step"] == "blender_still" for node in config["nodes"])
|
||||||
|
assert any(node["step"] == "blender_turntable" for node in config["nodes"])
|
||||||
|
assert any(node["step"] == "export_blend" for node in config["nodes"])
|
||||||
|
assert sum(1 for node in config["nodes"] if node["step"] == "notify") == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_starter_workflow_config_creates_minimal_valid_custom_graph():
|
||||||
|
config = build_starter_workflow_config()
|
||||||
|
|
||||||
|
assert config["version"] == 1
|
||||||
|
assert config["ui"]["preset"] == "custom"
|
||||||
|
assert config["ui"]["blueprint"] == "starter_order_rendering"
|
||||||
|
assert config["nodes"] == [
|
||||||
|
{
|
||||||
|
"id": "setup",
|
||||||
|
"step": "order_line_setup",
|
||||||
|
"params": {},
|
||||||
|
"ui": {
|
||||||
|
"type": "processNode",
|
||||||
|
"position": {"x": 120, "y": 140},
|
||||||
|
"label": "Order Line Setup",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
|
||||||
|
import { createPresetWorkflowConfig } from '../../api/workflows'
|
||||||
|
|
||||||
|
describe('workflow preset config builders', () => {
|
||||||
|
test('builds a non-legacy still graph preset', () => {
|
||||||
|
const config = createPresetWorkflowConfig('still_graph', {
|
||||||
|
render_engine: 'cycles',
|
||||||
|
samples: 128,
|
||||||
|
resolution: [1600, 900],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.ui?.preset).toBe('still_graph')
|
||||||
|
expect(config.ui?.execution_mode).toBe('graph')
|
||||||
|
expect(config.nodes.map(node => node.step)).toEqual([
|
||||||
|
'order_line_setup',
|
||||||
|
'auto_populate_materials',
|
||||||
|
'resolve_template',
|
||||||
|
'material_map_resolve',
|
||||||
|
'blender_still',
|
||||||
|
'output_save',
|
||||||
|
'notify',
|
||||||
|
])
|
||||||
|
expect(config.nodes.find(node => node.step === 'blender_still')?.params).toMatchObject({
|
||||||
|
render_engine: 'cycles',
|
||||||
|
samples: 128,
|
||||||
|
width: 1600,
|
||||||
|
height: 900,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import api from './client'
|
import api from './client'
|
||||||
|
|
||||||
export type WorkflowPresetType = 'still' | 'turntable' | 'multi_angle' | 'still_with_exports' | 'custom'
|
export type WorkflowPresetType = 'still' | 'still_graph' | 'turntable' | 'multi_angle' | 'still_with_exports' | 'custom'
|
||||||
export type WorkflowExecutionMode = 'legacy' | 'graph' | 'shadow'
|
export type WorkflowExecutionMode = 'legacy' | 'graph' | 'shadow'
|
||||||
|
|
||||||
export interface WorkflowDefinition {
|
export interface WorkflowDefinition {
|
||||||
@@ -21,6 +21,7 @@ export interface WorkflowConfig {
|
|||||||
|
|
||||||
export interface WorkflowParams {
|
export interface WorkflowParams {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
|
use_custom_render_settings?: boolean
|
||||||
render_engine?: 'cycles' | 'eevee'
|
render_engine?: 'cycles' | 'eevee'
|
||||||
samples?: number
|
samples?: number
|
||||||
resolution?: [number, number]
|
resolution?: [number, number]
|
||||||
@@ -53,6 +54,7 @@ export interface WorkflowEdge {
|
|||||||
export interface WorkflowUi {
|
export interface WorkflowUi {
|
||||||
preset?: WorkflowPresetType
|
preset?: WorkflowPresetType
|
||||||
execution_mode?: WorkflowExecutionMode
|
execution_mode?: WorkflowExecutionMode
|
||||||
|
blueprint?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowCreate {
|
export interface WorkflowCreate {
|
||||||
@@ -67,6 +69,7 @@ export interface WorkflowRun {
|
|||||||
workflow_def_id: string | null
|
workflow_def_id: string | null
|
||||||
order_line_id: string | null
|
order_line_id: string | null
|
||||||
celery_task_id: string | null
|
celery_task_id: string | null
|
||||||
|
execution_mode: WorkflowExecutionMode
|
||||||
status: 'pending' | 'running' | 'completed' | 'failed'
|
status: 'pending' | 'running' | 'completed' | 'failed'
|
||||||
started_at: string | null
|
started_at: string | null
|
||||||
completed_at: string | null
|
completed_at: string | null
|
||||||
@@ -85,6 +88,84 @@ export interface WorkflowNodeResult {
|
|||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowDispatchResponse {
|
||||||
|
workflow_run: WorkflowRun
|
||||||
|
context_id: string
|
||||||
|
execution_mode: WorkflowExecutionMode
|
||||||
|
dispatched: number
|
||||||
|
task_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowPreflightIssue {
|
||||||
|
severity: 'error' | 'warning' | 'info'
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
node_id: string | null
|
||||||
|
step: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowPreflightNode {
|
||||||
|
node_id: string
|
||||||
|
step: string
|
||||||
|
label: string | null
|
||||||
|
execution_kind: WorkflowNodeExecutionKind
|
||||||
|
supported: boolean
|
||||||
|
status: 'ready' | 'warning' | 'error' | 'unsupported'
|
||||||
|
issues: WorkflowPreflightIssue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowPreflightResponse {
|
||||||
|
workflow_id: string | null
|
||||||
|
context_id: string
|
||||||
|
context_kind: 'order_line' | 'cad_file' | null
|
||||||
|
expected_context_kind: 'order_line' | 'cad_file'
|
||||||
|
execution_mode: WorkflowExecutionMode
|
||||||
|
graph_dispatch_allowed: boolean
|
||||||
|
summary: string
|
||||||
|
resolved_order_line_id: string | null
|
||||||
|
resolved_cad_file_id: string | null
|
||||||
|
unsupported_node_ids: string[]
|
||||||
|
issues: WorkflowPreflightIssue[]
|
||||||
|
nodes: WorkflowPreflightNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowDraftPreflightRequest {
|
||||||
|
workflow_id?: string | null
|
||||||
|
context_id: string
|
||||||
|
config: WorkflowConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowDraftDispatchRequest {
|
||||||
|
workflow_id?: string | null
|
||||||
|
context_id: string
|
||||||
|
config: WorkflowConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowComparisonArtifact {
|
||||||
|
path: string | null
|
||||||
|
storage_key: string | null
|
||||||
|
exists: boolean
|
||||||
|
file_size_bytes: number | null
|
||||||
|
sha256: string | null
|
||||||
|
mime_type: string | null
|
||||||
|
image_width: number | null
|
||||||
|
image_height: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowRunComparison {
|
||||||
|
workflow_run_id: string
|
||||||
|
workflow_def_id: string | null
|
||||||
|
order_line_id: string | null
|
||||||
|
execution_mode: WorkflowExecutionMode
|
||||||
|
status: string
|
||||||
|
summary: string
|
||||||
|
authoritative_output: WorkflowComparisonArtifact
|
||||||
|
observer_output: WorkflowComparisonArtifact
|
||||||
|
exact_match: boolean | null
|
||||||
|
dimensions_match: boolean | null
|
||||||
|
mean_pixel_delta: number | null
|
||||||
|
}
|
||||||
|
|
||||||
export const getWorkflows = (): Promise<WorkflowDefinition[]> =>
|
export const getWorkflows = (): Promise<WorkflowDefinition[]> =>
|
||||||
api.get('/workflows').then(r => r.data.map(normalizeWorkflowDefinition))
|
api.get('/workflows').then(r => r.data.map(normalizeWorkflowDefinition))
|
||||||
|
|
||||||
@@ -103,10 +184,35 @@ export const deleteWorkflow = (id: string): Promise<void> =>
|
|||||||
export const getWorkflowRuns = (workflowId: string): Promise<WorkflowRun[]> =>
|
export const getWorkflowRuns = (workflowId: string): Promise<WorkflowRun[]> =>
|
||||||
api.get(`/workflows/${workflowId}/runs`).then(r => r.data)
|
api.get(`/workflows/${workflowId}/runs`).then(r => r.data)
|
||||||
|
|
||||||
|
export const dispatchWorkflow = (
|
||||||
|
workflowId: string,
|
||||||
|
contextId: string,
|
||||||
|
): Promise<WorkflowDispatchResponse> =>
|
||||||
|
api.post(`/workflows/${workflowId}/dispatch`, undefined, { params: { context_id: contextId } }).then(r => r.data)
|
||||||
|
|
||||||
|
export const dispatchWorkflowDraft = (
|
||||||
|
data: WorkflowDraftDispatchRequest,
|
||||||
|
): Promise<WorkflowDispatchResponse> =>
|
||||||
|
api.post('/workflows/dispatch', data).then(r => r.data)
|
||||||
|
|
||||||
|
export const preflightWorkflow = (
|
||||||
|
workflowId: string,
|
||||||
|
contextId: string,
|
||||||
|
): Promise<WorkflowPreflightResponse> =>
|
||||||
|
api.get(`/workflows/${workflowId}/preflight`, { params: { context_id: contextId } }).then(r => r.data)
|
||||||
|
|
||||||
|
export const preflightWorkflowDraft = (
|
||||||
|
data: WorkflowDraftPreflightRequest,
|
||||||
|
): Promise<WorkflowPreflightResponse> =>
|
||||||
|
api.post('/workflows/preflight', data).then(r => r.data)
|
||||||
|
|
||||||
|
export const getWorkflowRunComparison = (runId: string): Promise<WorkflowRunComparison> =>
|
||||||
|
api.get(`/workflows/runs/${runId}/comparison`).then(r => r.data)
|
||||||
|
|
||||||
// ─── Node Definitions / Pipeline Steps ───────────────────────────────────────
|
// ─── Node Definitions / Pipeline Steps ───────────────────────────────────────
|
||||||
|
|
||||||
export type StepCategory = 'input' | 'processing' | 'rendering' | 'output'
|
export type StepCategory = 'input' | 'processing' | 'rendering' | 'output'
|
||||||
export type WorkflowNodeFieldType = 'number' | 'select' | 'boolean'
|
export type WorkflowNodeFieldType = 'number' | 'select' | 'boolean' | 'text'
|
||||||
export type WorkflowNodeExecutionKind = 'native' | 'bridge'
|
export type WorkflowNodeExecutionKind = 'native' | 'bridge'
|
||||||
|
|
||||||
export interface WorkflowNodeFieldOption {
|
export interface WorkflowNodeFieldOption {
|
||||||
@@ -189,6 +295,40 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'still_graph') {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
ui: { preset: type, execution_mode: 'graph' },
|
||||||
|
nodes: [
|
||||||
|
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
|
||||||
|
{
|
||||||
|
id: 'populate_materials',
|
||||||
|
step: 'auto_populate_materials',
|
||||||
|
params: {},
|
||||||
|
ui: { type: 'processNode', label: 'Auto Populate Materials', position: { x: 220, y: 100 } },
|
||||||
|
},
|
||||||
|
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 440, y: 100 } } },
|
||||||
|
{
|
||||||
|
id: 'resolve_materials',
|
||||||
|
step: 'material_map_resolve',
|
||||||
|
params: {},
|
||||||
|
ui: { type: 'processNode', label: 'Resolve Material Map', position: { x: 660, y: 100 } },
|
||||||
|
},
|
||||||
|
{ id: 'render', step: 'blender_still', params: renderParams, ui: { type: 'renderNode', label: 'Still Render', position: { x: 880, y: 100 } } },
|
||||||
|
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 1100, y: 70 } } },
|
||||||
|
{ id: 'notify', step: 'notify', params: {}, ui: { type: 'outputNode', label: 'Notify Result', position: { x: 1100, y: 160 } } },
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ from: 'setup', to: 'populate_materials' },
|
||||||
|
{ from: 'populate_materials', to: 'template' },
|
||||||
|
{ from: 'template', to: 'resolve_materials' },
|
||||||
|
{ from: 'resolve_materials', to: 'render' },
|
||||||
|
{ from: 'render', to: 'output' },
|
||||||
|
{ from: 'render', to: 'notify' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'turntable') {
|
if (type === 'turntable') {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|||||||
+1205
-119
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user