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

@fulate/tools

v1.0.11

Published

每个活动 Root 只有一个 `EditorInteractionController`。它拥有 selection、输入仲裁、Transform/Point gesture、Content session、Line/LineTree branch draft 与 semantic History 边界;`Select` 只保留 command/paint surface,`LineTool` 只保留 command/preview surface。Transform 的每个已派发 pointer 输

Readme

@fulate/tools Editor integration

每个活动 Root 只有一个 EditorInteractionController。它拥有 selection、输入仲裁、Transform/Point gesture、Content session、Line/LineTree branch draft 与 semantic History 边界;Select 只保留 command/paint surface,LineTool 只保留 command/preview surface。Transform 的每个已派发 pointer 输入都立即从 frozen baseline 解析并提交一个 trusted batch;Point gesture 继续只在每个显示帧消费最后一个 intent。 每个 owner 都把 world delta 交给同一个 Core reference-transform kernel;普通 transform 与显式 reparent intent由该 intent决定写 pose、resize-owned geometry或preserve-owned geometry。移动/旋转 Group时只写Group pose;Group或多选的尺寸调整、镜像会递归把同一个world仿射结果写回可编辑子元素 的 authored geometry(anchor、cubic control、LineTree point),不会把尺寸变化留在Group scale上。 DirectChild手势只写该child;derived content bounds不形成command、History或补偿写入。 Line/LineTree固定world paint padding只属于自身visual/render coverage,不进入Group selection、resize或 snap bounds。所有工作复用现有RootFrameRuntime,Tools 不创建 RAF、timer 或第二 scheduler。 SelectSnapRuleLineTool 都使用 Core 生成的普通 Node key。工具间协作由应用组合时交付 的对象引用完成,不把固定 key 当作 Root 服务定位:

const snap = new Snap();
const lineTool = new LineTool({ contentLayer });
const select = new Select({ snap, lineTool });
const rule = new Rule({ select });
const editorLayer = new EditorLayer({
  children: [select, snap, rule, lineTool]
});

DirectChild开始axis Snap时,手势排除集按候选使用的同一selection-owner规则解析;包含该child的 ancestor Group不会成为自身拖拽候选,Group外部元素仍正常参与对齐。

LineTree Point改变一个anchor时,即使当前pose包含resize或反向scale,也在同一owner change内保持 既有world matrix,未编辑points不发生位移;整树resize把拓扑点按完整仿射写回。Group默认旋转中心与九点 pivot吸附按当前local visual/content selection bounds解析;稳定layout reference只拥有Group pose。

Selection controls 是一份 immutable SelectionOverlayProjection。paint、control/body hit 与 cursor 仲裁读取同一对象;场景内容 hit/hover 仍由 Core Root.hoverPath/currentElement 唯一拥有。Tools 不写 DOM cursor,不保存 currentControlhoverElement 或第二份 selection frame。 单选与多选的每个 selected owner 都由这份投影调用其唯一 paintHover() 绘制本体高亮,不依赖鼠标 是否仍停留在图元上。 Rule 也直接消费这份投影:它把当前单选或多选 frame 的 world AABB 在水平、垂直标尺上各绘制 为一段区间,并标出两端坐标。尺寸、背景、角块、主/次刻度、边框、文字与选区颜色都可以在构造时 配置,并可在挂载后通过一次 setOptions() 更新;实际变化只登记一次 Editor overlay 重绘,相同值为 noop。主刻度与次刻度分别绘制。省略 selectionColor 时每次绘制读取当前 root.skin.selection,自动跟随 Select 外框:

const rule = new Rule({
  select,
  rulerSize: 50,
  backgroundColor: "#000000",
  cornerBackgroundColor: "#0f0f0f",
  majorTickColor: "#3f3f3f",
  minorTickColor: "#3f3f3f",
  borderColor: "#252525",
  textColor: "#cccccc"
});

rule.setOptions({ rulerSize: 40, textColor: "#ffffff" });

