Skip to content

Canvas API 开发指南

Obsidian Canvas 是内置的白板功能,支持创建可视化节点图。通过 Canvas API,插件开发者可以读取、修改和扩展白板内容。

Canvas API 概述

Canvas API 在 Obsidian 1.1+ 中引入,允许插件:

  • 读取白板数据(节点、边、位置)
  • 程序化创建/修改/删除节点
  • 添加自定义节点类型
  • 监听白板事件

Canvas 数据结构

Canvas(白板)
├── Node(节点)
│   ├── Text Node(文本节点)
│   ├── File Node(文件节点)
│   ├── Link Node(链接节点)
│   └── Group Node(分组节点)
├── Edge(边/连接)
│   ├── id, fromNode, toNode
│   ├── label, color
│   └── fromSide, toSide
└── Viewport(视口)
    ├── x, y, zoom
    └── centerX, centerY

访问 Canvas

获取当前活动的 Canvas

typescript
import { App, TFile } from "obsidian";

function getActiveCanvas(app: App): Canvas | null {
  const view = app.workspace.getActiveViewOfType(ItemView);
  if (view && view.getViewType() === "canvas") {
    return (view as any).canvas as Canvas;
  }
  return null;
}

打开白板文件

typescript
async function openCanvas(app: App, file: TFile): Promise<Canvas | null> {
  await app.workspace.openLinkText(file.path, "", false);
  const leaf = app.workspace.getActiveViewOfType(ItemView);
  if (leaf && leaf.getViewType() === "canvas") {
    return (leaf as any).canvas;
  }
  return null;
}

节点操作

读取节点

typescript
interface Canvas {
  nodes: Map<string, CanvasNode>;
  edges: Map<string, CanvasEdge>;
  // ...
}

interface CanvasNode {
  id: string;
  x: number;
  y: number;
  width: number;
  height: number;
  color: string;
  type: "text" | "file" | "link" | "group";
  text?: string;      // 文本节点内容
  file?: TFile;       // 文件节点
  url?: string;       // 链接节点 URL
  // ...
}

// 读取所有节点
function getAllNodes(canvas: Canvas): CanvasNode[] {
  return Array.from(canvas.nodes.values());
}

// 读取文本节点
function getTextNodes(canvas: Canvas): CanvasNode[] {
  return getAllNodes(canvas).filter(n => n.type === "text");
}

// 获取选中节点
function getSelectedNodes(canvas: Canvas): CanvasNode[] {
  return getAllNodes(canvas).filter(n => n.isEditing);
}

创建节点

typescript
// 创建文本节点
function createTextNode(
  canvas: Canvas,
  text: string,
  x: number,
  y: number,
  width: number = 250,
  height: number = 60
): CanvasNode {
  return canvas.createTextNode({
    text: text,
    pos: { x, y },
    size: { width, height },
  });
}

// 创建文件节点
function createFileNode(
  canvas: Canvas,
  file: TFile,
  x: number,
  y: number,
  width: number = 400,
  height: number = 300
): CanvasNode {
  return canvas.createFileNode({
    file: file,
    pos: { x, y },
    size: { width, height },
  });
}

// 创建链接节点
function createLinkNode(
  canvas: Canvas,
  url: string,
  x: number,
  y: number,
  width: number = 400,
  height: number = 300
): CanvasNode {
  return canvas.createLinkNode({
    url: url,
    pos: { x, y },
    size: { width, height },
  });
}

// 创建分组节点
function createGroupNode(
  canvas: Canvas,
  label: string,
  x: number,
  y: number,
  width: number = 500,
  height: number = 400
): CanvasNode {
  return canvas.createGroupNode({
    pos: { x, y },
    size: { width, height },
    label: label,
  });
}

修改节点

typescript
// 移动节点
function moveNode(node: CanvasNode, x: number, y: number) {
  node.moveTo({ x, y });
}

// 调整大小
function resizeNode(node: CanvasNode, width: number, height: number) {
  node.resize({ width, height });
}

// 设置颜色
function setNodeColor(node: CanvasNode, color: string) {
  node.setColor(color);
}

// 设置文本内容
function setNodeText(node: CanvasNode, text: string) {
  if (node.type === "text") {
    node.setText(text);
  }
}

