import type { Node, XYPosition } from "@xyflow/react"; import type { AIConfig, FramePort, FramePresentation } from "./canvas-v2"; export type ElementKind = "text" | "image" | "script" | "plain-text" | "icon" | "frame" | "plain-image"; export type FrameLabelMode = "hidden" | "fixed" | "scaled"; export type TextStyle = { color?: string; background?: string; bold?: boolean; italic?: boolean; underline?: boolean; align?: "left" | "center" | "right"; fontFamily?: "sans" | "serif" | "mono"; lineHeight?: number }; export type CanvasNodeData = { kind: ElementKind; title: string; text: string; src?: string; rotation?: number; fontSize?: number; textStyle?: TextStyle; icon?: string; frameId?: string; labelMode?: FrameLabelMode; ai?: AIConfig; assetId?: string; subgraph?: { boardId: string }; ports?: FramePort[]; presentation?: FramePresentation; viewScale?: number; }; export type CanvasNode = Node & { pendingUpload?: string }; export type ElementCapabilities = { resizable: boolean; rotatable: boolean; connectable: boolean; branchable: boolean }; /** Business inheritance only: React views reuse these capabilities by composition. */ export abstract class CanvasElement { static readonly capabilities: ElementCapabilities = { resizable: true, rotatable: true, connectable: false, branchable: false }; abstract readonly kind: ElementKind; abstract readonly title: string; protected readonly nodeType: CanvasNode["type"] = "plainText"; protected readonly size = { width: 240, height: 72 }; readonly position: XYPosition; readonly text: string; readonly src?: string; constructor(position: XYPosition, text = "", src?: string) { this.position = position; this.text = text; this.src = src; } toNode(): CanvasNode { return { id: crypto.randomUUID(), type: this.nodeType, position: { ...this.position }, width: this.size.width, height: this.size.height, style: { width: this.size.width, height: this.size.height }, data: { kind: this.kind, title: this.title, text: this.text, rotation: 0, fontSize: this.kind === "plain-text" ? 24 : 13, ...(this.src ? { src: this.src } : {}) }, }; } } export abstract class CardElement extends CanvasElement { static override readonly capabilities: ElementCapabilities = { ...CanvasElement.capabilities, connectable: true, branchable: true }; protected override readonly nodeType = "material" as const; protected override readonly size = { width: 260, height: 200 }; } export class TextCardElement extends CardElement { readonly kind = "text"; readonly title = "灵感笔记"; } export class ImageCardElement extends CardElement { readonly kind = "image"; readonly title = "视觉参考"; } export class ScriptCardElement extends CardElement { readonly kind = "script"; readonly title = "脚本"; } export class PlainTextElement extends CanvasElement { readonly kind = "plain-text"; readonly title = "纯文本"; } export class IconElement extends CanvasElement { readonly kind = "icon"; readonly title = "图标"; protected override readonly nodeType = "icon" as const; protected override readonly size = { width: 80, height: 80 }; override toNode(): CanvasNode { const node = super.toNode(); return { ...node, data: { ...node.data, icon: "Star" } }; } } export class FrameElement extends CanvasElement { static override readonly capabilities: ElementCapabilities = { ...CanvasElement.capabilities, rotatable: false }; readonly kind = "frame"; readonly title = "未命名分区"; protected override readonly nodeType = "frame" as const; protected override readonly size = { width: 640, height: 400 }; override toNode(): CanvasNode { const node = super.toNode(); return { ...node, zIndex: -1, style: { ...node.style, pointerEvents: "none" }, data: { ...node.data, labelMode: "fixed" } }; } } export class PlainImageElement extends CanvasElement { readonly kind = "plain-image"; readonly title = "图片"; protected override readonly nodeType = "plainImage" as const; protected override readonly size = { width: 320, height: 240 }; } export const elementClasses = { text: TextCardElement, image: ImageCardElement, script: ScriptCardElement, "plain-text": PlainTextElement, icon: IconElement, frame: FrameElement, "plain-image": PlainImageElement }; export function elementCapabilities(kind: ElementKind) { return elementClasses[kind].capabilities; } export function createCanvasElement(kind: ElementKind, position: XYPosition, src?: string): CanvasNode { return new elementClasses[kind](position, "", src).toNode(); }