Rule 是覆盖在 Root 顶部和左侧的 screen-space 编辑器层,不会平移 Viewport。活动 Rule 会把当前 标尺占用提供给同一 Root;Workspace 在初始几何提交后、首个Canvas paint前执行首次无动画聚焦, Root尺寸变化后按最终尺寸执行一次无动画聚焦。两条路径都会读取标尺占用,并从可见区域的顶部和 左侧扣除标尺尺寸:

const rule = new Rule({ select, rulerSize: 40 });
const workspace = new Workspace({
  width: 1920,
  height: 1080
});

这项组合信息只参与 Workspace 的首次与尺寸变化自动聚焦,不进入场景属性、History 或序列化结果。

这些配置只属于 EditorLayer 中的 Rule,不进入 Scene History 或 FileData。移动、缩放、旋转、选择 清空/重入与 viewport 变化随唯一 Root frame 更新;Rule 不重新遍历 selected elements、查询它们的 geometry 或保存独立 range/resolved-color 状态。 Ctrl/Meta拖拽是additive marquee:pointerup只执行一次visual area query,并且只有visual bounds被框完整 包围的最终selection owner会加入结果;只包住Group内部child不会选中整个Group。普通无修饰marquee保留 真实轮廓相交语义。marquee候选遵守visible/selectable且不受silent取值影响,true/false都可框选; silent仍阻止pointer target。pointermove期间只有ephemeral rectangle,零area query、owner write和History。

Group dive每次只选择当前Group scope内命中的DirectChild。下钻后,若下一次raw content hit仍在当前 单选owner分支内,无位移点击保持该owner,使连续双击可以继续进入更深Group、Text Content或当前已有的 LineTree Point;点击该分支外的内容仍按普通selection owner规则解析,可以重新选中ancestor Group。 这一交接只使用当前selection和raw ancestry,不保存第二份Group scope状态。

Select.arrange()接受公开枚举ArrangeType

import { ArrangeType } from "@fulate/tools";

select.arrange(ArrangeType.LayerUp);
select.arrange(ArrangeType.LayerDown);
select.arrange(ArrangeType.Forward);
select.arrange(ArrangeType.Backward);
select.arrange(ArrangeType.Top);
select.arrange(ArrangeType.Bottom);

Layer默认不参与排列;应用创建内容Layer时必须显式设置elementArrangementEnabled: true。 跨Layer移动按当前Root.layers绘制顺序寻找相邻的已启用Layer,跳过未启用Layer;上移进入目标Layer 的children开头,下移进入末尾,并通过既有reparent Transform writer保持world pose。 ArrangeType.Forward/ArrangeType.Backward不读取Layer顺序,只让各元素直接parent内的selected sibling块移动一步;ArrangeType.Top/ArrangeType.Bottom同样只在直接parent内把selected siblings 稳定移动到children末尾/开头。四种父级内操作均不拆Group或跨parent。一次有效命令只提交一个trusted batch和一个semantic History entry;边界noop为零batch/History。

Select.setProperties() 把同一份 authored 顶层属性补丁应用到当前全部选中元素:

select.setProperties({
  backgroundColor: "#ff0000",
  opacity: 0.8
});

面板需要具体元素字段检查时,可让该对象 satisfies ShapeOptionPatch 或自定义元素自己的 patch 类型, setProperties() 本身保持对第三方 authored 字段开放。

省略字段保持不变,undefined 恢复该字段默认值,null 是属性自身的普通领域值。不做deep merge或 deep patch;命令把同一份完整patch引用直接交给每个selected element。结构化字段按引用作为 whole-value replacement;同一引用是普通noop,调用者需要独立副本时在交付前自行复制。标量相同值 仍是noop。Tools不按元素类型过滤字段, 属性面板负责只展示当前selection可编辑的属性。空selection不结束其他活动交互;非空selection 先完成已经接受的交互,再从 live selection 以一次 trusted batch 提交,最多形成一条 History。 setProperties() 不改变 selection,也不派发 select:end;影响 visual geometry 的属性可在稳定后触发 既有 select:update 通知,纯 paint 属性不会触发该几何事件。只要最终 configured 值有净变化,命令就在 History 安装后排队一次 select:configured-change 通知;标量 noop 不派发。表示 world 有效尺寸或位置的 width/height/x/y 仍使用 Select.resize() 或其他 Transform 命令,不能用普通 authored patch 绕过 父级矩阵反算。

