npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@scory02/leafer-connector

v0.1.4

Published

A LeaferJS connector (edge) component with arrows, multiple route types, labels, and collaboration helpers.

Readme

leafer-connector

基于 LeaferJS(通过 leafer-editor)实现的连接线(Connector / Edge)组件,面向“白板 / 流程图 / 节点图”场景。

你可以把它理解成:给两个 IUI 节点自动画出一条“像流程图工具一样”的连线,并支持 label、协同等能力。

能力概览

  • 连接 2 个节点from/to: IUI
  • 端点模型padding / margin / side(auto) / percent / linkPoint
  • 路由类型orthogonal / bezier / straight / custom
  • 样式stroke / strokeWidth / dashPattern / startArrow / endArrow
  • 缩放策略scaleMode: world | pixel(线宽/箭头是否随 zoom 缩放)
  • 交互:双击连线创建/编辑 label(label 永远在路径中点)
  • 协同/程序更新updateMode="render" + renderThrottleMs
  • 状态同步getState/setState + onChange/onLabelChange 输出 diff

安装

pnpm add leafer-connector leafer-editor

leafer-editor 为 peerDependency,需要业务侧安装并锁定版本。

快速开始(最小可用)

最小输入参数只有:app + from/to

import { App, Rect } from "leafer-editor";
import { Connector } from "leafer-connector";

const app = new App({ view: container, editor: {} });

const a = new Rect({ x: 100, y: 100, width: 200, height: 160, fill: "#32cd79", draggable: true });
const b = new Rect({ x: 520, y: 280, width: 220, height: 160, fill: "#3b82f6", draggable: true });

const edge = new Connector(app, { from: a, to: b });
app.tree.add([a, b, edge]);

路由示例

正交(orthogonal)

const edge = new Connector(app, {
  from: a,
  to: b,
  routeType: "orthogonal",
  cornerRadius: 16,
});

Preview

贝塞尔(bezier)

const edge = new Connector(app, {
  from: a,
  to: b,
  routeType: "bezier",
  bezierCurvature: 0.6,
  routeOptions: {
    // 0:尽量保持贝塞尔;140:近距离自动降级正交更稳定
    bezierFallbackDistance: 0,
  },
});

Preview

直线(straight)

const edge = new Connector(app, {
  from: a,
  to: b,
  routeType: "straight",
});

Preview

自定义(custom + onDraw)

你可以在组件算出默认结果后,通过 onDraw 覆盖:

  • 覆盖 points(world 坐标):组件会基于 points 重新生成圆角路径并更新 label 中点
  • 覆盖 path(world 坐标 SVG path):支持 M/L/C/Q/Z(绝对坐标)。若只覆盖 path 不覆盖 points,label 会沿用默认中点
const edge = new Connector(app, {
  from: a,
  to: b,
  routeType: "custom",
  onDraw: ({ s, e, defaultResult }) => {
    // 1) 直接用默认结果
    // return

    // 2) 覆盖 points(world)
    // return { points: [s.linkPoint, s.paddingPoint, e.paddingPoint, e.linkPoint] }

    // 3) 覆盖 path(world,M/L/C/Q/Z)
    return { path: defaultResult.path };
  },
});

Preview

label(连线文字)示例

默认 label(有背景遮挡)

const edge = new Connector(app, {
  from: a,
  to: b,
  label: { text: "Hello", editable: true },
});

Preview

自定义 label 样式

const edge = new Connector(app, {
  from: a,
  to: b,
  label: {
    text: "关系",
    style: {
      fill: "#fff",
      fontSize: 12,
      fontFamily: "Arial",
      fontWeight: "bold",
      padding: [2, 6],
      boxStyle: { fill: "#00000099", cornerRadius: 6 },
    },
  },
});

参数总览(表格)

这一节把所有入参集中在一个地方,方便快速查阅(字段名使用“点号路径”表示嵌套结构)。