删除节点

typescript
function deleteNode(canvas: Canvas, node: CanvasNode) {
  canvas.removeNode(node);
}

// 批量删除
function deleteNodes(canvas: Canvas, nodes: CanvasNode[]) {
  canvas.beginTransaction();
  for (let node of nodes) {
    canvas.removeNode(node);
  }
  canvas.endTransaction();
}

边操作

创建连接

typescript
interface CanvasEdge {
  id: string;
  fromNode: string;
  toNode: string;
  fromSide?: "top" | "right" | "bottom" | "left";
  toSide?: "top" | "right" | "bottom" | "left";
  label?: string;
  color?: string;
}

// 创建边
function createEdge(
  canvas: Canvas,
  fromNode: CanvasNode,
  toNode: CanvasNode,
  label?: string
): CanvasEdge {
  return canvas.createEdge({
    from: fromNode.id,
    to: toNode.id,
    label: label,
    fromSide: "right",
    toSide: "left",
  });
}

批量创建连接

typescript
function connectNodes(
  canvas: Canvas,
  connections: Array<{ from: string; to: string; label?: string }>
) {
  canvas.beginTransaction();
  
  for (let conn of connections) {
    const fromNode = canvas.nodes.get(conn.from);
    const toNode = canvas.nodes.get(conn.to);
    
    if (fromNode && toNode) {
      canvas.createEdge({
        from: fromNode.id,
        to: toNode.id,
        label: conn.label,
      });
    }
  }
  
  canvas.endTransaction();
}

视口操作

typescript
// 缩放到指定节点
function zoomToNode(canvas: Canvas, node: CanvasNode) {
  canvas.zoomToSelection();
  // 或手动设置视口
  canvas.setViewport({
    x: node.x - 100,
    y: node.y - 100,
    zoom: 1.0,
  });
}

// 缩放以适应全部内容
function zoomToFit(canvas: Canvas) {
  canvas.zoomToFit();
}

// 获取当前视口
function getViewport(canvas: Canvas) {
  return {
    x: canvas.x,
    y: canvas.y,
    zoom: canvas.zoom,
  };
}

事件监听

typescript
// 监听节点选择变化
canvas.on("selection-change", () => {
  const selected = getSelectedNodes(canvas);
  console.log(`选中了 ${selected.length} 个节点`);
});

// 监听节点创建
canvas.on("node-added", (node: CanvasNode) => {
  console.log(`新节点: ${node.id}`);
});

// 监听节点删除
canvas.on("node-removed", (node: CanvasNode) => {
  console.log(`删除节点: ${node.id}`);
});

// 监听边创建
canvas.on("edge-added", (edge: CanvasEdge) => {
  console.log(`新连接: ${edge.fromNode} → ${edge.toNode}`);
});

// 监听视口变化
canvas.on("viewport-change", (viewport: any) => {
  // 防抖处理
  console.log(`视口: ${viewport.x}, ${viewport.y}, zoom: ${viewport.zoom}`);
});

实战案例:从笔记自动生成思维导图

typescript
import { Plugin, TFile } from "obsidian";