Select.resize() 是属性面板使用的公开尺寸命令:

const bounds = element.transform.getWorldVisualBoundsView();
select.resize({
  width: bounds.width * 0.8,
  height: bounds.height * 0.8,
  anchor: "top-left"
});

widthheight 省略时保持该轴当前的 selection frame 尺寸;对 Text,selection frame 始终读取 authored/layout box,即使 overflow="visible" 且未换行的 glyph 超出该 box;anchor 省略等于 "top-left",支持九个 frame 固定点:top-left/top/top-right/left/center/right/ bottom-left/bottom/bottom-right。这些值是 world logical 单位,不包含 Viewport 缩放; controller 会自动处理父级和多层 Group 变换,并通过同一个 trusted batch 和 History 记录。Group resize 会递归改写可编辑子元素的 authored geometry;Group 自身不再承载这次尺寸变化的 scale。移动/旋转 Group 仍只改 Group pose。对 Rectangle/Text 等 frame-authored 子元素,尺寸变化写入它们自己的 frame width/height;对 VectorPath/Polygon/Line/LineTree,则写入点、控制柄或拓扑。

Select.flip() 是同一 Transform 命令链上的镜像操作:

select.flip("horizontal");
select.flip("vertical");

镜像轴经过当前 selection frame 中心。单选旋转元素沿其 frame 自身水平/垂直轴镜像;多选的 aggregate frame 为 world 轴对齐,因此沿 world 水平/垂直轴镜像。命令遵守 selection 的 resize 轴 策略,使用同一批 selected owner baseline、trusted batch 与 semantic History;Group 不承载这次镜像的 scale,镜像会递归 bake children authored geometry。空选择或被策略禁止的方向是普通 noop。

Keyboard、Clipboard 与应用命令 owner

每个活动 Root 由当前 EditorInteractionController 安装一个 DOM keydown listener。应用若要接管 Ctrl/Meta+CCtrl/Meta+XCtrl/Meta+VDeleteBackspace,只绑定一个命令 owner:

const unbind = select.bindKeyboardCommands({
  copy: () => select.copy(),
  cut: () => {
    select.copy();
    select.delete();
  },
  paste: () => select.paste({ resolveParent, afterInstall }),
  delete: () => select.delete(afterDelete)
});

再次绑定直接替换当前引用;unbind 只会清除仍属于它的绑定。Controller 保留平台修饰键、form/Content 排除、活动 gesture gate 与 preventDefault() 时机;未绑定或 gate 拒绝时不消费元素级按键,未绑定应用 owner 时 Point target 仍由现有 Point owner 处理。Fulate 不提供额外的 单键复制快捷键或独立 cut API;应用的 cut 只在上层按 Copy 后 Delete 编排。

Clipboard v2 的唯一 payload 是 elementsparentKeys 和透明 applicationDataselect.copy(producer) 在不改 Canvas/selection/History 的前提下最多同步调用一次 producer,并把完整 source subtree keys 交给 CopyContextselect.paste(options) 在 clone、parent placement 和新 selection 安装后、Root drain 与 History finish 前调用 afterInstall({ applicationData, keyMap, history })keyMap 是 clone codec 一次生成的 完整旧/新 key 映射。应用 owner 先完成自己的首次 create/delete,再用 history.record() 登记 undo/redo, 不调用首次 change 的 redo()。History 关闭时 hook 的 historyundefined,Canvas 操作仍照常执行。