| 字段 | 类型 | 必填 | 默认值 | 说明 | | --- | --- | --- | --- | --- | | from | IUI | 是 | - | 起点节点 | | to | IUI | 是 | - | 终点节点 | | routeType | "orthogonal" \| "bezier" \| "straight" \| "custom" | 否 | "orthogonal" | 路由类型 | | padding | number | 否 | 20 | 出线段长度(从 linkPoint 沿法线外扩) | | margin | number | 否 | 0 | 连接点与节点边界间距(让线不贴边) | | cornerRadius | number | 否 | 16 | 正交/智能路由的圆角半径 | | opt1 | TargetOption | 否 | - | 起点单端端点策略(覆盖全局) | | opt2 | TargetOption | 否 | - | 终点单端端点策略(覆盖全局) | | bezierCurvature | number | 否 | 0.6 | bezier 曲率/张力(越大越“张开”) | | routeOptions | { ... } | 否 | - | smart-route 参数(会做深合并,未传字段会用默认值) | | routeOptions.avoidPadding | number | 否 | margin | 避障 padding:将需要避开的 bounds 外扩多少(local) | | routeOptions.intersectionPenalty | number | 否 | 1e6 | 线段与避障矩形相交的惩罚分(越大越“绕开”) | | routeOptions.longStraightRatio | number | 否 | 0.65 | 长直线惩罚阈值(maxSegment/total > ratio 开始惩罚) | | routeOptions.longStraightWeight | number | 否 | 2000 | 长直线惩罚权重 | | routeOptions.enableSRoutes | boolean | 否 | true | 是否生成 S-route(两次转折)候选 | | routeOptions.bezierFallbackDistance | number | 否 | 0 | bezier 近距离降级阈值(小于该值或节点重叠可降级为正交圆角) | | onDraw | ({ s, e, defaultResult }) => Partial<{ points; path }> \| void | 否 | - | 自定义绘制(入参/出参 points/path 都是 world 坐标) | | updateMode | "event" \| "render" \| "manual" | 否 | "event" | 自动更新模式(协同场景建议用 render) | | renderThrottleMs | number | 否 | 16 | render 模式节流(ms) | | getNodeId | (node: IUI) => string | 否 | - | 协同序列化:node -> id(用于 getState/onChange) | | onChange | ({ reason, prev, next, diff, changedKeys }) => void | 否 | - | 统一变更回调(reason: "label" \| "setState") | | onLabelChange | ({ oldText, newText }) => void | 否 | - | label 文本变化回调 | | stroke | string | 否 | "#ffffff" | 线条颜色 | | strokeWidth | number | 否 | 2 | 线宽 | | dashPattern | number[] | 否 | - | 虚线,例如 [6, 4] | | startArrow | IArrowStyle | 否 | - | 起点箭头样式 | | endArrow | IArrowStyle | 否 | "triangle" | 终点箭头样式 | | scaleMode | "world" \| "pixel" | 否 | "world" | 缩放策略(pixel:线宽/箭头保持像素大小) | | arrowBaseScale | number | 否 | 1 | 箭头基准缩放(配合 pixel 更常用) | | label | { ... } | 否 | - | 连线文字配置(存在时可显示/编辑) | | label.text | string | 否 | - | 初始文字(空/空白会被视为不创建 label) | | label.editable | boolean | 否 | - | 是否允许编辑(打开 inner editor) | | label.style | Partial<ITextInputData> | 否 | - | 文案样式(fill/fontSize/boxStyle/padding 等) | | labelOnDoubleClick | boolean | 否 | true | 是否允许双击连线打开/创建 label |

TargetOption(用于 opt1/opt2)表格

| 字段 | 类型 | 必填 | 默认值 | 说明 | | --- | --- | --- | --- | --- | | optX.side | "top" \| "right" \| "bottom" \| "left" \| "auto" | 否 | "auto" | 固定连接面,或自动择优 | | optX.percent | number | 否 | 0.5 | 连接点在该面的比例(0~1,0.5=边中点) | | optX.padding | number | 否 | - | 单端 padding(覆盖全局 padding) | | optX.margin | number | 否 | - | 单端 margin(覆盖全局 margin) | | optX.linkPoint | IPointData | 否 | - | 固定连接点(world,优先级最高) |

必填

  • from: IUI:起点节点
  • to: IUI:终点节点

端点与路由

  • routeType?: "orthogonal" | "bezier" | "straight" | "custom"
    • "orthogonal":正交折线 + 圆角(smart-route)
    • "bezier":smooth-step 风格曲线(在节点很近/重叠时可选降级为正交)
    • "straight":直线(仍会包含 linkPoint/paddingPoint 的出线段)
    • "custom":默认给一个可用结果,但你应通过 onDraw 覆盖
  • padding?: number:出线段长度(从 linkPoint 沿法线外扩)
  • margin?: number:连接点与节点边界的间距(让线不要贴边)
  • cornerRadius?: number:正交圆角半径
  • opt1?: TargetOption / opt2?: TargetOption:单端覆盖(见下方 TargetOption)

Bezier

  • bezierCurvature?: number:曲率/张力(越大曲线“张开”越明显)
  • routeOptions?.bezierFallbackDistance?: number:当 routeType="bezier" 时,若两端 padding 点距离小于该值(或节点重叠),可降级为正交圆角
    • 默认 0:尽量保持贝塞尔
    • 推荐 140:近距离更稳定、避免回勾

