CanvasHandle
CanvasHandle 是宿主操控画布的主要入口。它是稳定对外接口;宿主不要直接依赖内部 CanvasContext。
拿到方式:
vue
<HyperCardCanvasDesigner
v-model="document"
:component-source-map="sourceMap"
@handle-ready="handle = $event"
/>快照和选中
| API | 说明 |
|---|---|
getSnapshot() | 返回 PageDocument 深拷贝 |
selectedIds | readonly ref,当前选中实例 id |
selectedInstances | computed,当前选中实例对象 |
getInstanceAtClientPoint(point) | 右键菜单 / hover 命中实例 |
duplicateInstance(id, options?) | 复制实例并返回新 id |
属性面板通常监听 selectedInstances:
ts
watch(
() => handle.value?.selectedInstances.value,
(instances) => {
primary.value = instances?.[0] ?? null
},
)文档修改
| API | 说明 |
|---|---|
dispatch(action) | 文档修改唯一入口 |
beginBatch() / endBatch(type?) | 把一组修改合成一条 undo;0.6.0 起深度计数:begin/end 成对配平、可嵌套,只有最外层 endBatch 才 flush(重复 beginBatch 不再幂等) |
isBatching | readonly ref,当前是否处于 batch |
undo() / redo() | 命令栈 |
canUndo / canRedo | computed,工具栏按钮状态 |
undoStackSize / redoStackSize | computed,调试 / 埋点用 |
clearHistory() | 清空历史 |
getRemoveImpact(id) | dispatch removeInstance 之前预检:返回会被一并删掉的子树 { instanceIds, descendantCount },宿主据此弹确认 |
常见 action:
ts
handle.dispatch({
type: 'updateInstance',
payload: {
instanceId,
patch: { props: nextProps },
},
})批量修改:
ts
handle.beginBatch()
try {
for (const id of ids) {
handle.dispatch({ type: 'removeInstance', payload: { instanceId: id } })
}
} finally {
handle.endBatch('delete-many')
}删除与级联
删除实例走 dispatch({ type: 'removeInstance', payload: { instanceId } }),没有单独的 deleteInstance 方法。
- 默认级联(0.6.0 起):删容器一并删整棵子树,并清掉只引用被删实例的 binding;
undo完整恢复子树 + binding。 - 退回旧口径:
createCanvasContext({ cascadeRemove: false })或<HyperCardCanvasDesigner :cascade-remove="false">→ 只删本实例,子实例留成missing-parent孤儿(不是升级到 root)。 - 删前预检:
getRemoveImpact(id)纯读返回{ instanceIds, descendantCount }(根在首位),descendantCount > 0时宿主自行弹确认 /「已删 N 个 · 可撤销」提示。
ts
const impact = handle.getRemoveImpact(id)
if (impact.descendantCount > 0 && !confirm(`将一并删除 ${impact.descendantCount} 个子组件(可 ⌘Z 撤销)`)) return
handle.dispatch({ type: 'removeInstance', payload: { instanceId: id } })命令系统
| API | 说明 |
|---|---|
executeCommand(id, payload?) | 菜单、快捷键、AI 命令统一入口 |
canExecuteCommand(id) | 判断命令当前能不能执行 |
executeShortcut(event) | 跑默认快捷键表 |
getDefaultShortcuts() | 返回默认快捷键配置 |
常见命令:
undoredodeleteSelectionduplicateSelectionclearSelectioncancelInteractionlockSelectionunlockSelectionbringForwardsendBackwardbringToFrontsendToBackzoomInzoomOutzoom100resetViewportfitToContentfitToSelectionfitToSelectionOrContentnudgeSelectionnudgeSelectionLargetoggleGridtoggleRuler
命令适合 toolbar、右键菜单和快捷键共用。宿主业务命令可以自己分发,不要假装成 SDK 内置命令。
视口
| API | 说明 |
|---|---|
viewport | 底层 ViewportStore 逃生口 |
clientToCanvasPoint(point) | DOM client 坐标转 canvas 坐标 |
getViewportState() | 获取当前 scale / pan |
getVisibleBounds() | 当前可见区域 |
getCanvasBounds() | 画布物理边界 |
getContentBounds() | 全部实例外包围盒 |
getSelectionBounds() | 当前选区外包围盒 |
getInstanceBounds(id) | 单实例 bounds |
panBy(dx, dy) / panTo(x, y) | 平移视口 |
zoomTo(scale) / zoomBy(delta) | 缩放 |
zoomToPoint(scale, clientPoint) | 围绕鼠标点缩放 |
fitToContent() / fitToSelection() | 自动适配内容或选区 |
scrollToRect(rect, options?) | 滚动并缩放到指定 canvas rect |
resetViewport() | 回到 100% + 原点 |
视口是瞬时态,不进 undo。
工具模式和快捷键
| API | 说明 |
|---|---|
toolMode | 当前生效工具模式 |
getToolMode() | 同步读取工具模式 |
setToolMode(mode) | 设置永久工具模式 |
| `setTemporaryToolMode(mode | null)` |
getDefaultShortcuts() | 默认快捷键表 |
executeShortcut(event) | 跑默认快捷键表并执行命令 |
SDK 不自动绑定 window:
ts
window.addEventListener('keydown', (event) => {
handle.executeShortcut(event)
})实例树
| API | 说明 |
|---|---|
getInstanceTree() | 树面板 |
getInstanceList() | 平铺列表 |
getInstance(id) | 按 id 查单实例,命中返深拷贝 / 不命中 null |
getSelectedPath() | 面包屑 |
getBreadcrumb(id) | 任意实例路径 |
focusInstance(id) | 选中并滚动到实例 |
scrollToInstance(id) | 只滚动不选中 |
getInstanceTree() 会按 parent/slot 派生嵌套树;getInstanceList() 保留文档平铺顺序。
getInstance(id) / getInstanceList() / getBreadcrumb(id) 都是 深拷贝口径:宿主修改返回对象的嵌套字段(props / rect / layoutBox)不污染 SDK 内部 reactive。命中失败 getInstance 返 null,不是 undefined。
布局
| API | 说明 |
|---|---|
getLayoutConfig() | 当前画布 layout 深拷贝 |
updateCanvasSize(patch) | 更新画布尺寸配置 |
| `updateCanvasBackground(patch | null)` |
updateBehavior(patch) | 持久化行为开关 |
setInteractionOptions(patch) | 临时行为开关,不进文档 |
getEffectiveBehavior() | mode + layout.behavior + 临时开关合并结果 |
getLayoutBox(id) / updateLayoutBox(id, patch) | 实例外层盒子 |
getLayoutItem(id) / updateLayoutItem(id, patch) | 子项布局参数 |
getContainerLayout(id) / updateContainerLayout(id, layout) | 容器内部布局 |
getRotation(id) / updateRotation(id, value) | 旋转角 |
getInstanceOutletRect(parentId, slot?) | 拿 slot outlet 的 DOM rect |
setRootLayout(modeOrCfg | null) | 改 root 实例的容器布局规则(走 undo 栈) |
新代码优先使用 layoutBox / layoutItem:
ts
handle.updateLayoutBox(instanceId, {
widthMode: 'percent',
width: 100,
heightMode: 'auto',
})getInstanceSize() / updateInstanceSize() 仍是 public,但只作为兼容层。
setRootLayout 用于切换顶层布局(从自由摆位切到 flex / grid / split,或反过来),它走 dispatch 路径,可 undo/redo。字符串便捷形式自动展开为完整 ContainerLayoutConfig:
ts
// 字符串便捷:展开成 { mode: 'flex', direction: 'horizontal', ... }
handle.setRootLayout('flex')
// 完整 cfg
handle.setRootLayout({ mode: 'grid', columns: 3, rows: 2 })
// null 清字段,回到默认 { mode: 'free' }
handle.setRootLayout(null)辅助线和网格
| API | 说明 |
|---|---|
setRulerVisible(visible) | 显隐标尺 |
setGridVisible(visible) | 显隐网格 |
updateRulerConfig(patch) | 更新标尺配置 |
updateGridConfig(patch) | 更新网格配置 |
addGuide(guide) | 添加手动辅助线并返回 id |
removeGuide(id) | 删除辅助线 |
updateGuide(id, patch) | 更新辅助线 |
getGuides() | 获取辅助线深拷贝 |
clearGuides() | 清空辅助线 |
getMouseCanvasPoint() | 当前鼠标 canvas 坐标 |
这些配置写入 PageDocument.layout.guides,会进入 undo 栈。
跨容器移动
| API | 说明 |
|---|---|
getDropTarget(point, root?) | client 坐标命中容器 outlet |
canReparent(instanceId, target) | dry-run,看能不能放 |
moveInstanceInTree(instanceId, target) | 真正移动,可 undo |
cannotReparentEvent | reparent 守门拒绝事件,host 可以 toast |
cannotDragLayoutManagedChildEvent | layout-managed child 拖动失败事件(flex/grid/split 子项位置由父决定,直接拖会被拒) |
多画布场景直接调用 getDropTarget 时,建议传当前 canvas stage/root,避免全局扫描命中其它画布。
cannotReparentEvent 的 reason 是 union:'self-parent' / 'parent-not-exists' / 'circular' / 'kind-mismatch' / 'slot-single-occupied' / 'max-children-exceeded' / ...。
'max-children-exceeded' 对应 SlotDecl.maxChildren:
ts
// 组件契约里声明 slot 上限
const contract: ComponentContract = {
slotsDecl: [
{ key: 'children', multiple: true, maxChildren: 3 },
],
// ...
}canReparent dry-run 时返 { ok: false, reason: 'max-children-exceeded' },真拖时 cannotReparentEvent 同款触发。maxChildren 优先级低于 multiple: false(单 slot 排他先拒)。
cannotDragLayoutManagedChildEvent 是 devtools 自动接通的事件。在 flex / grid / split 容器里,子项位置由父布局算,直接 drag 会被 SDK 拒。host 可以监听做 toast 提示,devtools collector 已自动 watch 进 recentDragFailures(见 devtools API)。
校验和覆盖
| API | 说明 |
|---|---|
getLayoutIssues() | 当前布局校验问题 |
getStaleComponentOverrides() | 页面级实例覆盖中已失效的项 |
stale override 表示 override.baseVersionKey !== instance.componentVersionKey;Renderer 不会消费它。
资源
ts
const url = await handle.resolveAsset(assetRef)如果宿主没有传 adapter,SDK 会直接使用 ref.url。如果传了 assets.resolve(ref),SDK 优先调用 adapter 并缓存结果。
进一步阅读
CanvasHandle 还提供 0.3.0 mount-ready 信号(getInstanceReady / getInstanceRuntime / getCachedBySourceId / HandleDisposedError)、dispose 终态守门、binding 事件订阅等能力。