chore: snapshot workflow migration progress

This commit is contained in:
2026-04-12 11:49:04 +02:00
parent 0cd02513d5
commit 3e810c74a3
163 changed files with 31773 additions and 2752 deletions
+455 -107
View File
@@ -1,8 +1,43 @@
import api from './client'
import type { OutputTypeArtifactKind, OutputTypeWorkflowRolloutMode } from './outputTypes'
export type WorkflowPresetType = 'still' | 'still_graph' | 'turntable' | 'multi_angle' | 'still_with_exports' | 'custom'
export type WorkflowExecutionMode = 'legacy' | 'graph' | 'shadow'
export type WorkflowStarterFamily = 'cad_file' | 'order_line'
export type WorkflowBlueprintType = 'cad_intake' | 'order_rendering' | 'still_graph_reference'
export type WorkflowCanonicalBlueprintType = WorkflowBlueprintType | 'starter_cad_intake' | 'starter_order_rendering'
export interface WorkflowRolloutLatestRun {
workflow_run_id: string
execution_mode: WorkflowExecutionMode
status: string
created_at: string
completed_at: string | null
}
export interface WorkflowRolloutLinkedOutputType {
id: string
name: string
is_active: boolean
artifact_kind: OutputTypeArtifactKind
workflow_rollout_mode: OutputTypeWorkflowRolloutMode
}
export interface WorkflowRolloutSummary {
linked_output_type_count: number
active_output_type_count: number
linked_output_type_names: string[]
linked_output_types: WorkflowRolloutLinkedOutputType[]
rollout_modes: ('legacy_only' | 'shadow' | 'graph' | string)[]
has_blocking_contracts: boolean
blocking_reasons: string[]
latest_run: WorkflowRolloutLatestRun | null
latest_shadow_run: WorkflowRolloutLatestRun | null
latest_rollout_gate_verdict: 'pass' | 'warn' | 'fail' | null
latest_rollout_ready: boolean | null
latest_rollout_status: 'ready_for_rollout' | 'hold_legacy_authoritative' | string | null
latest_rollout_reasons: string[]
}
export interface WorkflowDefinition {
id: string
@@ -10,6 +45,8 @@ export interface WorkflowDefinition {
output_type_id: string | null
config: WorkflowConfig
family: WorkflowNodeFamily | 'mixed' | null
supported_artifact_kinds?: OutputTypeArtifactKind[]
rollout_summary: WorkflowRolloutSummary
is_active: boolean
created_at: string
}
@@ -132,6 +169,18 @@ export interface WorkflowPreflightResponse {
nodes: WorkflowPreflightNode[]
}
export interface WorkflowOrderLineContextOption {
value: string
label: string
meta: string
}
export interface WorkflowOrderLineContextGroup {
order_id: string
order_label: string
options: WorkflowOrderLineContextOption[]
}
export interface WorkflowDraftPreflightRequest {
workflow_id?: string | null
context_id: string
@@ -162,6 +211,11 @@ export interface WorkflowRunComparison {
execution_mode: WorkflowExecutionMode
status: string
summary: string
rollout_gate_verdict: 'pass' | 'warn' | 'fail'
workflow_rollout_ready: boolean
workflow_rollout_status: 'ready_for_rollout' | 'hold_legacy_authoritative'
rollout_reasons: string[]
rollout_thresholds: Record<string, number>
authoritative_output: WorkflowComparisonArtifact
observer_output: WorkflowComparisonArtifact
exact_match: boolean | null
@@ -209,6 +263,9 @@ export const preflightWorkflowDraft = (
): Promise<WorkflowPreflightResponse> =>
api.post('/workflows/preflight', data).then(r => r.data)
export const getWorkflowOrderLineContexts = (limit = 50): Promise<WorkflowOrderLineContextGroup[]> =>
api.get('/workflows/contexts/order-lines', { params: { limit } }).then(r => r.data)
export const getWorkflowRunComparison = (runId: string): Promise<WorkflowRunComparison> =>
api.get(`/workflows/runs/${runId}/comparison`).then(r => r.data)
@@ -235,9 +292,12 @@ export interface WorkflowNodeFieldDefinition {
step: number | null
unit: string | null
options: WorkflowNodeFieldOption[]
allow_blank?: boolean
max_length?: number | null
text_format?: string
}
export type WorkflowNodeFamily = 'cad_file' | 'order_line'
export type WorkflowNodeFamily = 'cad_file' | 'order_line' | 'shared'
export interface WorkflowNodeDefinition {
step: string
@@ -280,32 +340,75 @@ export const getNodeDefinitions = (): Promise<WorkflowNodeDefinitionsResponse> =
export const getPipelineSteps = (): Promise<PipelineStepsResponse> =>
api.get('/workflows/pipeline-steps').then(r => r.data)
function buildStillGraphNodes(renderParams: WorkflowParams): { nodes: WorkflowNode[]; edges: WorkflowEdge[] } {
function normalizeRenderParams(params: WorkflowParams = {}): WorkflowParams {
const normalized = { ...params }
const resolution = Array.isArray(normalized.resolution) ? normalized.resolution : undefined
if (resolution && resolution.length === 2) {
normalized.width = Number(resolution[0])
normalized.height = Number(resolution[1])
delete normalized.resolution
}
return normalized
}
function buildWorkflowNode(
id: string,
step: string,
x: number,
y: number,
options: {
label: string
type?: string
params?: WorkflowParams
},
): WorkflowNode {
return {
id,
step,
params: { ...(options.params ?? {}) },
ui: {
type: options.type,
label: options.label,
position: { x, y },
},
}
}
function extractRenderParamsFromNodes(nodes: WorkflowNode[], step: string): WorkflowParams {
const match = nodes.find(node => node.step === step)
return normalizeRenderParams(match?.params ?? {})
}
function buildOrderLineStillGraphNodes(renderParams: WorkflowParams): { nodes: WorkflowNode[]; edges: WorkflowEdge[] } {
return {
nodes: [
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 160 } } },
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 160 } } },
{
id: 'populate_materials',
step: 'auto_populate_materials',
params: {},
ui: { type: 'processNode', label: 'Auto Populate Materials', position: { x: 220, y: 320 } },
},
{ id: 'bbox', step: 'glb_bbox', params: {}, ui: { type: 'processNode', label: 'Compute Bounding Box', position: { x: 220, y: 40 } } },
{
id: 'resolve_materials',
step: 'material_map_resolve',
params: {},
ui: { type: 'processNode', label: 'Resolve Material Map', position: { x: 440, y: 200 } },
},
{
id: 'render',
step: 'blender_still',
params: { use_custom_render_settings: true, ...renderParams },
ui: { type: 'renderNode', label: 'Still Render', position: { x: 680, y: 160 } },
},
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 920, y: 120 } } },
{ id: 'notify', step: 'notify', params: {}, ui: { type: 'outputNode', label: 'Notify Result', position: { x: 920, y: 220 } } },
buildWorkflowNode('setup', 'order_line_setup', 0, 160, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 160, { label: 'Resolve Template' }),
buildWorkflowNode('populate_materials', 'auto_populate_materials', 220, 320, {
label: 'Auto Populate Materials',
type: 'processNode',
}),
buildWorkflowNode('bbox', 'glb_bbox', 220, 40, {
label: 'Compute Bounding Box',
type: 'processNode',
}),
buildWorkflowNode('resolve_materials', 'material_map_resolve', 440, 200, {
label: 'Resolve Material Map',
type: 'processNode',
}),
buildWorkflowNode('render', 'blender_still', 680, 160, {
label: 'Still Render',
type: 'renderNode',
params: { use_custom_render_settings: false, ...renderParams },
}),
buildWorkflowNode('output', 'output_save', 920, 120, {
label: 'Save Output',
type: 'outputNode',
}),
buildWorkflowNode('notify', 'notify', 920, 220, {
label: 'Notify Result',
type: 'outputNode',
}),
],
edges: [
{ from: 'setup', to: 'template' },
@@ -322,24 +425,25 @@ function buildStillGraphNodes(renderParams: WorkflowParams): { nodes: WorkflowNo
}
}
function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams = {}): WorkflowConfig {
const renderParams = { ...params }
const resolution = Array.isArray(renderParams.resolution) ? renderParams.resolution : undefined
if (resolution && resolution.length === 2) {
renderParams.width = Number(resolution[0])
renderParams.height = Number(resolution[1])
delete renderParams.resolution
}
function buildPresetWorkflowConfigInternal(type: WorkflowPresetType, params: WorkflowParams = {}): WorkflowConfig {
const renderParams = normalizeRenderParams(params)
if (type === 'still') {
return {
version: 1,
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
nodes: [
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 100 } } },
{ id: 'render', step: 'blender_still', params: renderParams, ui: { type: 'renderNode', label: 'Still Render', position: { x: 440, y: 100 } } },
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 660, y: 100 } } },
buildWorkflowNode('setup', 'order_line_setup', 0, 100, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 100, { label: 'Resolve Template' }),
buildWorkflowNode('render', 'blender_still', 440, 100, {
label: 'Still Render',
type: 'renderNode',
params: renderParams,
}),
buildWorkflowNode('output', 'output_save', 660, 100, {
label: 'Save Output',
type: 'outputNode',
}),
],
edges: [
{ from: 'setup', to: 'template' },
@@ -350,7 +454,7 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
}
if (type === 'still_graph') {
const { nodes, edges } = buildStillGraphNodes(renderParams)
const { nodes, edges } = buildOrderLineStillGraphNodes(renderParams)
return {
version: 1,
ui: { preset: type, execution_mode: 'graph', family: 'order_line' },
@@ -364,10 +468,17 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
version: 1,
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
nodes: [
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 100 } } },
{ id: 'turntable', step: 'blender_turntable', params: renderParams, ui: { type: 'renderFramesNode', label: 'Turntable Render', position: { x: 440, y: 100 } } },
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 660, y: 100 } } },
buildWorkflowNode('setup', 'order_line_setup', 0, 100, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 100, { label: 'Resolve Template' }),
buildWorkflowNode('turntable', 'blender_turntable', 440, 100, {
label: 'Turntable Render',
type: 'renderFramesNode',
params: renderParams,
}),
buildWorkflowNode('output', 'output_save', 660, 100, {
label: 'Save Output',
type: 'outputNode',
}),
],
edges: [
{ from: 'setup', to: 'template' },
@@ -385,15 +496,19 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
version: 1,
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
nodes: [
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 195 } } },
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 195 } } },
...angles.map((angle, index) => ({
id: `render_${index}`,
step: 'blender_still',
params: { ...sharedParams, rotation_z: angle },
ui: { type: 'renderNode', label: `Render ${angle}°`, position: { x: 440, y: index * 130 } },
})),
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 700, y: 195 } } },
buildWorkflowNode('setup', 'order_line_setup', 0, 195, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 195, { label: 'Resolve Template' }),
...angles.map((angle, index) =>
buildWorkflowNode(`render_${index}`, 'blender_still', 440, index * 130, {
label: `Render ${angle}°`,
type: 'renderNode',
params: { ...sharedParams, rotation_z: angle },
}),
),
buildWorkflowNode('output', 'output_save', 700, 195, {
label: 'Save Output',
type: 'outputNode',
}),
],
edges: [
{ from: 'setup', to: 'template' },
@@ -408,11 +523,21 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
version: 1,
ui: { preset: type, execution_mode: 'legacy', family: 'order_line' },
nodes: [
{ id: 'setup', step: 'order_line_setup', params: {}, ui: { label: 'Order Line Setup', position: { x: 0, y: 100 } } },
{ id: 'template', step: 'resolve_template', params: {}, ui: { label: 'Resolve Template', position: { x: 220, y: 100 } } },
{ id: 'render', step: 'blender_still', params: renderParams, ui: { type: 'renderNode', label: 'Still Render', position: { x: 440, y: 100 } } },
{ id: 'output', step: 'output_save', params: {}, ui: { type: 'outputNode', label: 'Save Output', position: { x: 660, y: 70 } } },
{ id: 'blend', step: 'export_blend', params: {}, ui: { type: 'outputNode', label: 'Export Blend', position: { x: 660, y: 160 } } },
buildWorkflowNode('setup', 'order_line_setup', 0, 100, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 100, { label: 'Resolve Template' }),
buildWorkflowNode('render', 'blender_still', 440, 100, {
label: 'Still Render',
type: 'renderNode',
params: renderParams,
}),
buildWorkflowNode('output', 'output_save', 660, 70, {
label: 'Save Output',
type: 'outputNode',
}),
buildWorkflowNode('blend', 'export_blend', 660, 160, {
label: 'Export Blend',
type: 'outputNode',
}),
],
edges: [
{ from: 'setup', to: 'template' },
@@ -427,22 +552,245 @@ function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams =
version: 1,
ui: { preset: 'custom', execution_mode: 'legacy', family: 'order_line' },
nodes: [
{
id: 'setup',
step: 'order_line_setup',
params: {},
ui: { label: 'Order Line Setup', position: { x: 120, y: 140 } },
},
buildWorkflowNode('setup', 'order_line_setup', 120, 140, {
label: 'Order Line Setup',
type: 'processNode',
}),
],
edges: [],
}
}
export function buildWorkflowBlueprintConfig(blueprint: WorkflowBlueprintType): WorkflowConfig {
if (blueprint === 'cad_intake') {
return {
version: 1,
ui: { preset: 'custom', execution_mode: 'legacy', family: 'cad_file', blueprint },
nodes: [
buildWorkflowNode('resolve_step', 'resolve_step_path', 0, 180, { label: 'Resolve STEP Path' }),
buildWorkflowNode('extract_objects', 'occ_object_extract', 220, 180, {
label: 'Extract STEP Objects',
}),
buildWorkflowNode('export_glb', 'occ_glb_export', 440, 180, { label: 'Export GLB' }),
buildWorkflowNode('bbox', 'glb_bbox', 660, 120, {
label: 'Compute Bounding Box',
type: 'processNode',
}),
buildWorkflowNode('stl_cache', 'stl_cache_generate', 660, 300, { label: 'Generate STL Cache' }),
buildWorkflowNode('blender_thumb', 'blender_render', 880, 120, {
label: 'Render Thumbnail (Blender)',
type: 'renderNode',
params: { render_engine: 'cycles', samples: 64, width: 512, height: 512 },
}),
buildWorkflowNode('threejs_thumb', 'threejs_render', 880, 320, {
label: 'Render Thumbnail (Three.js)',
type: 'renderNode',
params: { width: 512, height: 512, transparent_bg: true },
}),
buildWorkflowNode('save_blender_thumb', 'thumbnail_save', 1100, 120, {
label: 'Save Blender Thumbnail',
type: 'outputNode',
}),
buildWorkflowNode('save_threejs_thumb', 'thumbnail_save', 1100, 320, {
label: 'Save Three.js Thumbnail',
type: 'outputNode',
}),
],
edges: [
{ from: 'resolve_step', to: 'extract_objects' },
{ from: 'extract_objects', to: 'export_glb' },
{ from: 'export_glb', to: 'bbox' },
{ from: 'export_glb', to: 'stl_cache' },
{ from: 'export_glb', to: 'blender_thumb' },
{ from: 'export_glb', to: 'threejs_thumb' },
{ from: 'bbox', to: 'threejs_thumb' },
{ from: 'blender_thumb', to: 'save_blender_thumb' },
{ from: 'threejs_thumb', to: 'save_threejs_thumb' },
],
}
}
if (blueprint === 'order_rendering') {
return {
version: 1,
ui: { preset: 'custom', execution_mode: 'legacy', family: 'order_line', blueprint },
nodes: [
buildWorkflowNode('setup', 'order_line_setup', 0, 220, { label: 'Order Line Setup' }),
buildWorkflowNode('template', 'resolve_template', 220, 220, { label: 'Resolve Template' }),
buildWorkflowNode('populate_materials', 'auto_populate_materials', 220, 360, {
label: 'Auto Populate Materials',
}),
buildWorkflowNode('bbox', 'glb_bbox', 220, 80, { label: 'Compute Bounding Box' }),
buildWorkflowNode('resolve_materials', 'material_map_resolve', 440, 220, {
label: 'Resolve Material Map',
}),
buildWorkflowNode('still_render', 'blender_still', 680, 80, {
label: 'Render Still',
type: 'renderNode',
params: { rotation_z: 0 },
}),
buildWorkflowNode('turntable_render', 'blender_turntable', 680, 220, {
label: 'Render Turntable',
type: 'renderFramesNode',
params: { fps: 24, duration_s: 5 },
}),
buildWorkflowNode('blend_export', 'export_blend', 680, 360, {
label: 'Export Blend',
type: 'outputNode',
}),
buildWorkflowNode('save_still', 'output_save', 920, 80, {
label: 'Save Still Output',
type: 'outputNode',
}),
buildWorkflowNode('save_turntable', 'output_save', 920, 220, {
label: 'Save Turntable Output',
type: 'outputNode',
}),
buildWorkflowNode('notify_still', 'notify', 920, 140, {
label: 'Notify Still Result',
type: 'outputNode',
}),
buildWorkflowNode('notify_turntable', 'notify', 920, 280, {
label: 'Notify Turntable Result',
type: 'outputNode',
}),
buildWorkflowNode('notify_export', 'notify', 920, 360, {
label: 'Notify Blend Export',
type: 'outputNode',
}),
],
edges: [
{ from: 'setup', to: 'template' },
{ from: 'setup', to: 'populate_materials' },
{ from: 'setup', to: 'bbox' },
{ from: 'template', to: 'resolve_materials' },
{ from: 'populate_materials', to: 'resolve_materials' },
{ from: 'resolve_materials', to: 'still_render' },
{ from: 'resolve_materials', to: 'turntable_render' },
{ from: 'bbox', to: 'still_render' },
{ from: 'bbox', to: 'turntable_render' },
{ from: 'template', to: 'still_render' },
{ from: 'template', to: 'turntable_render' },
{ from: 'template', to: 'blend_export' },
{ from: 'still_render', to: 'save_still' },
{ from: 'still_render', to: 'notify_still' },
{ from: 'turntable_render', to: 'save_turntable' },
{ from: 'turntable_render', to: 'notify_turntable' },
{ from: 'blend_export', to: 'notify_export' },
],
}
}
const { nodes, edges } = buildOrderLineStillGraphNodes({
render_engine: 'cycles',
samples: 256,
width: 1920,
height: 1080,
})
return {
version: 1,
ui: { preset: 'custom', execution_mode: 'graph', family: 'order_line', blueprint },
nodes,
edges,
}
}
function buildStarterWorkflowConfigInternal(family: WorkflowStarterFamily = 'order_line'): WorkflowConfig {
if (family === 'cad_file') {
return {
version: 1,
ui: {
preset: 'custom',
execution_mode: 'legacy',
family: 'cad_file',
blueprint: 'starter_cad_intake',
},
nodes: [
buildWorkflowNode('resolve_step', 'resolve_step_path', 120, 140, {
label: 'Resolve STEP Path',
type: 'inputNode',
}),
],
edges: [],
}
}
return {
version: 1,
ui: {
preset: 'custom',
execution_mode: 'legacy',
family: 'order_line',
blueprint: 'starter_order_rendering',
},
nodes: [
buildWorkflowNode('setup', 'order_line_setup', 120, 140, {
label: 'Order Line Setup',
type: 'processNode',
}),
],
edges: [],
}
}
export function buildStillGraphNodes(renderParams: WorkflowParams): { nodes: WorkflowNode[]; edges: WorkflowEdge[] } {
return buildOrderLineStillGraphNodes(normalizeRenderParams(renderParams))
}
function migratePresetConfig(type: WorkflowPresetType, params: WorkflowParams = {}): WorkflowConfig {
return buildPresetWorkflowConfigInternal(type, params)
}
function normalizeWorkflowDefinition(raw: WorkflowDefinition): WorkflowDefinition {
const config = normalizeWorkflowConfig(raw.config as unknown as Record<string, unknown>)
return {
...raw,
family: raw.family ?? inferWorkflowFamily(config),
supported_artifact_kinds: Array.isArray(raw.supported_artifact_kinds)
? raw.supported_artifact_kinds
: [],
rollout_summary: {
linked_output_type_count: Number(raw.rollout_summary?.linked_output_type_count ?? 0),
active_output_type_count: Number(raw.rollout_summary?.active_output_type_count ?? 0),
linked_output_type_names: Array.isArray(raw.rollout_summary?.linked_output_type_names)
? raw.rollout_summary.linked_output_type_names
: [],
linked_output_types: Array.isArray(raw.rollout_summary?.linked_output_types)
? raw.rollout_summary.linked_output_types
.filter((outputType): outputType is WorkflowRolloutLinkedOutputType => (
outputType != null
&& typeof outputType === 'object'
&& typeof outputType.id === 'string'
&& typeof outputType.name === 'string'
))
.map(outputType => ({
id: outputType.id,
name: outputType.name,
is_active: Boolean(outputType.is_active),
artifact_kind: outputType.artifact_kind,
workflow_rollout_mode: outputType.workflow_rollout_mode ?? 'legacy_only',
}))
: [],
rollout_modes: Array.isArray(raw.rollout_summary?.rollout_modes)
? raw.rollout_summary.rollout_modes
: [],
has_blocking_contracts: Boolean(raw.rollout_summary?.has_blocking_contracts),
blocking_reasons: Array.isArray(raw.rollout_summary?.blocking_reasons)
? raw.rollout_summary.blocking_reasons
: [],
latest_run: raw.rollout_summary?.latest_run ?? null,
latest_shadow_run: raw.rollout_summary?.latest_shadow_run ?? null,
latest_rollout_gate_verdict: raw.rollout_summary?.latest_rollout_gate_verdict ?? null,
latest_rollout_ready:
typeof raw.rollout_summary?.latest_rollout_ready === 'boolean'
? raw.rollout_summary.latest_rollout_ready
: null,
latest_rollout_status: raw.rollout_summary?.latest_rollout_status ?? null,
latest_rollout_reasons: Array.isArray(raw.rollout_summary?.latest_rollout_reasons)
? raw.rollout_summary.latest_rollout_reasons
: [],
},
config,
}
}
@@ -455,14 +803,51 @@ export function normalizeWorkflowConfig(raw: Record<string, unknown>): WorkflowC
params: { ...(node.params ?? {}) },
}))
const edges = Array.isArray(raw.edges) ? (raw.edges as WorkflowEdge[]) : []
const mergedUi = {
...rawUi,
execution_mode: rawUi.execution_mode ?? 'legacy',
}
if (rawUi.preset === 'still_graph') {
const canonical = buildPresetWorkflowConfigInternal('still_graph', extractRenderParamsFromNodes(nodes, 'blender_still'))
return {
...canonical,
ui: {
...canonical.ui,
...mergedUi,
},
}
}
if (rawUi.blueprint === 'cad_intake' || rawUi.blueprint === 'order_rendering' || rawUi.blueprint === 'still_graph_reference') {
const canonical = buildWorkflowBlueprintConfig(rawUi.blueprint)
return {
...canonical,
ui: {
...canonical.ui,
...mergedUi,
},
}
}
if (rawUi.blueprint === 'starter_cad_intake' || rawUi.blueprint === 'starter_order_rendering') {
const canonical = buildStarterWorkflowConfigInternal(rawUi.blueprint === 'starter_cad_intake' ? 'cad_file' : 'order_line')
return {
...canonical,
ui: {
...canonical.ui,
...mergedUi,
},
}
}
return {
version: Number(raw.version ?? 1),
nodes,
edges,
ui: {
...rawUi,
execution_mode: rawUi.execution_mode ?? 'legacy',
family: rawUi.family ?? inferWorkflowFamily({ version: Number(raw.version ?? 1), nodes, edges }),
...mergedUi,
family: rawUi.family ?? inferWorkflowFamily({ version: Number(raw.version ?? 1), nodes, edges }) ?? undefined,
},
}
}
@@ -480,49 +865,11 @@ export function normalizeWorkflowConfig(raw: Record<string, unknown>): WorkflowC
}
export function createPresetWorkflowConfig(type: WorkflowPresetType, params: WorkflowParams = {}): WorkflowConfig {
return migratePresetConfig(type, params)
return buildPresetWorkflowConfigInternal(type, params)
}
export function createStarterWorkflowConfig(family: WorkflowStarterFamily = 'order_line'): WorkflowConfig {
if (family === 'cad_file') {
return {
version: 1,
ui: {
preset: 'custom',
execution_mode: 'legacy',
family: 'cad_file',
blueprint: 'starter_cad_intake',
},
nodes: [
{
id: 'resolve_step',
step: 'resolve_step_path',
params: {},
ui: { type: 'inputNode', label: 'Resolve STEP Path', position: { x: 120, y: 140 } },
},
],
edges: [],
}
}
return {
version: 1,
ui: {
preset: 'custom',
execution_mode: 'legacy',
family: 'order_line',
blueprint: 'starter_order_rendering',
},
nodes: [
{
id: 'setup',
step: 'order_line_setup',
params: {},
ui: { type: 'processNode', label: 'Order Line Setup', position: { x: 120, y: 140 } },
},
],
edges: [],
}
return buildStarterWorkflowConfigInternal(family)
}
export function getWorkflowPresetType(config: WorkflowConfig): WorkflowPresetType {
@@ -542,11 +889,12 @@ export function inferWorkflowFamily(config: WorkflowConfig): WorkflowNodeFamily
case 'threejs_render':
case 'thumbnail_save':
return 'cad_file'
case 'glb_bbox':
return null
case 'order_line_setup':
case 'resolve_template':
case 'material_map_resolve':
case 'auto_populate_materials':
case 'glb_bbox':
case 'blender_still':
case 'blender_turntable':
case 'output_save':
@@ -557,7 +905,7 @@ export function inferWorkflowFamily(config: WorkflowConfig): WorkflowNodeFamily
return null
}
})
.filter((family): family is WorkflowNodeFamily => family !== null),
.filter((family): family is Exclude<WorkflowNodeFamily, 'shared'> => family !== null),
)
if (families.size === 0) return null
if (families.size > 1) return 'mixed'