PasteParentResolver 对每个非 Layer 顶层副本调用一次;Fulate 先按 parentKeys 解析当前 live source parent,未提供 resolver 时只有 source parent 不存在才使用 Root 第一个 Layer。副本自身为 Layer 时直接放入 当前 Root。select.delete(afterInstall) 在真实 Canvas 删除闭包安装后、drain/finish 前交付完整 DeleteContext;Point 删除不调用该 hook。System Clipboard 读失败时使用同一 memory payload,不读取旧格式, 不做版本协商或迁移。

异步 paste 在 Clipboard 读取前捕获当前 controller、selection owner、interaction state 与本次 Promise 引用。Clipboard 返回后,只要 controller 生命周期仍有效,合法文档就正常提交并形成实际 History; 只有这些 selection/interaction/task 引用仍共同证明本次 paste 拥有选择意图时,才把 selection 改为新 元素。等待期间建立的新选择或手势保持当前,旧 paste 不结束它也不派发虚假的 select:end;两个 paste 倒序完成时两次文档提交都保留,较新的 paste 意图拥有最终 selection。

属性面板应同时监听 selection 完成和已选几何更新:

function syncPanel() {
  const element = select.selectedElements[0];
  if (!element) return;
  const { left, top, width, height } =
    element.transform.getWorldVisualBoundsView();
  updatePanel({ x: left, y: top, width, height });
}

const stopSelection = select.addEventListener("select:end", syncPanel);
const stopGeometry = select.addEventListener("select:update", syncPanel);

场景持久化只监听 configured 语义完成事件:

import { serializeScene } from "@fulate/import";

const stopPersistence = select.addEventListener(
  "select:configured-change",
  () => saveToIndexedDB(serializeScene(root))
);

select:configured-change 不携带 payload。一次 Transform/Point/Content/Line、setProperties()、 resize/flip、align、group/ungroup、delete、copy/paste 或 History replay,只要最终 configured 属性或结构确有净变化,就在最终状态和适用的唯一 History entry/stack 安装后排队恰好一次通知;连续手势中的 多个内部 batch 不逐批派发,最终回到起点的净空操作也不派发。setRuntimeOptions()clearRuntimeOptions() 只改变 runtime override,因此不派发。选择、marquee、copy 等不写 configured 状态的操作同样不派发。

select:update 只在 Select 自身存在该事件的直接监听器时登记;没有监听器时 pending completion 和 事件派发均为 0。它在当前 Root 的 geometry、spatial、selection projection、paint、hover 与既有 transformUpdated completion 完成后同步派发,不携带 bounds 副本;监听器直接读取 live selection。 同一显示帧内连续 move/resize/rotate/Point 或外部 selected geometry 变化合并为一次通知。监听器在 completion 中再次写入时,新变化进入下一轮稳定后再通知。Viewport zoom 不改变 world logical bounds, 因此不会单独触发该事件。对同步 resize()/flip() 命令,顺序是 frame稳定及transformUpdated phase → select:update → History finish → 排队 select:configured-change → 排队 select:end; 这些通知在同步命令返回后按同一顺序执行。 getWorldVisualBoundsView() 是当前帧的 borrowed view,不应跨帧保存对象引用。setProperties() 若改变 几何,则同样先稳定并同步派发select:update、再完成 History 与排队select:configured-change,但不派发 select:end;纯paint patch按 batch → History → 排队 select:configured-change 完成。

Transform、Point、setProperties()、align、group/ungroup、delete、copy/paste 与 Text editing 都把最终 typed owner/structure effects 交给同一 trusted batch。setProperties()复用同一输入引用, Core batch不复制。History只在semantic scope完成时保存actual changed fields/placements 的 earliest-before/final-after,以及外部 owner 通过HistoryScope.record()登记的 最小HistoryChangerecord()只登记已经正常执行的change,不调用其undo()/redo()。History 关闭时 不创建 History entry、完整 Properties/tree snapshot、receipt 或结构化副本;仅当直接订阅select:configured-change时,semantic scope用已交付的actual before和最终live引用判断净变化。undo/redo把history-owned typed delta重新交给 同一batch,并在Canvas安装后、drain前调用外部change的undo()/redo();drain后恢复selection并转移 History stack。回放是线性、非重入操作;当前 undo()/redo() 返回前,不得再次调用同一 manager 的 undo()redo()clear()push(),外部 HistoryChange 只恢复自己拥有的数据。只有Canvas configured存在实际变化时才先派发 select:configured-change,再依次排队select:endselect:undo/select:redo;这些通知在 replay 返回后按同一顺序执行。

