feat(canvas): add inherited elements, resizing and rotation

This commit is contained in:
2026-11-06 02:03:23 +08:00
parent e28ddb673f
commit 54ff5a0fbd
12 changed files with 411 additions and 38 deletions
+11 -8
View File
@@ -1,21 +1,23 @@
"use client";
import { createContext, useContext } from "react";
import { useContext } from "react";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import { FileText, Image as ImageIcon, Plus, Type } from "lucide-react";
import { elementCapabilities } from "@/lib/canvas-elements";
import { nodeDirections, type CanvasNode, type NodeDirection } from "@/lib/canvas";
import styles from "./canvas.module.css";
export const NodeEditing = createContext({ begin: () => {}, edit: (_id: string, _text: string) => {}, branch: (_id: string, _direction: NodeDirection) => {} });
import { TransformableNode, NodeEditing } from "./transformable-node";
export { NodeEditing } from "./transformable-node";
const positions = { top: Position.Top, right: Position.Right, bottom: Position.Bottom, left: Position.Left };
const labels = { top: "上方", right: "右侧", bottom: "下方", left: "左侧" };
export function MaterialNode({ id, data, selected }: NodeProps<CanvasNode>) {
export function MaterialNode(props: NodeProps<CanvasNode>) {
const { id, data, selected } = props;
const editing = useContext(NodeEditing);
const Icon = data.kind === "image" ? ImageIcon : data.kind === "script" ? FileText : Type;
return (
<article className={`${styles.node} ${selected ? styles.selectedNode : ""}`}>
<TransformableNode {...props} className={`${styles.node} ${selected ? styles.selectedNode : ""}`}>
<header className={styles.nodeHeading}><Icon size={13} /><span>{data.title}</span></header>
<Handle type="target" position={Position.Left} className={styles.legacyHandle} isConnectable={false} />
{data.kind === "image" ? <img className={styles.image} src={data.src} alt={data.title} draggable={false} referrerPolicy="no-referrer" /> : (
@@ -23,8 +25,9 @@ export function MaterialNode({ id, data, selected }: NodeProps<CanvasNode>) {
value={data.text} maxLength={100000} onFocus={editing.begin} onChange={(event) => editing.edit(id, event.target.value)} />
)}
<Handle type="source" position={Position.Right} className={styles.legacyHandle} isConnectable={false} />
{nodeDirections.map((direction) => <Handle key={direction} id={direction} type="source" position={positions[direction]} />)}
{selected && nodeDirections.map((direction) => (
{nodeDirections.map((direction) => <Handle key={direction} id={direction} type="source" position={positions[direction]}
style={direction === "left" || direction === "right" ? { top: "calc(50% + 20px)" } : { left: "calc(50% + 20px)" }} />)}
{selected && elementCapabilities(data.kind).branchable && nodeDirections.map((direction) => (
<button key={direction} type="button" className={`${styles.branchButton} ${styles["branch_" + direction]} nodrag nopan`}
aria-label={`在${labels[direction]}添加节点`} title={`在${labels[direction]}添加节点并连线`}
onPointerDown={(event) => event.stopPropagation()}
@@ -32,6 +35,6 @@ export function MaterialNode({ id, data, selected }: NodeProps<CanvasNode>) {
<Plus size={16} strokeWidth={1.6} aria-hidden="true" />
</button>
))}
</article>
</TransformableNode>
);
}