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"] ExecutionKind = Literal["native", "bridge"] 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] = [] class WorkflowNodeDefinition(BaseModel): step: str label: 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 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, ) -> 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 []) ], ) def _definition( step: StepName, label: 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", ) -> WorkflowNodeDefinition: return WorkflowNodeDefinition( step=step.value, label=label, category=category, description=description, node_type=node_type, icon=icon, defaults=defaults or {}, fields=fields or [], execution_kind=execution_kind, ) _NODE_DEFINITIONS: list[WorkflowNodeDefinition] = [ _definition( StepName.RESOLVE_STEP_PATH, "Resolve STEP Path", "input", "Locate the STEP file on disk from the CAD file record.", node_type="inputNode", icon="file-up", execution_kind="bridge", ), _definition( StepName.OCC_OBJECT_EXTRACT, "Extract STEP Objects", "processing", "Extract part objects and metadata from the STEP file via OCC/cadquery.", node_type="processNode", icon="layers", execution_kind="bridge", ), _definition( StepName.OCC_GLB_EXPORT, "Export GLB", "processing", "Convert STEP geometry into GLB for previews and downstream rendering.", node_type="processNode", icon="refresh-cw", execution_kind="bridge", ), _definition( StepName.GLB_BBOX, "Compute Bounding Box", "processing", "Compute the model bounding box from the exported GLB for framing decisions.", node_type="processNode", icon="layers", execution_kind="bridge", ), _definition( StepName.MATERIAL_MAP_RESOLVE, "Resolve Material Map", "processing", "Map raw part material names to HartOMat material records via aliases.", node_type="processNode", icon="layers", execution_kind="bridge", ), _definition( StepName.AUTO_POPULATE_MATERIALS, "Auto Populate Materials", "processing", "Create missing material records for newly discovered part materials.", node_type="processNode", icon="layers", execution_kind="bridge", ), _definition( StepName.BLENDER_RENDER, "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=[("cycles", "Cycles"), ("eevee", "EEVEE")], ), _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"), ], ), _definition( StepName.THREEJS_RENDER, "Render Thumbnail (Three.js)", "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", ), _definition( StepName.THUMBNAIL_SAVE, "Save Thumbnail", "output", "Persist the generated thumbnail back onto the CAD file record.", node_type="outputNode", icon="download", execution_kind="bridge", ), _definition( StepName.ORDER_LINE_SETUP, "Order Line Setup", "processing", "Validate order-line inputs and prepare the render job context.", node_type="processNode", icon="layers", execution_kind="bridge", ), _definition( StepName.RESOLVE_TEMPLATE, "Resolve Template", "processing", "Resolve the render template for the order line and output type.", node_type="processNode", icon="layers", execution_kind="bridge", ), _definition( StepName.BLENDER_STILL, "Render Still", "rendering", "Render a production still image with Blender.", node_type="renderNode", icon="camera", defaults={"render_engine": "cycles", "samples": 256, "width": 2048, "height": 2048, "rotation_z": 0}, fields=[ _field( "render_engine", "Render Engine", "select", description="Renderer backend for the still render.", section="Render", default="cycles", options=[("cycles", "Cycles"), ("eevee", "EEVEE")], ), _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( "rotation_z", "Rotation Z", "number", description="Additional Z-axis rotation in degrees.", section="Camera", default=0, min=-360, max=360, step=1, unit="deg", ), ], ), _definition( StepName.BLENDER_TURNTABLE, "Render Turntable", "rendering", "Render an animated turntable sequence with Blender.", node_type="renderFramesNode", icon="film", defaults={ "render_engine": "cycles", "samples": 64, "width": 2048, "height": 2048, "fps": 24, "duration_s": 5, "rotation_z": 0, }, fields=[ _field( "render_engine", "Render Engine", "select", description="Renderer backend for the turntable job.", section="Render", default="cycles", options=[("cycles", "Cycles"), ("eevee", "EEVEE")], ), _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("fps", "FPS", "number", section="Animation", default=24, min=1, max=120, 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( "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", ), ], ), _definition( StepName.OUTPUT_SAVE, "Save Output", "output", "Persist the rendered output file and create the media record.", node_type="outputNode", icon="download", execution_kind="bridge", ), _definition( StepName.EXPORT_BLEND, "Export Blend", "output", "Persist the generated .blend file as a downloadable media asset.", node_type="outputNode", icon="download", execution_kind="bridge", ), _definition( StepName.STL_CACHE_GENERATE, "Generate STL Cache", "processing", "Generate and cache STL derivatives next to the STEP source.", node_type="convertNode", icon="refresh-cw", execution_kind="bridge", ), _definition( StepName.NOTIFY, "Notify", "output", "Emit a user-visible notification for workflow completion or failure.", node_type="outputNode", icon="bell", defaults={"channel": "audit_log"}, fields=[ _field( "channel", "Channel", "select", description="Notification target channel.", section="Notification", default="audit_log", options=[("audit_log", "Audit Log")], ), ], execution_kind="bridge", ), ] _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