Skip to content

CanvasHandle

CanvasHandle 是宿主操控画布的主要入口。它是稳定对外接口;宿主不要直接依赖内部 CanvasContext

拿到方式:

vue
<HyperCardCanvasDesigner
  v-model="document"
  :component-source-map="sourceMap"
  @handle-ready="handle = $event"
/>

快照和选中

API说明
getSnapshot()返回 PageDocument 深拷贝
selectedIdsreadonly ref,当前选中实例 id
selectedInstancescomputed,当前选中实例对象
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 不再幂等)
isBatchingreadonly ref,当前是否处于 batch
undo() / redo()命令栈
canUndo / canRedocomputed,工具栏按钮状态
undoStackSize / redoStackSizecomputed,调试 / 埋点用
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()返回默认快捷键配置

常见命令:

  • undo
  • redo
  • deleteSelection
  • duplicateSelection
  • clearSelection
  • cancelInteraction
  • lockSelection
  • unlockSelection
  • bringForward
  • sendBackward
  • bringToFront
  • sendToBack
  • zoomIn
  • zoomOut
  • zoom100
  • resetViewport
  • fitToContent
  • fitToSelection
  • fitToSelectionOrContent
  • nudgeSelection
  • nudgeSelectionLarge
  • toggleGrid
  • toggleRuler

命令适合 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(modenull)`
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。命中失败 getInstancenull,不是 undefined

布局

API说明
getLayoutConfig()当前画布 layout 深拷贝
updateCanvasSize(patch)更新画布尺寸配置
`updateCanvasBackground(patchnull)`
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
cannotReparentEventreparent 守门拒绝事件,host 可以 toast
cannotDragLayoutManagedChildEventlayout-managed child 拖动失败事件(flex/grid/split 子项位置由父决定,直接拖会被拒)

多画布场景直接调用 getDropTarget 时,建议传当前 canvas stage/root,避免全局扫描命中其它画布。

cannotReparentEventreason 是 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 事件订阅等能力。