样式

  • stroke?: string
  • strokeWidth?: number
  • dashPattern?: number[]:虚线,例如 [6, 4]
  • startArrow?: IArrowStyle
  • endArrow?: IArrowStyle(默认 "triangle"

缩放策略

  • scaleMode?: "world" | "pixel"
    • "world":跟随画布缩放(默认)
    • "pixel":保持像素大小(线宽/箭头不随 zoom 变化)
  • arrowBaseScale?: number:箭头基准缩放(配合 pixel 模式更常用)

label(连线文字)

  • label?: { text?: string; editable?: boolean; style?: Partial<ITextInputData> }
  • labelOnDoubleClick?: boolean:是否允许双击连线打开/创建 label(默认 true)

提示:如果你不传 style.boxStyle/padding,组件会给 label 自动加半透明背景遮挡线条,保证可读。

更新模式(协同/性能)

  • updateMode?: "event" | "render" | "manual"
    • event:仅交互事件触发 update()(性能最好,默认)
    • render:每帧 RenderEvent.END 触发(适合协同/程序改变坐标)
    • manual:完全手动
  • renderThrottleMs?: numberrender 模式节流,推荐 16~33

协同同步

  • getNodeId?: (node: IUI) => string:用于 getState
  • onChange?: ({ reason, prev, next, diff, changedKeys }) => void:结构变化统一回调(用于写入 Yjs diff)
  • onLabelChange?: ({ oldText, newText }) => void:label 文本变化

TargetOption(单端端点策略)

opt1/opt2 的字段与优先级(从高到低):

  1. linkPoint?: IPointData(world 坐标):固定连接点(最高优先级)
  2. side?: "top" | "right" | "bottom" | "left" | "auto" + percent?: number:在某条边上按比例取点

其它:

  • padding?: number / margin?: number:单端覆盖
  • percent 默认 0.5(边中点)

协同:序列化/恢复(getState / setState)

你可以把 Connector 状态写入 Yjs(或其它 CRDT),并在远端恢复:

// 1) 序列化:需要提供 node -> id
const state = edge.getState((node) => String((node as any).id));

// 2) 恢复:需要提供 id -> node
edge.setState(state, (id) => nodeById.get(id));

协同:onChange / onLabelChange

如果你希望 label 变化 能自动产出 “可同步的 diff”,可以用 onChange

const edge = new Connector(app, {
  from: a,
  to: b,
  getNodeId: (node) => String((node as any).id),
  onChange: ({ reason, diff }) => {
    // 把 diff 写入 Yjs
    console.log(reason, diff);
  },
  onLabelChange: ({ oldText, newText }) => {
    console.log(oldText, newText);
  },
});

协同/性能注意事项:

  • diff 对比是稳定的:内部对对象字段做 key 排序稳定序列化,避免误触发 onChange
  • 避免重复绑定:同一节点多次被设置为 from/to 时,内部会去重绑定拖拽监听
  • label 变更会自动合并:输入过程中可能产生高频 RenderEvent.END,内部会合并到同一微任务批次再触发回调

性能与更新模式(updateMode)

  • event(默认):仅拖拽/交互触发 update(),性能最好
  • render:每帧 RenderEvent.END 强制 update(),适合协同/程序频繁更新坐标
  • manual:你自己控制刷新(可调用 connector.invalidate()connector.update()

render 模式节流

协同场景建议开启节流减少压力:

const edge = new Connector(app, {
  from: a,
  to: b,
  updateMode: "render",
  renderThrottleMs: 16,
});

routeOptions(smart-route 参数)

你可以用 routeOptions 调整正交 smart-route 的取舍(更偏好绕开/更偏好 S-route 等):

const edge = new Connector(app, {
  from: a,
  to: b,
  routeType: "orthogonal",
  routeOptions: {
    avoidPadding: 12,
    intersectionPenalty: 1e6,
    longStraightRatio: 0.65,
    longStraightWeight: 2000,
    enableSRoutes: true,
  },
});

bezierFallbackDistance 仅在 routeType="bezier" 时生效。

常见问题(FAQ)

1) 为什么我设置了 bezier,看起来还是折线?

  • 你可能没有设置 routeType: "bezier"(默认是 "orthogonal"
  • 或者你显式把 routeOptions.bezierFallbackDistance 设置成较大值(例如 140),导致近距离自动降级为正交圆角

2) 为什么平移画布后连线会“漂移”?

本组件内部会把路由计算与绘制统一在 Connector 的 local 坐标,并在 onDraw 回调里对外提供 world 坐标,已避免常见的坐标系漂移问题。
如果你在 onDraw 返回自定义 path,务必返回 world 坐标 path(组件会自动转回 local)。

API 导出

  • 导出:Connector 以及相关类型(见 src/types.ts

构建与发布

本包默认输出 双产物

  • ESM:dist/esm
  • CJS:dist/cjs.cjs 后缀)

可选:Rollup 生产 bundle(更适合做体积检查/发布前 smoke test):

pnpm run bundle:rollup

产物会输出到 dist/bundle/(含 *.min.*)。

发布前:

pnpm run build
npm publish

备注

  • 本包是 ESMtype: module),发布时会输出到 dist/(含类型声明)
  • leafer-editor 作为 peerDependency,需要由业务侧安装与锁定版本