418 lines
13 KiB
Python
418 lines
13 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"]
|
|
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
|