159 lines
5.9 KiB
TypeScript
159 lines
5.9 KiB
TypeScript
/**
|
|
* Extensible node type registry. Register node types with registerNodeType();
|
|
* built-in types are registered in registerBuiltinNodes.ts.
|
|
* Use getRegisteredNodeTypes() / getNodeType(id) for defaults, validation, and UI.
|
|
*/
|
|
|
|
import type React from 'react'
|
|
import type { Node } from '@xyflow/react'
|
|
|
|
/** Node classification for UI: Psyche, Pneuma, Physis, Archon (AI Agent). */
|
|
export type NodeClassification = 'psyche' | 'pneuma' | 'physis' | 'archon'
|
|
|
|
export const NODE_CLASSIFICATION_LABELS: Record<NodeClassification, string> = {
|
|
psyche: 'Psyche',
|
|
pneuma: 'Pneuma',
|
|
physis: 'Physis',
|
|
archon: 'Archon',
|
|
}
|
|
|
|
export type NodeHelpEntry = {
|
|
title: string
|
|
content: React.ReactNode
|
|
}
|
|
|
|
export type NodeTypeDescriptor = {
|
|
id: string
|
|
component: React.ComponentType<any>
|
|
defaultStyle: { width: number; height: number }
|
|
defaultData: Record<string, unknown>
|
|
idPrefix: string
|
|
hasInput: boolean
|
|
hasOutput: boolean
|
|
/** Classification for creation menu and node footer: Psyche, Pneuma, Physis. */
|
|
classification?: NodeClassification
|
|
/** When this type is the connection target, which source types are allowed. Omit = all. */
|
|
allowedSourceTypes?: string[]
|
|
/** When this type is the connection source, which target types are allowed. Omit = all. */
|
|
allowedTargetTypes?: string[]
|
|
help: NodeHelpEntry
|
|
menuLabel: string
|
|
menuIcon: React.ReactNode
|
|
/** Optional: override default data for new nodes (e.g. set title from newId). */
|
|
getDefaultData?: (newId?: string) => Record<string, unknown>
|
|
/** Optional: data for Reset action; if omitted, getDefaultData(nodeId) or defaultData is used. */
|
|
getResetData?: (nodeId?: string) => Record<string, unknown>
|
|
/** Optional: label shown on edge when this type is the target (e.g. "render", "add input"). */
|
|
connectionLabel?: string
|
|
/** When true, double-clicking the node header opens a fullscreen dialog for this node. */
|
|
supportsFullscreen?: boolean
|
|
}
|
|
|
|
const registry = new Map<string, NodeTypeDescriptor>()
|
|
|
|
export function registerNodeType(descriptor: NodeTypeDescriptor): void {
|
|
if (registry.has(descriptor.id)) {
|
|
console.warn(`[nodeRegistry] Overwriting existing node type: ${descriptor.id}`)
|
|
}
|
|
registry.set(descriptor.id, descriptor)
|
|
}
|
|
|
|
export function getNodeType(id: string): NodeTypeDescriptor | undefined {
|
|
return registry.get(id)
|
|
}
|
|
|
|
const CLASSIFICATION_ORDER: NodeClassification[] = ['psyche', 'pneuma', 'physis', 'archon']
|
|
|
|
export function getRegisteredNodeTypes(): NodeTypeDescriptor[] {
|
|
return Array.from(registry.values())
|
|
}
|
|
|
|
/** Node types grouped by classification for the Create Node menu. Order: Psyche, Pneuma, Physis. */
|
|
export function getRegisteredNodeTypesGroupedByClassification(): {
|
|
classification: NodeClassification
|
|
label: string
|
|
types: NodeTypeDescriptor[]
|
|
}[] {
|
|
const byClass = new Map<NodeClassification, NodeTypeDescriptor[]>()
|
|
for (const c of CLASSIFICATION_ORDER) byClass.set(c, [])
|
|
for (const desc of registry.values()) {
|
|
const c = desc.classification ?? 'physis'
|
|
byClass.get(c)!.push(desc)
|
|
}
|
|
return CLASSIFICATION_ORDER.map((classification) => ({
|
|
classification,
|
|
label: NODE_CLASSIFICATION_LABELS[classification],
|
|
types: byClass.get(classification)!,
|
|
}))
|
|
}
|
|
|
|
export function getRegisteredNodeTypeIds(): string[] {
|
|
return Array.from(registry.keys())
|
|
}
|
|
|
|
/** Default data for a new node of this type. Uses getDefaultData from descriptor if provided. */
|
|
export function getDefaultDataForType(type: string, newId?: string): Record<string, unknown> {
|
|
const desc = registry.get(type)
|
|
if (!desc) return {}
|
|
if (desc.getDefaultData) return { ...desc.getDefaultData(newId) }
|
|
const base = { ...desc.defaultData }
|
|
if (type === 'config' && newId) (base as any).title = `${newId}`
|
|
return base
|
|
}
|
|
|
|
/** Data for Reset action. Uses getResetData from descriptor if provided. */
|
|
export function getResetDataForType(type: string, nodeId?: string): Record<string, unknown> {
|
|
const desc = registry.get(type)
|
|
if (!desc) return getDefaultDataForType(type, nodeId)
|
|
if (desc.getResetData) return { ...desc.getResetData(nodeId) }
|
|
return getDefaultDataForType(type, nodeId)
|
|
}
|
|
|
|
/** Id prefix for this type (e.g. cfg_, rnd_). Used by getNextNodeId. */
|
|
export function getIdPrefix(type: string): string {
|
|
return getNodeType(type)?.idPrefix ?? 'node_'
|
|
}
|
|
|
|
/** Default style for this type. */
|
|
export function getDefaultStyle(type: string): { width: number; height: number } {
|
|
const desc = getNodeType(type)
|
|
if (desc) return desc.defaultStyle
|
|
return { width: 320, height: 320 }
|
|
}
|
|
|
|
/** Whether a connection from source to target is allowed based on registered types. */
|
|
export function isConnectionAllowed(
|
|
sourceType: string,
|
|
targetType: string,
|
|
sourceNodeId: string,
|
|
targetNodeId: string
|
|
): boolean {
|
|
if (sourceNodeId === targetNodeId) return false
|
|
const sourceDesc = getNodeType(sourceType)
|
|
const targetDesc = getNodeType(targetType)
|
|
if (!sourceDesc || !targetDesc) return false
|
|
if (!targetDesc.hasInput) return false
|
|
if (!sourceDesc.hasOutput) return false
|
|
if (targetDesc.allowedSourceTypes != null && !targetDesc.allowedSourceTypes.includes(sourceType)) return false
|
|
if (sourceDesc.allowedTargetTypes != null && !sourceDesc.allowedTargetTypes.includes(targetType)) return false
|
|
return true
|
|
}
|
|
|
|
/** Edge label when target is of this type (for AnimatedEdge). */
|
|
export function getConnectionLabelForTarget(targetType: string): string | undefined {
|
|
return getNodeType(targetType)?.connectionLabel
|
|
}
|
|
|
|
/** Help entry for a node type. Use in NodeHelpPopover. */
|
|
export function getNodeHelp(nodeType: string): NodeHelpEntry {
|
|
const desc = getNodeType(nodeType)
|
|
return desc?.help ?? { title: nodeType, content: null }
|
|
}
|
|
|
|
/** Classification label for a node type (e.g. "Psyche"). Shown in node footer before help button. */
|
|
export function getNodeClassificationLabel(nodeType: string): string | null {
|
|
const desc = getNodeType(nodeType)
|
|
const c = desc?.classification
|
|
return c ? NODE_CLASSIFICATION_LABELS[c] : null
|
|
}
|