应用层可以在 Paste 的 Canvas 节点和新 selection 安装后、同一 semantic scope 尚未 finish 时补齐自己的 authored owner:

select.paste({
  afterInstall: ({ applicationData, keyMap, history }) => {
    const created = dataDocument.createDefinitions(applicationData, keyMap);
    if (!history || created.length === 0) return;
    history.record({
      undo: () => dataDocument.removeDefinitions(created),
      redo: () => dataDocument.restoreDefinitions(created)
    });
  }
});

hook运行时Canvas副本和新selection已经安装,但frame drain、History finish与最终事件尚未发生。 history在History关闭时为undefined,调用方因此不创建回放对象。首次Data提交由其owner正常完成, 不能通过调用redo()代替。Canvas与Data最终只形成一条entry;Data-only entry同样可undo/redo,但不会把 HistoryScope.finish()的返回值变成true,也不会单独触发select:configured-change

select:end是selection-producing操作的异步完成通知,不是通用Transform更新事件,也不是持久化 合同:显式select()、点击/框选、selection-producing command与History replay都先结束旧交互、安装 最终selection并完成当前selection projection;command还会先完成History及适用的 select:configured-change。 外部scene removal引起的selection修复复用唯一Root frame的completion phase,在geometry、spatial、 overlay、RBush、paint与hit完成后排队通知。每次显式select()都独立完成并排队通知,包括再次传入同一个数组; 不按数组内容或引用过滤。删除固定为structure/selection → drain → History → select:configured-change → hit refresh → 返回;完成通知只在返回后按顺序执行。Delete 不再派发 专用 Delete 完成事件;应用需要在删除闭包安装后登记外部 owner 时使用 afterInstall。 所有 Select 完成通知都只读取派发时的 live state,不参与、阻止或使已完成的内部操作失败;listener 异常在异步通知任务中自然传播。

公开doGroup()/unGroup()支持活动Transform中的程序化调用:先保留并结束已经接受的Transform,随后 把group/ungroup记录为独立History命令;结构变化后旧pointer continuation失效,下一次真实Transform从 live selection开始。明显不足的selection或非Group选择在drain()前直接noop;有效候选才drain,并在 该可覆写边界返回后重新读取live selection。

Line、LineTree、Polygon与VectorPath普通选择都只投影Transform frame,Point capability、controls、paint 和hit为0;双击当前单选owner后才进入唯一显式Point state。进入后隐藏Transform外框和信息面板,只显示 当前owner/scope轮廓与Point controls。Line、Polygon与VectorPath使用owner scope, LineTree只请求active branch scope。四类owner都从live canonical topology投影稳定anchorId/segmentId; VectorPath的cubic handle只用segmentId + cubic-control-1|cubic-control-2,与anchor/segment共用同一flat projection和gesture。所有Point control使用同一外径:anchor是实心圆,segment midpoint是菱形,cubic handle是由细线连接anchor的空心圆;guide只绘制、不命中。handle可自由移动、不可删除、不是endpoint,也不触发Port probe。Line保持open line-only;Polygon保持closed contour且至少三个anchors;VectorPath保留各subpath的open/closed状态并只 编辑当前line/cubic记录。move、midpoint insert-and-drag、handle move与anchor delete/merge都不持久化 authored array index,未受影响的records与IDs保持引用。负缩放Line拖点时同一owner change保持world pose和 未编辑anchors稳定。

Point control点击后会保持stable Delete target,移开鼠标再按Delete/Backspace仍处理该target;只有可删除 anchor显示active删除反馈。不可删除或已失效target不显示该反馈,Delete是已处理noop,不会误删整个element。 只有没有active/hover target时, LineTree active child branch通过capability产生一个topology change并进入parent scope;root branch以及 owner-scope adopter返回null,随后复用普通Select element delete。dispatch不读取LineTree私有topology。

