92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
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<CanvasNodeData, "material" | "plainText" | "icon" | "frame" | "plainImage"> & { 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();
|
|
}
|