@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。
Select、Snap、Rule 与 LineTool 都使用 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,不保存 currentControl、hoverElement 或第二份 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"
});width 或 height 省略时保持该轴当前的 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+C、Ctrl/Meta+X、Ctrl/Meta+V、Delete 或 Backspace,只绑定一个命令 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 是 elements、parentKeys 和透明 applicationData。select.copy(producer)
在不改 Canvas/selection/History 的前提下最多同步调用一次 producer,并把完整 source subtree keys 交给
CopyContext。select.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 的 history 为 undefined,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()登记的
最小HistoryChange。record()只登记已经正常执行的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:end与select: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。LineTool的resultType与tailDecoration
在drawing session开始时读取一次;resultType省略为line,也可创建单branch lineTree;
tailDecoration省略为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。
Select的connectionGesture只有"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;silent和pickable只决定指针
资格,不启用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 只保存elements、
parentKeys与透明applicationData;memory 与 system 使用同一 payload。非 Layer 副本先解析当前 live
source parent;未提供resolveParent时使用该 parent,只有 parent 不存在时才落到 Root 第一个 Layer;提供
resolver 时对每个非 Layer 顶层副本调用一次。v2 reader 不读取旧格式,不做版本协商或迁移。