export default class MindMapPlugin extends Plugin {
  async generateMindMapFromNote(file: TFile) {
    // 读取笔记内容
    const content = await this.app.vault.read(file);
    
    // 解析标题层级
    const lines = content.split("\n");
    const nodes: Array<{
      id: string;
      text: string;
      level: number;
      x: number;
      y: number;
    }> = [];
    
    let y = 0;
    const baseX = 0;
    
    for (let line of lines) {
      const match = line.match(/^(#{1,6})\s+(.+)/);
      if (match) {
        const level = match[1].length;
        const text = match[2];
        nodes.push({
          id: `node-${nodes.length}`,
          text: text,
          level: level,
          x: baseX + (level - 1) * 300,
          y: y,
        });
        y += 80 + level * 10;
      }
    }
    
    if (nodes.length === 0) {
      new Notice("笔记中没有标题,无法生成思维导图");
      return;
    }
    
    // 打开白板
    const canvas = await this.openOrCreateCanvas();
    if (!canvas) return;
    
    // 创建节点
    canvas.beginTransaction();
    
    const nodeMap = new Map<string, CanvasNode>();
    
    for (let nodeData of nodes) {
      const colors = ["5", "4", "3", "2", "1", "6"]; // Canvas 颜色索引
      const color = colors[Math.min(nodeData.level - 1, colors.length - 1)];
      
      const canvasNode = canvas.createTextNode({
        text: nodeData.text,
        pos: { x: nodeData.x, y: nodeData.y },
        size: { width: 250, height: 60 },
        color: color,
      });
      
      nodeMap.set(nodeData.id, canvasNode);
    }
    
    // 创建连接(父子关系)
    for (let i = 1; i < nodes.length; i++) {
      const current = nodes[i];
      let parent = null;
      
      // 找到上一个层级更小的节点作为父节点
      for (let j = i - 1; j >= 0; j--) {
        if (nodes[j].level < current.level) {
          parent = nodes[j];
          break;
        }
      }
      
      if (parent) {
        const parentNode = nodeMap.get(parent.id);
        const childNode = nodeMap.get(current.id);
        if (parentNode && childNode) {
          canvas.createEdge({
            from: parentNode.id,
            to: childNode.id,
            fromSide: "right",
            toSide: "left",
          });
        }
      }
    }
    
    canvas.endTransaction();
    canvas.zoomToFit();
    
    new Notice(`已生成思维导图:${nodes.length} 个节点`);
  }
  
  async openOrCreateCanvas(): Promise<Canvas | null> {
    // 创建新的白板文件
    const fileName = `mindmap-${Date.now()}.canvas`;
    const filePath = `canvas/${fileName}`;
    
    // 确保 canvas 目录存在
    const folder = this.app.vault.getAbstractFileByPath("canvas");
    if (!folder) {
      await this.app.vault.createFolder("canvas");
    }
    
    // 创建空白 .canvas 文件
    const file = await this.app.vault.create(filePath, `{"nodes":[],"edges":[]}`);
    
    // 打开白板
    await this.app.workspace.openLinkText(filePath, "", false);
    const view = this.app.workspace.getActiveViewOfType(ItemView);
    if (view && view.getViewType() === "canvas") {
      return (view as any).canvas;
    }
    return null;
  }
}

保存与导出

读取 Canvas JSON

typescript
async function readCanvasData(app: App, file: TFile): Promise<any> {
  const content = await app.vault.read(file);
  return JSON.parse(content);
}

// 获取所有节点和边的数据
async function exportCanvas(app: App, file: TFile) {
  const data = await readCanvasData(app, file);
  return {
    nodes: data.nodes || [],
    edges: data.edges || [],
    nodeCount: (data.nodes || []).length,
    edgeCount: (data.edges || []).length,
  };
}

导出为 Markdown

typescript
async function canvasToMarkdown(canvas: Canvas): Promise<string> {
  const nodes = Array.from(canvas.nodes.values());
  const edges = Array.from(canvas.edges.values());
  
  let markdown = "# 白板导出\n\n";
  
  // 按位置排序
  nodes.sort((a, b) => a.y - b.y || a.x - b.x);
  
  for (let node of nodes) {
    if (node.type === "text") {
      markdown += `## ${node.text || "未命名节点"}\n\n`;
      markdown += `> 位置: (${node.x}, ${node.y})\n\n`;
    } else if (node.type === "file") {
      markdown += `## [[${node.file?.basename || "文件"}]]\n\n`;
    }
  }
  
  markdown += "\n## 连接关系\n\n";
  for (let edge of edges) {
    const fromNode = canvas.nodes.get(edge.fromNode);
    const toNode = canvas.nodes.get(edge.toNode);
    if (fromNode && toNode) {
      markdown += `- ${fromNode.text || fromNode.id} → ${toNode.text || toNode.id}\n`;
    }
  }
  
  return markdown;
}

注意事项

Canvas API 注意事项

  1. 实验性 API:Canvas API 仍可能变化,请关注 Obsidian 更新日志
  2. 事务管理:批量操作使用 beginTransaction() / endTransaction()
  3. 坐标系统:Canvas 坐标原点在左上角,x 向右增,y 向下增
  4. 文件格式.canvas 文件是 JSON 格式,可直接读写
  5. 性能:大量节点时注意性能,建议分批操作

相关文档