feat: stabilize workflow phase 1 foundation
This commit is contained in:
+178
-10
@@ -1,5 +1,7 @@
|
||||
import api from './client'
|
||||
|
||||
export type WorkflowPresetType = 'still' | 'turntable' | 'multi_angle' | 'still_with_exports' | 'custom'
|
||||
|
||||
export interface WorkflowDefinition {
|
||||
id: string
|
||||
name: string
|
||||
@@ -10,25 +12,45 @@ export interface WorkflowDefinition {
|
||||
}
|
||||
|
||||
export interface WorkflowConfig {
|
||||
type: 'still' | 'turntable' | 'multi_angle' | 'still_with_exports' | 'custom'
|
||||
params: WorkflowParams
|
||||
nodes?: WorkflowNode[]
|
||||
version: number
|
||||
nodes: WorkflowNode[]
|
||||
edges: WorkflowEdge[]
|
||||
ui?: WorkflowUi
|
||||
}
|
||||
|
||||
export interface WorkflowParams {
|
||||
[key: string]: unknown
|
||||
render_engine?: 'cycles' | 'eevee'
|
||||
samples?: number
|
||||
resolution?: [number, number]
|
||||
fps?: number
|
||||
duration_s?: number
|
||||
angles?: number[]
|
||||
rotation_z?: number
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface WorkflowNode {
|
||||
id: string
|
||||
type: string
|
||||
position: { x: number; y: number }
|
||||
data: Record<string, unknown>
|
||||
step: string
|
||||
params: WorkflowParams
|
||||
ui?: WorkflowNodeUi
|
||||
}
|
||||
|
||||
export interface WorkflowNodeUi {
|
||||
type?: string
|
||||
position?: { x: number; y: number }
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface WorkflowEdge {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
export interface WorkflowUi {
|
||||
preset?: WorkflowPresetType
|
||||
}
|
||||
|
||||
export interface WorkflowCreate {
|
||||
@@ -62,16 +84,16 @@ export interface WorkflowNodeResult {
|
||||
}
|
||||
|
||||
export const getWorkflows = (): Promise<WorkflowDefinition[]> =>
|
||||
api.get('/workflows').then(r => r.data)
|
||||
api.get('/workflows').then(r => r.data.map(normalizeWorkflowDefinition))
|
||||
|
||||
export const getWorkflow = (id: string): Promise<WorkflowDefinition> =>
|
||||
api.get(`/workflows/${id}`).then(r => r.data)
|
||||
api.get(`/workflows/${id}`).then(r => normalizeWorkflowDefinition(r.data))
|
||||
|
||||
export const createWorkflow = (data: WorkflowCreate): Promise<WorkflowDefinition> =>
|
||||
api.post('/workflows', data).then(r => r.data)
|
||||
api.post('/workflows', data).then(r => normalizeWorkflowDefinition(r.data))
|
||||
|
||||
export const updateWorkflow = (id: string, data: Partial<WorkflowCreate>): Promise<WorkflowDefinition> =>
|
||||
api.put(`/workflows/${id}`, data).then(r => r.data)
|
||||
api.put(`/workflows/${id}`, data).then(r => normalizeWorkflowDefinition(r.data))
|
||||
|
||||
export const deleteWorkflow = (id: string): Promise<void> =>
|
||||
api.delete(`/workflows/${id}`).then(() => undefined)
|
||||
@@ -96,3 +118,149 @@ export interface PipelineStepsResponse {
|
||||
|
||||
export const getPipelineSteps = (): Promise<PipelineStepsResponse> =>
|
||||
api.get('/workflows/pipeline-steps').then(r => r.data)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (type === 'still') {
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type },
|
||||
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 } } },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'setup', to: 'template' },
|
||||
{ from: 'template', to: 'render' },
|
||||
{ from: 'render', to: 'output' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'turntable') {
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type },
|
||||
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 } } },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'setup', to: 'template' },
|
||||
{ from: 'template', to: 'turntable' },
|
||||
{ from: 'turntable', to: 'output' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'multi_angle') {
|
||||
const angles = (params.angles ?? [0, 45, 90]).map(Number)
|
||||
const sharedParams = { ...renderParams }
|
||||
delete sharedParams.angles
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type },
|
||||
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 } } },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'setup', to: 'template' },
|
||||
...angles.map((_, index) => ({ from: 'template', to: `render_${index}` })),
|
||||
...angles.map((_, index) => ({ from: `render_${index}`, to: 'output' })),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'still_with_exports') {
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: type },
|
||||
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 } } },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'setup', to: 'template' },
|
||||
{ from: 'template', to: 'render' },
|
||||
{ from: 'render', to: 'output' },
|
||||
{ from: 'render', to: 'blend' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
ui: { preset: 'custom' },
|
||||
nodes: [
|
||||
{
|
||||
id: 'setup',
|
||||
step: 'order_line_setup',
|
||||
params: {},
|
||||
ui: { label: 'Order Line Setup', position: { x: 120, y: 140 } },
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWorkflowDefinition(raw: WorkflowDefinition): WorkflowDefinition {
|
||||
return {
|
||||
...raw,
|
||||
config: normalizeWorkflowConfig(raw.config as unknown as Record<string, unknown>),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeWorkflowConfig(raw: Record<string, unknown>): WorkflowConfig {
|
||||
if ('version' in raw && Array.isArray(raw.nodes)) {
|
||||
return {
|
||||
version: Number(raw.version ?? 1),
|
||||
nodes: (raw.nodes as WorkflowNode[]).map(node => ({
|
||||
...node,
|
||||
params: { ...(node.params ?? {}) },
|
||||
})),
|
||||
edges: Array.isArray(raw.edges) ? (raw.edges as WorkflowEdge[]) : [],
|
||||
ui: raw.ui as WorkflowUi | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof raw.type === 'string') {
|
||||
return migratePresetConfig(raw.type as WorkflowPresetType, (raw.params as WorkflowParams | undefined) ?? {})
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
ui: { preset: 'custom' },
|
||||
}
|
||||
}
|
||||
|
||||
export function createPresetWorkflowConfig(type: WorkflowPresetType, params: WorkflowParams = {}): WorkflowConfig {
|
||||
return migratePresetConfig(type, params)
|
||||
}
|
||||
|
||||
export function getWorkflowPresetType(config: WorkflowConfig): WorkflowPresetType {
|
||||
return config.ui?.preset ?? 'custom'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user