import type { Edge, Node } from '@xyflow/react' import { buildWorkflowBlueprintConfig, type WorkflowNodeDefinition, type WorkflowConfig, type WorkflowParams, } from '../../api/workflows' import { inferNodeLabel, inferNodeType, normalizeWorkflowParams, type WorkflowCanvasNodeData, } from './workflowGraphDraft' import type { WorkflowGraphFamily } from './workflowNodeLibrary' export type WorkflowReferenceBundleId = | 'still_render_reference' | 'still_render_alpha_reference' | 'still_render_blend_reference' | 'cad_intake_reference' export type WorkflowReferenceBundleDefinition = { id: WorkflowReferenceBundleId label: string shortLabel: string description: string family: Exclude stepIds: string[] stage: string } type WorkflowReferenceBundleInsertionResult = | { ok: true bundle: WorkflowReferenceBundleDefinition nodes: Node[] edges: Edge[] } | { ok: false reason: string } const WORKFLOW_REFERENCE_BUNDLE_REGISTRY: WorkflowReferenceBundleDefinition[] = [ { id: 'cad_intake_reference', label: 'CAD Intake Reference', shortLabel: 'CAD Intake Reference', description: 'Insert the canonical CAD intake path so extraction, geometry export, STL cache generation, and thumbnail publishing stay wired in a known-good order.', family: 'cad_file', stepIds: [ 'resolve_step_path', 'occ_object_extract', 'occ_glb_export', 'stl_cache_generate', 'thumbnail_save', ], stage: 'Reference Path', }, { id: 'still_render_reference', label: 'Still Render Reference', shortLabel: 'Still Reference', description: 'Insert the complete canonical non-legacy still-render path, including material resolution, render, output, and notification branches.', family: 'order_line', stepIds: [ 'order_line_setup', 'resolve_template', 'auto_populate_materials', 'glb_bbox', 'material_map_resolve', 'blender_still', 'output_save', 'notify', ], stage: 'Reference Path', }, { id: 'still_render_alpha_reference', label: 'Still Render Alpha Reference', shortLabel: 'Still Alpha', description: 'Insert the canonical still-render path with transparent-alpha defaults so compositing-ready PNG outputs stay workflow-first.', family: 'order_line', stepIds: [ 'order_line_setup', 'resolve_template', 'auto_populate_materials', 'glb_bbox', 'material_map_resolve', 'blender_still', 'output_save', 'notify', ], stage: 'Reference Path', }, { id: 'still_render_blend_reference', label: 'Still Render + Blend Reference', shortLabel: 'Still + Blend', description: 'Insert the canonical still-render path plus explicit .blend export delivery nodes so still and blend output-types can bind to the same graph family cleanly.', family: 'order_line', stepIds: [ 'order_line_setup', 'resolve_template', 'auto_populate_materials', 'glb_bbox', 'material_map_resolve', 'blender_still', 'output_save', 'notify', 'export_blend', ], stage: 'Reference Path', }, ] function buildNodeData( step: string, params: WorkflowParams = {}, definition?: WorkflowNodeDefinition, overrides?: Partial, ): WorkflowCanvasNodeData { return { label: overrides?.label ?? definition?.label ?? inferNodeLabel(step), params: normalizeWorkflowParams(params), step, description: overrides?.description ?? definition?.description, icon: overrides?.icon ?? definition?.icon, category: overrides?.category ?? definition?.category, } } function getReferenceTemplate(bundleId: WorkflowReferenceBundleId) { const toTemplate = (config: WorkflowConfig) => ({ nodes: config.nodes, edges: config.edges, }) switch (bundleId) { case 'cad_intake_reference': return toTemplate(buildWorkflowBlueprintConfig('cad_intake')) case 'still_render_reference': return toTemplate(buildWorkflowBlueprintConfig('still_graph_reference')) case 'still_render_alpha_reference': return toTemplate(buildWorkflowBlueprintConfig('still_graph_alpha_reference')) case 'still_render_blend_reference': return toTemplate(buildWorkflowBlueprintConfig('still_graph_blend_reference')) default: return null } } export function getWorkflowReferenceBundle(bundleId: WorkflowReferenceBundleId) { return WORKFLOW_REFERENCE_BUNDLE_REGISTRY.find(bundle => bundle.id === bundleId) ?? null } export function getWorkflowReferenceBundles( nodeDefinitions: WorkflowNodeDefinition[], graphFamily: WorkflowGraphFamily, ): WorkflowReferenceBundleDefinition[] { const availableSteps = new Set(nodeDefinitions.map(definition => definition.step)) return WORKFLOW_REFERENCE_BUNDLE_REGISTRY.filter(bundle => { if (graphFamily !== 'mixed' && bundle.family !== graphFamily) return false return bundle.stepIds.every(step => availableSteps.has(step)) }) } export function createWorkflowReferenceBundleInsertion(args: { bundleId: WorkflowReferenceBundleId graphFamily: WorkflowGraphFamily nodeDefinitionsByStep: Record existingNodes: Node[] preferredPosition?: { x: number; y: number } }): WorkflowReferenceBundleInsertionResult { const { bundleId, graphFamily, nodeDefinitionsByStep, existingNodes, preferredPosition } = args const bundle = getWorkflowReferenceBundle(bundleId) if (!bundle) { return { ok: false, reason: 'Unknown workflow reference path.' } } if (graphFamily !== 'mixed' && bundle.family !== graphFamily) { return { ok: false, reason: `${bundle.label} does not belong to the active authoring family.` } } const missingStep = bundle.stepIds.find(step => !nodeDefinitionsByStep[step]) if (missingStep) { return { ok: false, reason: `Workflow reference path is missing definition for ${missingStep}.` } } const template = getReferenceTemplate(bundleId) if (!template) { return { ok: false, reason: 'Workflow reference path template is unavailable.' } } const existingMaxX = existingNodes.length > 0 ? Math.max(...existingNodes.map(node => node.position.x)) : null const existingMaxY = existingNodes.length > 0 ? Math.max(...existingNodes.map(node => node.position.y)) : null const anchorX = preferredPosition?.x ?? (existingMaxX !== null ? existingMaxX + 260 : 120) const anchorY = preferredPosition?.y ?? (existingMaxY !== null ? existingMaxY + 80 : 120) const timestamp = Date.now() const templatePositions = template.nodes.map(node => node.ui?.position ?? { x: 0, y: 0 }) const minX = Math.min(...templatePositions.map(position => position.x)) const minY = Math.min(...templatePositions.map(position => position.y)) const nodeIdMap = new Map() const nodes = template.nodes.map(node => { const definition = nodeDefinitionsByStep[node.step] const templatePosition = node.ui?.position ?? { x: 0, y: 0 } const id = `${bundle.id}_${node.id}_${timestamp}` nodeIdMap.set(node.id, id) return { id, type: definition?.node_type ?? node.ui?.type ?? inferNodeType(node.step), position: { x: anchorX + (templatePosition.x - minX), y: anchorY + (templatePosition.y - minY), }, data: buildNodeData( node.step, node.params ?? definition?.defaults ?? {}, definition, node.ui?.label ? { label: node.ui.label, } : undefined, ), } satisfies Node }) const edges = template.edges.map(edge => ({ id: `${nodeIdMap.get(edge.from)}->${nodeIdMap.get(edge.to)}`, source: nodeIdMap.get(edge.from) ?? edge.from, target: nodeIdMap.get(edge.to) ?? edge.to, }) satisfies Edge) return { ok: true, bundle, nodes, edges, } }