import type { StepCategory, WorkflowNodeDefinition, WorkflowNodeFamily } from '../../api/workflows' export type WorkflowNodeFamilyFilter = 'all' | WorkflowNodeFamily export type WorkflowGraphFamily = Exclude | 'mixed' export type WorkflowNodeKindFilter = 'all' | 'legacy' | 'bridge' | 'graph' export type WorkflowNodeLibraryGroup = 'legacy' | 'bridge' | 'graph' export type WorkflowAuthoringStage = | 'cad_intake' | 'scene_prep' | 'materials' | 'render' | 'publish' | 'orchestration' export const CATEGORY_LABELS: Record = { input: 'Input', processing: 'Processing', rendering: 'Rendering', output: 'Output', } export const CATEGORY_COLORS: Record = { input: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', processing: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', rendering: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300', output: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300', } export const NODE_CATEGORY_ORDER: StepCategory[] = ['input', 'processing', 'rendering', 'output'] export const FAMILY_FILTER_LABELS: Record = { all: 'All Nodes', cad_file: 'CAD Intake', shared: 'Shared', order_line: 'Order Rendering', } export const NODE_KIND_FILTER_LABELS: Record = { all: 'All Modes', legacy: 'Legacy', bridge: 'Bridge', graph: 'Graph', } export const NODE_LIBRARY_GROUP_LABELS: Record = { legacy: 'Legacy Nodes', bridge: 'Bridge Nodes', graph: 'Graph Nodes', } export const NODE_LIBRARY_GROUP_STYLES: Record = { legacy: 'bg-slate-100 text-slate-700 dark:bg-slate-900/40 dark:text-slate-300', bridge: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', graph: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300', } export const NODE_LIBRARY_GROUP_DESCRIPTIONS: Record = { legacy: 'Legacy-safe nodes that map cleanly to the existing production path.', bridge: 'Compatibility nodes that still rely on bridge execution behavior.', graph: 'Native graph runtime nodes for the non-legacy editor flow.', } export const AUTHORING_STAGE_ORDER: WorkflowAuthoringStage[] = [ 'cad_intake', 'scene_prep', 'materials', 'render', 'publish', 'orchestration', ] export const AUTHORING_STAGE_LABELS: Record = { cad_intake: 'CAD Intake', scene_prep: 'Scene Prep', materials: 'Materials', render: 'Render', publish: 'Publish', orchestration: 'Orchestration', } export const AUTHORING_STAGE_DESCRIPTIONS: Record = { cad_intake: 'Import CAD sources, extract geometry, and prepare downstream preview assets.', scene_prep: 'Resolve context, templates, geometry metadata, and upstream render state.', materials: 'Map, normalize, or override materials before render execution.', render: 'Generate stills, thumbnails, or other rendered artifacts.', publish: 'Persist outputs and emit downstream completion signals.', orchestration: 'Support glue, control flow, or utility nodes that do not belong to a single production stage.', } export const AUTHORING_STAGE_STYLES: Record = { cad_intake: 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-300', scene_prep: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300', materials: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', render: 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300', publish: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300', orchestration: 'bg-slate-100 text-slate-700 dark:bg-slate-900/40 dark:text-slate-300', } export const FAMILY_FILTER_DESCRIPTIONS: Record = { cad_file: 'Start with a CAD file context and produce previews, caches, or derived assets.', shared: 'Reusable nodes that can be dropped into either CAD-intake or order-rendering workflows.', order_line: 'Start with an order line context and run production rendering/output steps.', } export const FAMILY_FILTER_STYLES: Record = { cad_file: 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-300', shared: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300', order_line: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300', } export const GRAPH_FAMILY_LABELS: Record = { cad_file: 'CAD Intake', order_line: 'Order Rendering', mixed: 'Mixed Family', } export const GRAPH_FAMILY_STYLES: Record = { cad_file: FAMILY_FILTER_STYLES.cad_file, order_line: FAMILY_FILTER_STYLES.order_line, mixed: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', } export type WorkflowNodeDefinitionMap = Record const CAD_FILE_NODE_STEPS = new Set([ 'resolve_step_path', 'occ_object_extract', 'occ_glb_export', 'stl_cache_generate', 'blender_render', 'threejs_render', 'thumbnail_save', ]) const SHARED_NODE_STEPS = new Set([ 'glb_bbox', ]) export function getNodeFamily(step: string, nodeDefinitionsByStep?: WorkflowNodeDefinitionMap): WorkflowNodeFamily { if (nodeDefinitionsByStep?.[step]?.family) { return nodeDefinitionsByStep[step].family } if (SHARED_NODE_STEPS.has(step)) return 'shared' return CAD_FILE_NODE_STEPS.has(step) ? 'cad_file' : 'order_line' } export function getDefinitionFamily( definition: WorkflowNodeDefinition, nodeDefinitionsByStep?: WorkflowNodeDefinitionMap, ): WorkflowNodeFamily { return definition.family ?? getNodeFamily(definition.step, nodeDefinitionsByStep) } export function isDefinitionAllowedForGraphFamily( definition: WorkflowNodeDefinition, graphFamily: WorkflowGraphFamily, nodeDefinitionsByStep?: WorkflowNodeDefinitionMap, ): boolean { if (graphFamily === 'mixed') return true const family = getDefinitionFamily(definition, nodeDefinitionsByStep) return family === 'shared' || family === graphFamily } export function compareNodeDefinitions(a: WorkflowNodeDefinition, b: WorkflowNodeDefinition) { const categoryDelta = NODE_CATEGORY_ORDER.indexOf(a.category) - NODE_CATEGORY_ORDER.indexOf(b.category) if (categoryDelta !== 0) return categoryDelta return a.label.localeCompare(b.label) } export function getDefinitionModuleNamespace(definition: WorkflowNodeDefinition): string { const [namespace] = definition.module_key.split('.') return namespace || 'workflow' } export function getDefinitionModuleLabel(definition: WorkflowNodeDefinition): string { const namespace = getDefinitionModuleNamespace(definition) return namespace .split('_') .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(' ') } export function getDefinitionAuthoringStage(definition: WorkflowNodeDefinition): WorkflowAuthoringStage { const moduleKey = definition.module_key.toLowerCase() if (moduleKey.startsWith('cad.')) { if (definition.category === 'rendering') return 'render' if (definition.category === 'output') return 'publish' return 'cad_intake' } if ( moduleKey.startsWith('order_line.') || moduleKey.startsWith('geometry.') || moduleKey.startsWith('context.') ) { return 'scene_prep' } if (moduleKey.startsWith('materials.')) { return 'materials' } if (moduleKey.startsWith('render.') || moduleKey.startsWith('rendering.')) { return 'render' } if (moduleKey.startsWith('media.') || moduleKey.startsWith('notifications.')) { return 'publish' } if (definition.category === 'rendering') return 'render' if (definition.category === 'output') return 'publish' if (definition.category === 'input' || definition.category === 'processing') return 'orchestration' return 'orchestration' } export function groupDefinitionsForStepSelect(definitions: WorkflowNodeDefinition[]) { const groups = new Map() for (const definition of [...definitions].sort(compareNodeDefinitions)) { const family = getDefinitionFamily(definition) const groupLabel = `${FAMILY_FILTER_LABELS[family]} · ${AUTHORING_STAGE_LABELS[getDefinitionAuthoringStage(definition)]} · ${getDefinitionModuleLabel(definition)}` groups.set(groupLabel, [...(groups.get(groupLabel) ?? []), definition]) } return Array.from(groups.entries()).map(([label, options]) => ({ label, options })) } export function groupDefinitionsByFamily( definitions: WorkflowNodeDefinition[], nodeDefinitionsByStep?: WorkflowNodeDefinitionMap, ) { return { cad_file: definitions .filter(definition => getDefinitionFamily(definition, nodeDefinitionsByStep) === 'cad_file') .sort(compareNodeDefinitions), shared: definitions .filter(definition => getDefinitionFamily(definition, nodeDefinitionsByStep) === 'shared') .sort(compareNodeDefinitions), order_line: definitions .filter(definition => getDefinitionFamily(definition, nodeDefinitionsByStep) === 'order_line') .sort(compareNodeDefinitions), } as Record } export function groupDefinitionsByModule(definitions: WorkflowNodeDefinition[]) { const groups = new Map< string, { namespace: string label: string definitions: WorkflowNodeDefinition[] } >() for (const definition of [...definitions].sort(compareNodeDefinitions)) { const namespace = getDefinitionModuleNamespace(definition) const existing = groups.get(namespace) if (existing) { existing.definitions.push(definition) continue } groups.set(namespace, { namespace, label: getDefinitionModuleLabel(definition), definitions: [definition], }) } return Array.from(groups.values()).sort((a, b) => a.label.localeCompare(b.label)) } export function getPrimaryLibraryGroup(definition: WorkflowNodeDefinition): WorkflowNodeLibraryGroup { if (definition.execution_kind === 'native') { return 'graph' } if (definition.legacy_compatible) { return 'legacy' } return 'bridge' } export function matchesNodeKindFilter( definition: WorkflowNodeDefinition, filter: WorkflowNodeKindFilter, ): boolean { if (filter === 'all') return true if (filter === 'legacy') return definition.legacy_compatible if (filter === 'bridge') return definition.execution_kind === 'bridge' return definition.execution_kind === 'native' } export function getDefinitionSearchText(definition: WorkflowNodeDefinition): string { return [ definition.label, definition.step, definition.module_key, getDefinitionModuleLabel(definition), definition.description, CATEGORY_LABELS[definition.category], FAMILY_FILTER_LABELS[getDefinitionFamily(definition)], definition.execution_kind === 'bridge' ? 'bridge' : 'graph', definition.legacy_compatible ? 'legacy' : '', definition.artifact_roles_consumed.join(' '), definition.artifact_roles_produced.join(' '), ] .join(' ') .toLowerCase() } export function getDefinitionBadges(definition: WorkflowNodeDefinition) { const badges: { label: string; className: string }[] = [] if (definition.legacy_compatible) { badges.push({ label: 'Legacy', className: NODE_LIBRARY_GROUP_STYLES.legacy, }) } badges.push({ label: definition.execution_kind === 'bridge' ? 'Bridge' : 'Graph', className: definition.execution_kind === 'bridge' ? NODE_LIBRARY_GROUP_STYLES.bridge : NODE_LIBRARY_GROUP_STYLES.graph, }) return badges } export function groupDefinitionsByPrimaryLibraryGroup(definitions: WorkflowNodeDefinition[]) { return { legacy: definitions.filter(definition => getPrimaryLibraryGroup(definition) === 'legacy').sort(compareNodeDefinitions), bridge: definitions.filter(definition => getPrimaryLibraryGroup(definition) === 'bridge').sort(compareNodeDefinitions), graph: definitions.filter(definition => getPrimaryLibraryGroup(definition) === 'graph').sort(compareNodeDefinitions), } as Record }