LineTree按L直接把active branch tail视为已点击并进入draft;同一junction tail可以反复产生sibling branches。Transform状态下当前pointer已在tail时第一次L 直达,否则进入只命中branch tail的source-pick,第二次L或click均可确认;树干segment和中间anchor 不可作为source。keydown只消费Core唯一hover/cursor链已经解析的当前pointer来确认source,下一次真实 pointermove才开始preview。source-pick或未提交branch-draw中双击真实branch会先取消当前session并进入 该branch Point,随后L从active tail正常开始,不保留重叠状态。自由终点已经确认后Escape/Enter会像普通Line drawing一样提交,不会丢弃整段。draft确认前 零document write;确认只安装一个最终topology change。LineToolresultTypetailDecoration 在drawing session开始时读取一次;resultType省略为line,也可创建单branch lineTreetailDecoration省略为arrow,传none时新Line、LineTree root及新branch均不带尾装饰。

LineTree Point/add/remove branch History只保存actual changed point/branch records;不深拷贝完整topology。 source-pick、branch scope切换和取消都不创建History或cleanup batch。 Standalone Line和LineTree都是EndpointRelation的source owner,不进入普通Port target索引;空白按L新建 结果时不会为零frame LineTree投影默认Ports。Root默认关闭Port;只有显式enablePorts: true且元素 自身显式配置ports: true或custom数组时,Ports才进入Root-scoped增量PortIndex和连接picker。未开启时LineTool不启动Port picker, Line和LineTree仍可正常绘制、编辑与选择,Point/Transform继续复用同一个Snap执行普通axis对齐。 按L进入LineTool或开始endpoint连接手势时,picker立即显示当前Viewport内所有已声明且可作为pointer target的Port,不需要鼠标先接近或命中元素。鼠标进入固定10屏幕像素半径后,只把最近且同距时paint order最上层的一项设为active并用于吸附;离开半径只取消active,其他Port继续显示。取消或结束session 才清空全部marker。marker不绘制owner矩形轮廓,也不写authored state或History。

SelectconnectionGesture只有"auto" | false | callback三种值。auto在pointerup把最终relation 合入当前semantic History;callback在交互完全结束后只发布stable source/target refs和新的同步 connect()命令,回调本身不写relation也不创建Connection History;false不探测Port。普通Point的 pointercancel、Escape、selection switch与teardown会消费已经排队的latest accepted intent;endpoint派生的 Port手势才丢弃尚未消费的intent/candidate、停止picker且不连接/回调。两者下一次手势都从当前live authored state重入,已正常安装的Point变化不回滚。 启用Port手势时,Line/LineTree首端只接受output | both,尾端只接受input | both;方向不兼容或 容量已满的候选只显示阻止反馈,不确认Line/branch point,也不提交endpoint移动。离开Port命中范围后 仍可确认普通自由点。Port ownership只来自元素自身的ports;未声明的Text、Button或内部装饰默认 没有Port。PortIndex再按当前Root的pointer-target语义过滤owner;silentpickable只决定指针 资格,不启用Port,也不改写authored配置。

Clipboard copy 先同步生成独立 memory snapshot;系统 clipboard 是不阻塞 authored command 的 best-effort 边界。paste 为 element 生成新 key,保留 owner-local path/Port IDs;source与 target一起复制时只重映射element key,只复制source时把当前resolved source-local位置写入clone fallback并删除外部relation。所有结果通过一个trusted batch安装。Clipboard v2 只保存elementsparentKeys与透明applicationData;memory 与 system 使用同一 payload。非 Layer 副本先解析当前 live source parent;未提供resolveParent时使用该 parent,只有 parent 不存在时才落到 Root 第一个 Layer;提供 resolver 时对每个非 Layer 顶层副本调用一次。v2 reader 不读取旧格式,不做版本协商或迁移。