1167 lines
41 KiB
Python
1167 lines
41 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.process_steps import StepName
|
|
|
|
|
|
StepCategory = Literal["input", "processing", "rendering", "output"]
|
|
FieldType = Literal["number", "select", "boolean", "text"]
|
|
ExecutionKind = Literal["native", "bridge"]
|
|
WorkflowNodeFamily = Literal["cad_file", "order_line", "shared"]
|
|
TextFormat = Literal[
|
|
"plain",
|
|
"uuid",
|
|
"absolute_path",
|
|
"absolute_blend_path",
|
|
"absolute_glb_path",
|
|
"float_string",
|
|
"hex_color",
|
|
"safe_filename_suffix",
|
|
]
|
|
|
|
|
|
class WorkflowNodeFieldOption(BaseModel):
|
|
value: str | int | float | bool
|
|
label: str
|
|
|
|
|
|
class WorkflowNodeFieldDefinition(BaseModel):
|
|
key: str
|
|
label: str
|
|
type: FieldType
|
|
description: str = ""
|
|
section: str = "General"
|
|
default: Any = None
|
|
min: float | None = None
|
|
max: float | None = None
|
|
step: float | None = None
|
|
unit: str | None = None
|
|
options: list[WorkflowNodeFieldOption] = []
|
|
allow_blank: bool = True
|
|
max_length: int | None = None
|
|
text_format: TextFormat = "plain"
|
|
|
|
|
|
class WorkflowNodeDefinition(BaseModel):
|
|
step: str
|
|
label: str
|
|
family: WorkflowNodeFamily
|
|
module_key: str
|
|
category: StepCategory
|
|
description: str
|
|
node_type: str
|
|
icon: str
|
|
defaults: dict[str, Any] = {}
|
|
fields: list[WorkflowNodeFieldDefinition] = []
|
|
execution_kind: ExecutionKind = "native"
|
|
legacy_compatible: bool = True
|
|
input_contract: dict[str, Any] = {}
|
|
output_contract: dict[str, Any] = {}
|
|
artifact_roles_produced: list[str] = []
|
|
artifact_roles_consumed: list[str] = []
|
|
legacy_source: str | None = None
|
|
|
|
|
|
def _field(
|
|
key: str,
|
|
label: str,
|
|
field_type: FieldType,
|
|
*,
|
|
description: str = "",
|
|
section: str = "General",
|
|
default: Any = None,
|
|
min: float | None = None,
|
|
max: float | None = None,
|
|
step: float | None = None,
|
|
unit: str | None = None,
|
|
options: list[tuple[str | int | float | bool, str]] | None = None,
|
|
allow_blank: bool = True,
|
|
max_length: int | None = None,
|
|
text_format: TextFormat = "plain",
|
|
) -> WorkflowNodeFieldDefinition:
|
|
return WorkflowNodeFieldDefinition(
|
|
key=key,
|
|
label=label,
|
|
type=field_type,
|
|
description=description,
|
|
section=section,
|
|
default=default,
|
|
min=min,
|
|
max=max,
|
|
step=step,
|
|
unit=unit,
|
|
options=[
|
|
WorkflowNodeFieldOption(value=value, label=option_label)
|
|
for value, option_label in (options or [])
|
|
],
|
|
allow_blank=allow_blank,
|
|
max_length=max_length,
|
|
text_format=text_format,
|
|
)
|
|
|
|
|
|
def _definition(
|
|
step: StepName,
|
|
label: str,
|
|
family: WorkflowNodeFamily,
|
|
module_key: str,
|
|
category: StepCategory,
|
|
description: str,
|
|
*,
|
|
node_type: str,
|
|
icon: str,
|
|
defaults: dict[str, Any] | None = None,
|
|
fields: list[WorkflowNodeFieldDefinition] | None = None,
|
|
execution_kind: ExecutionKind = "native",
|
|
input_contract: dict[str, Any] | None = None,
|
|
output_contract: dict[str, Any] | None = None,
|
|
artifact_roles_produced: list[str] | None = None,
|
|
artifact_roles_consumed: list[str] | None = None,
|
|
legacy_source: str | None = None,
|
|
) -> WorkflowNodeDefinition:
|
|
return WorkflowNodeDefinition(
|
|
step=step.value,
|
|
label=label,
|
|
family=family,
|
|
module_key=module_key,
|
|
category=category,
|
|
description=description,
|
|
node_type=node_type,
|
|
icon=icon,
|
|
defaults=defaults or {},
|
|
fields=fields or [],
|
|
execution_kind=execution_kind,
|
|
input_contract=input_contract or {},
|
|
output_contract=output_contract or {},
|
|
artifact_roles_produced=artifact_roles_produced or [],
|
|
artifact_roles_consumed=artifact_roles_consumed or [],
|
|
legacy_source=legacy_source or f"legacy_step:{step.value}",
|
|
)
|
|
|
|
|
|
_BLENDER_ENGINE_OPTIONS = [("cycles", "Cycles"), ("eevee", "EEVEE")]
|
|
_CYCLES_DEVICE_OPTIONS = [("auto", "Auto"), ("gpu", "GPU"), ("cpu", "CPU")]
|
|
_GPU_TOGGLE_OPTIONS = [("", "Inherited / Default"), ("1", "Enabled"), ("0", "Disabled")]
|
|
_TURNTABLE_AXIS_OPTIONS = [
|
|
("world_z", "World Z"),
|
|
("world_x", "World X"),
|
|
("world_y", "World Y"),
|
|
]
|
|
|
|
|
|
_NODE_DEFINITIONS: list[WorkflowNodeDefinition] = [
|
|
_definition(
|
|
StepName.RESOLVE_STEP_PATH,
|
|
"Resolve STEP Path",
|
|
"cad_file",
|
|
"cad.resolve_step_path",
|
|
"input",
|
|
"Locate the STEP file on disk from the CAD file record.",
|
|
node_type="inputNode",
|
|
icon="file-up",
|
|
execution_kind="bridge",
|
|
input_contract={"context": "cad_file", "requires": ["cad_file_record"]},
|
|
output_contract={"context": "cad_file", "provides": ["step_path", "step_file_hash"]},
|
|
artifact_roles_produced=["step_path"],
|
|
),
|
|
_definition(
|
|
StepName.OCC_OBJECT_EXTRACT,
|
|
"Extract STEP Objects",
|
|
"cad_file",
|
|
"cad.extract_objects",
|
|
"processing",
|
|
"Extract part objects and metadata from the STEP file via OCC/cadquery.",
|
|
node_type="processNode",
|
|
icon="layers",
|
|
execution_kind="bridge",
|
|
input_contract={"context": "cad_file", "requires": ["step_path"]},
|
|
output_contract={"context": "cad_file", "provides": ["cad_objects", "cad_metadata"]},
|
|
artifact_roles_consumed=["step_path"],
|
|
artifact_roles_produced=["cad_objects", "cad_metadata"],
|
|
),
|
|
_definition(
|
|
StepName.OCC_GLB_EXPORT,
|
|
"Export GLB",
|
|
"cad_file",
|
|
"cad.export_glb",
|
|
"processing",
|
|
"Convert STEP geometry into GLB for previews and downstream rendering. Uses the system tessellation profile; this node does not expose per-node overrides yet.",
|
|
node_type="processNode",
|
|
icon="refresh-cw",
|
|
execution_kind="bridge",
|
|
input_contract={"context": "cad_file", "requires": ["step_path"]},
|
|
output_contract={"context": "cad_file", "provides": ["glb_preview"]},
|
|
artifact_roles_consumed=["step_path"],
|
|
artifact_roles_produced=["glb_preview"],
|
|
),
|
|
_definition(
|
|
StepName.GLB_BBOX,
|
|
"Compute Bounding Box",
|
|
"shared",
|
|
"geometry.compute_bbox",
|
|
"processing",
|
|
"Compute the model bounding box from a prepared GLB artifact for framing decisions in either CAD-intake or order-line workflows.",
|
|
node_type="processNode",
|
|
icon="layers",
|
|
execution_kind="bridge",
|
|
fields=[
|
|
_field(
|
|
"glb_path",
|
|
"GLB Path Override",
|
|
"text",
|
|
description="Optional absolute path to a specific GLB file. Leave empty to reuse the prepared preview/export artifact automatically.",
|
|
section="Inputs",
|
|
default="",
|
|
text_format="absolute_glb_path",
|
|
),
|
|
_field(
|
|
"source_preference",
|
|
"Source Preference",
|
|
"select",
|
|
description="Prefer a prepared GLB, force STEP fallback, or fail when no GLB artifact is available.",
|
|
section="Inputs",
|
|
default="auto",
|
|
options=[
|
|
("auto", "Auto"),
|
|
("step_only", "STEP Only"),
|
|
("glb_only", "GLB Only"),
|
|
],
|
|
),
|
|
],
|
|
input_contract={"requires": ["glb_preview"]},
|
|
output_contract={"provides": ["bbox"]},
|
|
artifact_roles_consumed=["glb_preview"],
|
|
artifact_roles_produced=["bbox"],
|
|
),
|
|
_definition(
|
|
StepName.MATERIAL_MAP_RESOLVE,
|
|
"Resolve Material Map",
|
|
"order_line",
|
|
"materials.resolve_map",
|
|
"processing",
|
|
"Map raw part material names to HartOMat material records via aliases.",
|
|
node_type="processNode",
|
|
icon="layers",
|
|
execution_kind="bridge",
|
|
defaults={"disable_materials": False, "material_override": ""},
|
|
fields=[
|
|
_field(
|
|
"disable_materials",
|
|
"Disable Materials",
|
|
"boolean",
|
|
description="Bypass template and alias-based material mapping for this node.",
|
|
section="Materials",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"material_override",
|
|
"Material Override",
|
|
"text",
|
|
description="Optional material name forced onto every detected part before rendering.",
|
|
section="Materials",
|
|
default="",
|
|
),
|
|
],
|
|
input_contract={"context": "order_line", "requires": ["order_line_context", "cad_materials"]},
|
|
output_contract={"context": "order_line", "provides": ["material_assignments"]},
|
|
artifact_roles_consumed=["order_line_context", "cad_materials"],
|
|
artifact_roles_produced=["material_assignments"],
|
|
),
|
|
_definition(
|
|
StepName.AUTO_POPULATE_MATERIALS,
|
|
"Auto Populate Materials",
|
|
"order_line",
|
|
"materials.auto_populate",
|
|
"processing",
|
|
"Create missing material records for newly discovered part materials.",
|
|
node_type="processNode",
|
|
icon="layers",
|
|
execution_kind="bridge",
|
|
defaults={
|
|
"persist_updates": True,
|
|
"refresh_material_source": True,
|
|
"include_populated_products": False,
|
|
},
|
|
fields=[
|
|
_field(
|
|
"persist_updates",
|
|
"Persist Updates",
|
|
"boolean",
|
|
description="Write discovered part-material mappings back to product records in graph mode.",
|
|
section="Behavior",
|
|
default=True,
|
|
),
|
|
_field(
|
|
"refresh_material_source",
|
|
"Refresh Material Source",
|
|
"boolean",
|
|
description="Reload product material mappings into the workflow context after persistence.",
|
|
section="Behavior",
|
|
default=True,
|
|
),
|
|
_field(
|
|
"include_populated_products",
|
|
"Rewrite Populated Products",
|
|
"boolean",
|
|
description="Also rebuild material mappings for products that already have non-empty assignments.",
|
|
section="Behavior",
|
|
default=False,
|
|
),
|
|
],
|
|
input_contract={"context": "order_line", "requires": ["cad_materials"]},
|
|
output_contract={"context": "order_line", "provides": ["material_catalog_updates"]},
|
|
artifact_roles_consumed=["cad_materials"],
|
|
artifact_roles_produced=["material_catalog_updates"],
|
|
),
|
|
_definition(
|
|
StepName.BLENDER_RENDER,
|
|
"Render Thumbnail (Blender)",
|
|
"cad_file",
|
|
"render.thumbnail.blender",
|
|
"rendering",
|
|
"Render a thumbnail image with Blender.",
|
|
node_type="renderNode",
|
|
icon="camera",
|
|
defaults={"render_engine": "cycles", "samples": 64, "width": 512, "height": 512},
|
|
fields=[
|
|
_field(
|
|
"render_engine",
|
|
"Render Engine",
|
|
"select",
|
|
description="Renderer backend for this Blender job.",
|
|
section="Render",
|
|
default="cycles",
|
|
options=_BLENDER_ENGINE_OPTIONS,
|
|
),
|
|
_field(
|
|
"samples",
|
|
"Samples",
|
|
"number",
|
|
description="Quality samples for the render.",
|
|
section="Render",
|
|
default=64,
|
|
min=1,
|
|
max=4096,
|
|
step=1,
|
|
),
|
|
_field("width", "Width", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
|
_field("height", "Height", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
|
],
|
|
input_contract={"context": "cad_file", "requires": ["step_path"]},
|
|
output_contract={"context": "cad_file", "provides": ["rendered_image"]},
|
|
artifact_roles_consumed=["step_path"],
|
|
artifact_roles_produced=["rendered_image"],
|
|
),
|
|
_definition(
|
|
StepName.THREEJS_RENDER,
|
|
"Render Thumbnail (Three.js)",
|
|
"cad_file",
|
|
"render.thumbnail.threejs",
|
|
"rendering",
|
|
"Render a thumbnail image with the headless Three.js renderer.",
|
|
node_type="renderNode",
|
|
icon="camera",
|
|
defaults={"width": 512, "height": 512, "transparent_bg": True},
|
|
fields=[
|
|
_field("width", "Width", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
|
_field("height", "Height", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
|
_field(
|
|
"transparent_bg",
|
|
"Transparent Background",
|
|
"boolean",
|
|
description="Render with alpha channel instead of an opaque background.",
|
|
section="Output",
|
|
default=True,
|
|
),
|
|
],
|
|
execution_kind="bridge",
|
|
input_contract={"context": "cad_file", "requires": ["glb_preview", "bbox"]},
|
|
output_contract={"context": "cad_file", "provides": ["rendered_image"]},
|
|
artifact_roles_consumed=["glb_preview", "bbox"],
|
|
artifact_roles_produced=["rendered_image"],
|
|
),
|
|
_definition(
|
|
StepName.THUMBNAIL_SAVE,
|
|
"Save Thumbnail",
|
|
"cad_file",
|
|
"media.save_thumbnail",
|
|
"output",
|
|
"Persist the generated thumbnail back onto the CAD file record. Rendering settings are supplied by the connected upstream thumbnail request node.",
|
|
node_type="outputNode",
|
|
icon="download",
|
|
execution_kind="bridge",
|
|
input_contract={"context": "cad_file", "requires": ["rendered_image"]},
|
|
output_contract={"context": "cad_file", "provides": ["cad_thumbnail_media"]},
|
|
artifact_roles_consumed=["rendered_image"],
|
|
artifact_roles_produced=["cad_thumbnail_media"],
|
|
),
|
|
_definition(
|
|
StepName.ORDER_LINE_SETUP,
|
|
"Order Line Setup",
|
|
"order_line",
|
|
"order_line.prepare_render_context",
|
|
"processing",
|
|
"Validate order-line inputs and prepare the render job context.",
|
|
node_type="processNode",
|
|
icon="layers",
|
|
execution_kind="bridge",
|
|
input_contract={"context": "order_line", "requires": ["order_line_record"]},
|
|
output_contract={
|
|
"context": "order_line",
|
|
"provides": [
|
|
"order_line_context",
|
|
"cad_file_ref",
|
|
"step_path",
|
|
"cad_materials",
|
|
"glb_preview",
|
|
"bbox",
|
|
"usd_render_path",
|
|
"glb_reuse_path",
|
|
],
|
|
},
|
|
artifact_roles_produced=[
|
|
"order_line_context",
|
|
"cad_file_ref",
|
|
"step_path",
|
|
"cad_materials",
|
|
"glb_preview",
|
|
"bbox",
|
|
"usd_render_path",
|
|
"glb_reuse_path",
|
|
],
|
|
),
|
|
_definition(
|
|
StepName.RESOLVE_TEMPLATE,
|
|
"Resolve Template",
|
|
"order_line",
|
|
"rendering.resolve_template",
|
|
"processing",
|
|
"Resolve the render template for the order line and output type.",
|
|
node_type="processNode",
|
|
icon="layers",
|
|
execution_kind="bridge",
|
|
defaults={
|
|
"template_id_override": "",
|
|
"material_library_path": "",
|
|
"require_template": False,
|
|
"disable_materials": False,
|
|
"target_collection": "",
|
|
"material_replace_mode": "inherit",
|
|
"lighting_only_mode": "inherit",
|
|
"shadow_catcher_mode": "inherit",
|
|
"camera_orbit_mode": "inherit",
|
|
},
|
|
fields=[
|
|
_field(
|
|
"template_id_override",
|
|
"Template ID Override",
|
|
"text",
|
|
description="Optional render-template UUID to force for this workflow node instead of category/output-type resolution.",
|
|
section="Template",
|
|
default="",
|
|
text_format="uuid",
|
|
),
|
|
_field(
|
|
"require_template",
|
|
"Require Template",
|
|
"boolean",
|
|
description="Fail this node when no active render template can be resolved.",
|
|
section="Template",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"material_library_path",
|
|
"Material Library Path",
|
|
"text",
|
|
description="Optional absolute .blend path used instead of the active asset library.",
|
|
section="Materials",
|
|
default="",
|
|
text_format="absolute_blend_path",
|
|
),
|
|
_field(
|
|
"disable_materials",
|
|
"Disable Materials",
|
|
"boolean",
|
|
description="Resolve the template but skip material-map generation for downstream nodes.",
|
|
section="Materials",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"target_collection",
|
|
"Target Collection Override",
|
|
"text",
|
|
description="Optional collection name override applied after template resolution. Leave blank to inherit from the template.",
|
|
section="Template Overrides",
|
|
default="",
|
|
),
|
|
_field(
|
|
"material_replace_mode",
|
|
"Material Replace",
|
|
"select",
|
|
description="Override whether template material replacement is active for downstream nodes.",
|
|
section="Template Overrides",
|
|
default="inherit",
|
|
options=[
|
|
("inherit", "Inherit Template"),
|
|
("enabled", "Force Enabled"),
|
|
("disabled", "Force Disabled"),
|
|
],
|
|
),
|
|
_field(
|
|
"lighting_only_mode",
|
|
"Lighting Only",
|
|
"select",
|
|
description="Override the template lighting-only flag for downstream nodes.",
|
|
section="Template Overrides",
|
|
default="inherit",
|
|
options=[
|
|
("inherit", "Inherit Template"),
|
|
("enabled", "Force Enabled"),
|
|
("disabled", "Force Disabled"),
|
|
],
|
|
),
|
|
_field(
|
|
"shadow_catcher_mode",
|
|
"Shadow Catcher",
|
|
"select",
|
|
description="Override the template shadow-catcher flag for downstream nodes.",
|
|
section="Template Overrides",
|
|
default="inherit",
|
|
options=[
|
|
("inherit", "Inherit Template"),
|
|
("enabled", "Force Enabled"),
|
|
("disabled", "Force Disabled"),
|
|
],
|
|
),
|
|
_field(
|
|
"camera_orbit_mode",
|
|
"Camera Orbit",
|
|
"select",
|
|
description="Override whether turntable renders orbit the camera or rotate the object.",
|
|
section="Template Overrides",
|
|
default="inherit",
|
|
options=[
|
|
("inherit", "Inherit Template"),
|
|
("enabled", "Force Camera Orbit"),
|
|
("disabled", "Force Object Rotation"),
|
|
],
|
|
),
|
|
],
|
|
input_contract={"context": "order_line", "requires": ["order_line_context"]},
|
|
output_contract={
|
|
"context": "order_line",
|
|
"provides": [
|
|
"render_template",
|
|
"output_profile",
|
|
"template_path",
|
|
"material_library",
|
|
"material_map",
|
|
"use_materials",
|
|
"override_material",
|
|
"category_key",
|
|
"workflow_input_schema",
|
|
"template_inputs",
|
|
],
|
|
},
|
|
artifact_roles_consumed=["order_line_context"],
|
|
artifact_roles_produced=[
|
|
"render_template",
|
|
"output_profile",
|
|
"template_path",
|
|
"material_library",
|
|
"material_map",
|
|
"use_materials",
|
|
"override_material",
|
|
"category_key",
|
|
"workflow_input_schema",
|
|
"template_inputs",
|
|
],
|
|
),
|
|
_definition(
|
|
StepName.BLENDER_STILL,
|
|
"Render Still",
|
|
"order_line",
|
|
"render.production.still",
|
|
"rendering",
|
|
"Render a production still image with Blender.",
|
|
node_type="renderNode",
|
|
icon="camera",
|
|
defaults={"use_custom_render_settings": False, "rotation_z": 0},
|
|
fields=[
|
|
_field(
|
|
"use_custom_render_settings",
|
|
"Custom Render Settings",
|
|
"boolean",
|
|
description="Enable explicit engine, sample, and resolution overrides for Graph/Shadow mode. When disabled, authoritative output-type and template settings are inherited.",
|
|
section="Render",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"render_engine",
|
|
"Render Engine",
|
|
"select",
|
|
description="Renderer backend for the still render.",
|
|
section="Render",
|
|
default="cycles",
|
|
options=_BLENDER_ENGINE_OPTIONS,
|
|
),
|
|
_field(
|
|
"cycles_device",
|
|
"Cycles Device",
|
|
"select",
|
|
description="Force CPU, GPU, or automatic device selection.",
|
|
section="Render",
|
|
default="gpu",
|
|
options=_CYCLES_DEVICE_OPTIONS,
|
|
),
|
|
_field(
|
|
"samples",
|
|
"Samples",
|
|
"number",
|
|
description="Quality samples for the still render.",
|
|
section="Render",
|
|
default=256,
|
|
min=1,
|
|
max=4096,
|
|
step=1,
|
|
),
|
|
_field("width", "Width", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
|
_field("height", "Height", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
|
_field(
|
|
"transparent_bg",
|
|
"Transparent Background",
|
|
"boolean",
|
|
description="Render with alpha output instead of an opaque background.",
|
|
section="Output",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"noise_threshold",
|
|
"Noise Threshold",
|
|
"text",
|
|
description="Optional Cycles adaptive sampling threshold, for example 0.01.",
|
|
section="Denoising",
|
|
default="",
|
|
text_format="float_string",
|
|
),
|
|
_field(
|
|
"denoiser",
|
|
"Denoiser",
|
|
"text",
|
|
description="Optional Blender denoiser name, for example OPENIMAGEDENOISE or OPTIX.",
|
|
section="Denoising",
|
|
default="",
|
|
),
|
|
_field(
|
|
"denoising_input_passes",
|
|
"Input Passes",
|
|
"text",
|
|
description="Optional Cycles denoising input pass mode.",
|
|
section="Denoising",
|
|
default="",
|
|
),
|
|
_field(
|
|
"denoising_prefilter",
|
|
"Prefilter",
|
|
"text",
|
|
description="Optional Cycles denoising prefilter mode.",
|
|
section="Denoising",
|
|
default="",
|
|
),
|
|
_field(
|
|
"denoising_quality",
|
|
"Denoising Quality",
|
|
"text",
|
|
description="Optional Cycles denoising quality mode.",
|
|
section="Denoising",
|
|
default="",
|
|
),
|
|
_field(
|
|
"denoising_use_gpu",
|
|
"Use GPU Denoising",
|
|
"select",
|
|
description="Override whether Blender denoising should run on the GPU.",
|
|
section="Denoising",
|
|
default="",
|
|
options=_GPU_TOGGLE_OPTIONS,
|
|
),
|
|
_field(
|
|
"target_collection",
|
|
"Target Collection",
|
|
"text",
|
|
description="Template collection name that receives the imported product geometry.",
|
|
section="Scene",
|
|
default="Product",
|
|
),
|
|
_field(
|
|
"lighting_only",
|
|
"Lighting Only",
|
|
"boolean",
|
|
description="Use template lighting and auto-framing without template materials.",
|
|
section="Scene",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"shadow_catcher",
|
|
"Shadow Catcher",
|
|
"boolean",
|
|
description="Enable a shadow catcher plane for composited renders.",
|
|
section="Scene",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"rotation_x",
|
|
"Rotation X",
|
|
"number",
|
|
description="Additional X-axis rotation in degrees.",
|
|
section="Camera",
|
|
default=0,
|
|
min=-360,
|
|
max=360,
|
|
step=1,
|
|
unit="deg",
|
|
),
|
|
_field(
|
|
"rotation_y",
|
|
"Rotation Y",
|
|
"number",
|
|
description="Additional Y-axis rotation in degrees.",
|
|
section="Camera",
|
|
default=0,
|
|
min=-360,
|
|
max=360,
|
|
step=1,
|
|
unit="deg",
|
|
),
|
|
_field(
|
|
"rotation_z",
|
|
"Rotation Z",
|
|
"number",
|
|
description="Additional Z-axis rotation in degrees.",
|
|
section="Camera",
|
|
default=0,
|
|
min=-360,
|
|
max=360,
|
|
step=1,
|
|
unit="deg",
|
|
),
|
|
_field(
|
|
"focal_length_mm",
|
|
"Focal Length",
|
|
"number",
|
|
description="Optional camera focal length override.",
|
|
section="Camera",
|
|
default=None,
|
|
min=1,
|
|
max=500,
|
|
step=0.1,
|
|
unit="mm",
|
|
),
|
|
_field(
|
|
"sensor_width_mm",
|
|
"Sensor Width",
|
|
"number",
|
|
description="Optional camera sensor width override.",
|
|
section="Camera",
|
|
default=None,
|
|
min=1,
|
|
max=100,
|
|
step=0.1,
|
|
unit="mm",
|
|
),
|
|
_field(
|
|
"material_override",
|
|
"Material Override",
|
|
"text",
|
|
description="Optional material name forced onto all parts during rendering.",
|
|
section="Materials",
|
|
default="",
|
|
),
|
|
],
|
|
input_contract={
|
|
"context": "order_line",
|
|
"requires": ["order_line_context", "render_template", "material_assignments", "bbox"],
|
|
},
|
|
output_contract={"context": "order_line", "provides": ["rendered_image"]},
|
|
artifact_roles_consumed=["order_line_context", "render_template", "material_assignments", "bbox"],
|
|
artifact_roles_produced=["rendered_image"],
|
|
),
|
|
_definition(
|
|
StepName.BLENDER_TURNTABLE,
|
|
"Render Turntable",
|
|
"order_line",
|
|
"render.production.turntable",
|
|
"rendering",
|
|
"Render an animated turntable sequence with Blender.",
|
|
node_type="renderFramesNode",
|
|
icon="film",
|
|
defaults={
|
|
"use_custom_render_settings": False,
|
|
"fps": 24,
|
|
"frame_count": 120,
|
|
"duration_s": 5,
|
|
"turntable_degrees": 360,
|
|
"turntable_axis": "world_z",
|
|
"camera_orbit": True,
|
|
"rotation_z": 0,
|
|
},
|
|
fields=[
|
|
_field(
|
|
"use_custom_render_settings",
|
|
"Custom Render Settings",
|
|
"boolean",
|
|
description="Enable explicit engine, sample, and resolution overrides for Graph/Shadow mode. When disabled, authoritative output-type and template settings are inherited.",
|
|
section="Render",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"render_engine",
|
|
"Render Engine",
|
|
"select",
|
|
description="Renderer backend for the turntable job.",
|
|
section="Render",
|
|
default="cycles",
|
|
options=_BLENDER_ENGINE_OPTIONS,
|
|
),
|
|
_field(
|
|
"cycles_device",
|
|
"Cycles Device",
|
|
"select",
|
|
description="Force CPU, GPU, or automatic device selection.",
|
|
section="Render",
|
|
default="auto",
|
|
options=_CYCLES_DEVICE_OPTIONS,
|
|
),
|
|
_field(
|
|
"samples",
|
|
"Samples",
|
|
"number",
|
|
description="Quality samples for each frame.",
|
|
section="Render",
|
|
default=64,
|
|
min=1,
|
|
max=4096,
|
|
step=1,
|
|
),
|
|
_field("width", "Width", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
|
_field("height", "Height", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
|
_field(
|
|
"transparent_bg",
|
|
"Transparent Background",
|
|
"boolean",
|
|
description="Render transparent PNG frames for alpha output or FFmpeg background compositing.",
|
|
section="Output",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"bg_color",
|
|
"Background Color",
|
|
"text",
|
|
description="Optional hex color used during FFmpeg compositing, for example #FFFFFF.",
|
|
section="Output",
|
|
default="",
|
|
text_format="hex_color",
|
|
),
|
|
_field("fps", "FPS", "number", section="Animation", default=24, min=1, max=120, step=1),
|
|
_field(
|
|
"frame_count",
|
|
"Frame Count",
|
|
"number",
|
|
description="Explicit total frame count for the rendered turntable clip.",
|
|
section="Animation",
|
|
default=120,
|
|
min=1,
|
|
max=7200,
|
|
step=1,
|
|
),
|
|
_field(
|
|
"duration_s",
|
|
"Duration",
|
|
"number",
|
|
description="Length of the animation clip.",
|
|
section="Animation",
|
|
default=5,
|
|
min=1,
|
|
max=120,
|
|
step=1,
|
|
unit="s",
|
|
),
|
|
_field(
|
|
"turntable_degrees",
|
|
"Turntable Degrees",
|
|
"number",
|
|
description="How far the model or camera rotates during the animation.",
|
|
section="Animation",
|
|
default=360,
|
|
min=1,
|
|
max=3600,
|
|
step=1,
|
|
unit="deg",
|
|
),
|
|
_field(
|
|
"turntable_axis",
|
|
"Turntable Axis",
|
|
"select",
|
|
description="Axis used for the model or camera orbit.",
|
|
section="Animation",
|
|
default="world_z",
|
|
options=_TURNTABLE_AXIS_OPTIONS,
|
|
),
|
|
_field(
|
|
"camera_orbit",
|
|
"Camera Orbit",
|
|
"boolean",
|
|
description="Orbit the camera instead of rotating the model.",
|
|
section="Animation",
|
|
default=True,
|
|
),
|
|
_field(
|
|
"target_collection",
|
|
"Target Collection",
|
|
"text",
|
|
description="Template collection name that receives the imported product geometry.",
|
|
section="Scene",
|
|
default="Product",
|
|
),
|
|
_field(
|
|
"lighting_only",
|
|
"Lighting Only",
|
|
"boolean",
|
|
description="Use template lighting and auto-framing without template materials.",
|
|
section="Scene",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"shadow_catcher",
|
|
"Shadow Catcher",
|
|
"boolean",
|
|
description="Enable a shadow catcher plane for composited renders.",
|
|
section="Scene",
|
|
default=False,
|
|
),
|
|
_field(
|
|
"rotation_x",
|
|
"Rotation X",
|
|
"number",
|
|
description="Additional X-axis rotation in degrees.",
|
|
section="Camera",
|
|
default=0,
|
|
min=-360,
|
|
max=360,
|
|
step=1,
|
|
unit="deg",
|
|
),
|
|
_field(
|
|
"rotation_y",
|
|
"Rotation Y",
|
|
"number",
|
|
description="Additional Y-axis rotation in degrees.",
|
|
section="Camera",
|
|
default=0,
|
|
min=-360,
|
|
max=360,
|
|
step=1,
|
|
unit="deg",
|
|
),
|
|
_field(
|
|
"rotation_z",
|
|
"Rotation Z",
|
|
"number",
|
|
description="Base Z-axis rotation before the turntable motion is applied.",
|
|
section="Camera",
|
|
default=0,
|
|
min=-360,
|
|
max=360,
|
|
step=1,
|
|
unit="deg",
|
|
),
|
|
_field(
|
|
"focal_length_mm",
|
|
"Focal Length",
|
|
"number",
|
|
description="Optional camera focal length override.",
|
|
section="Camera",
|
|
default=None,
|
|
min=1,
|
|
max=500,
|
|
step=0.1,
|
|
unit="mm",
|
|
),
|
|
_field(
|
|
"sensor_width_mm",
|
|
"Sensor Width",
|
|
"number",
|
|
description="Optional camera sensor width override.",
|
|
section="Camera",
|
|
default=None,
|
|
min=1,
|
|
max=100,
|
|
step=0.1,
|
|
unit="mm",
|
|
),
|
|
_field(
|
|
"material_override",
|
|
"Material Override",
|
|
"text",
|
|
description="Optional material name forced onto all parts during rendering.",
|
|
section="Materials",
|
|
default="",
|
|
),
|
|
],
|
|
input_contract={
|
|
"context": "order_line",
|
|
"requires": ["order_line_context", "render_template", "material_assignments", "bbox"],
|
|
},
|
|
output_contract={"context": "order_line", "provides": ["rendered_frames", "rendered_video"]},
|
|
artifact_roles_consumed=["order_line_context", "render_template", "material_assignments", "bbox"],
|
|
artifact_roles_produced=["rendered_frames", "rendered_video"],
|
|
),
|
|
_definition(
|
|
StepName.OUTPUT_SAVE,
|
|
"Save Output",
|
|
"order_line",
|
|
"media.save_output",
|
|
"output",
|
|
"Persist the rendered output file and create the media record.",
|
|
node_type="outputNode",
|
|
icon="download",
|
|
execution_kind="bridge",
|
|
defaults={"expected_artifact_role": "", "require_upstream_artifact": False},
|
|
fields=[
|
|
_field(
|
|
"expected_artifact_role",
|
|
"Expected Artifact Role",
|
|
"select",
|
|
description="Restrict this node to a specific upstream render artifact type.",
|
|
section="Output",
|
|
default="",
|
|
options=[
|
|
("", "Any Connected Artifact"),
|
|
("render_output", "Still Output"),
|
|
("turntable_output", "Turntable Output"),
|
|
("blend_export", "Blend Export"),
|
|
("thumbnail_output", "Thumbnail Output"),
|
|
],
|
|
),
|
|
_field(
|
|
"require_upstream_artifact",
|
|
"Require Upstream Artifact",
|
|
"boolean",
|
|
description="Fail the node when no matching upstream artifact is connected.",
|
|
section="Output",
|
|
default=False,
|
|
),
|
|
],
|
|
input_contract={
|
|
"context": "order_line",
|
|
"requires": ["order_line_context"],
|
|
"requires_any": ["rendered_image", "rendered_frames", "rendered_video"],
|
|
},
|
|
output_contract={"context": "order_line", "provides": ["media_asset", "workflow_result"]},
|
|
artifact_roles_consumed=["rendered_image", "rendered_frames", "rendered_video"],
|
|
artifact_roles_produced=["media_asset", "workflow_result"],
|
|
),
|
|
_definition(
|
|
StepName.EXPORT_BLEND,
|
|
"Export Blend",
|
|
"order_line",
|
|
"media.export_blend",
|
|
"output",
|
|
"Persist the generated .blend file as a downloadable media asset. Only the optional filename suffix is workflow-configurable today.",
|
|
node_type="outputNode",
|
|
icon="download",
|
|
defaults={"output_name_suffix": ""},
|
|
fields=[
|
|
_field(
|
|
"output_name_suffix",
|
|
"Output Name Suffix",
|
|
"text",
|
|
description="Optional suffix appended to the generated `.blend` filename.",
|
|
section="Output",
|
|
default="",
|
|
text_format="safe_filename_suffix",
|
|
max_length=64,
|
|
),
|
|
],
|
|
execution_kind="bridge",
|
|
input_contract={"context": "order_line", "requires": ["order_line_context", "render_template"]},
|
|
output_contract={"context": "order_line", "provides": ["blend_asset"]},
|
|
artifact_roles_consumed=["order_line_context", "render_template"],
|
|
artifact_roles_produced=["blend_asset"],
|
|
),
|
|
_definition(
|
|
StepName.STL_CACHE_GENERATE,
|
|
"Generate STL Cache",
|
|
"cad_file",
|
|
"cad.generate_stl_cache",
|
|
"processing",
|
|
"Compatibility node for legacy CAD flows. HartOMat graph execution uses direct OCC/GLB export instead, so this node intentionally performs no per-node-configurable cache generation.",
|
|
node_type="convertNode",
|
|
icon="refresh-cw",
|
|
execution_kind="bridge",
|
|
input_contract={"context": "cad_file", "requires": ["step_path"]},
|
|
output_contract={"context": "cad_file", "provides": ["stl_cache"]},
|
|
artifact_roles_consumed=["step_path"],
|
|
artifact_roles_produced=["stl_cache"],
|
|
),
|
|
_definition(
|
|
StepName.NOTIFY,
|
|
"Notify",
|
|
"order_line",
|
|
"notifications.emit",
|
|
"output",
|
|
"Emit a user-visible notification for workflow completion or failure.",
|
|
node_type="outputNode",
|
|
icon="bell",
|
|
defaults={"channel": "audit_log", "require_armed_render": False},
|
|
fields=[
|
|
_field(
|
|
"channel",
|
|
"Channel",
|
|
"select",
|
|
description="Notification target channel.",
|
|
section="Notification",
|
|
default="audit_log",
|
|
options=[("audit_log", "Audit Log")],
|
|
),
|
|
_field(
|
|
"require_armed_render",
|
|
"Require Armed Render",
|
|
"boolean",
|
|
description="Fail this node when no upstream graph render task is configured to hand off notifications.",
|
|
section="Notification",
|
|
default=False,
|
|
),
|
|
],
|
|
execution_kind="bridge",
|
|
input_contract={
|
|
"context": "order_line",
|
|
"requires": ["order_line_context"],
|
|
"requires_any": ["rendered_image", "rendered_frames", "rendered_video", "workflow_result"],
|
|
},
|
|
output_contract={"context": "order_line", "provides": ["notification_event"]},
|
|
artifact_roles_consumed=["workflow_result"],
|
|
artifact_roles_produced=["notification_event"],
|
|
),
|
|
]
|
|
|
|
|
|
_NODE_DEFINITION_BY_STEP = {definition.step: definition for definition in _NODE_DEFINITIONS}
|
|
|
|
|
|
def list_node_definitions() -> list[WorkflowNodeDefinition]:
|
|
return list(_NODE_DEFINITIONS)
|
|
|
|
|
|
def get_node_definition(step: StepName | str) -> WorkflowNodeDefinition | None:
|
|
step_value = step.value if isinstance(step, StepName) else step
|
|
return _NODE_DEFINITION_BY_STEP.get(step_value)
|
|
|
|
|
|
def get_node_type_for_step(step: StepName | str) -> str | None:
|
|
definition = get_node_definition(step)
|
|
return definition.node_type if definition else None
|