341 lines
12 KiB
TypeScript
341 lines
12 KiB
TypeScript
import type { StepCategory, WorkflowNodeDefinition, WorkflowNodeFamily } from '../../api/workflows'
|
|
|
|
export type WorkflowNodeFamilyFilter = 'all' | WorkflowNodeFamily
|
|
export type WorkflowGraphFamily = Exclude<WorkflowNodeFamily, 'shared'> | '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<StepCategory, string> = {
|
|
input: 'Input',
|
|
processing: 'Processing',
|
|
rendering: 'Rendering',
|
|
output: 'Output',
|
|
}
|
|
|
|
export const CATEGORY_COLORS: Record<StepCategory, string> = {
|
|
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<WorkflowNodeFamilyFilter, string> = {
|
|
all: 'All Nodes',
|
|
cad_file: 'CAD Intake',
|
|
shared: 'Shared',
|
|
order_line: 'Order Rendering',
|
|
}
|
|
|
|
export const NODE_KIND_FILTER_LABELS: Record<WorkflowNodeKindFilter, string> = {
|
|
all: 'All Modes',
|
|
legacy: 'Legacy',
|
|
bridge: 'Bridge',
|
|
graph: 'Graph',
|
|
}
|
|
|
|
export const NODE_LIBRARY_GROUP_LABELS: Record<WorkflowNodeLibraryGroup, string> = {
|
|
legacy: 'Legacy Nodes',
|
|
bridge: 'Bridge Nodes',
|
|
graph: 'Graph Nodes',
|
|
}
|
|
|
|
export const NODE_LIBRARY_GROUP_STYLES: Record<WorkflowNodeLibraryGroup, string> = {
|
|
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<WorkflowNodeLibraryGroup, string> = {
|
|
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<WorkflowAuthoringStage, string> = {
|
|
cad_intake: 'CAD Intake',
|
|
scene_prep: 'Scene Prep',
|
|
materials: 'Materials',
|
|
render: 'Render',
|
|
publish: 'Publish',
|
|
orchestration: 'Orchestration',
|
|
}
|
|
|
|
export const AUTHORING_STAGE_DESCRIPTIONS: Record<WorkflowAuthoringStage, string> = {
|
|
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<WorkflowAuthoringStage, string> = {
|
|
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<WorkflowNodeFamily, string> = {
|
|
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<WorkflowNodeFamily, string> = {
|
|
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<WorkflowGraphFamily, string> = {
|
|
cad_file: 'CAD Intake',
|
|
order_line: 'Order Rendering',
|
|
mixed: 'Mixed Family',
|
|
}
|
|
|
|
export const GRAPH_FAMILY_STYLES: Record<WorkflowGraphFamily, string> = {
|
|
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<string, WorkflowNodeDefinition>
|
|
|
|
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<string, WorkflowNodeDefinition[]>()
|
|
|
|
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<WorkflowNodeFamily, WorkflowNodeDefinition[]>
|
|
}
|
|
|
|
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<WorkflowNodeLibraryGroup, WorkflowNodeDefinition[]>
|
|
}
|