71 lines
7.0 KiB
TypeScript
71 lines
7.0 KiB
TypeScript
/**
|
|
* 节点组件与画布控制器之间的动作上下文。Provider实现文本、名称、样式、几何、分区及编辑模式修改。
|
|
* useScopedNodeEditing为新版渲染回调追加目标身份/归属检查,旧版无Overlay上下文时沿用原接口。
|
|
* 本模块不拥有文档、history或队列;它只把界面动作交给正确的控制器。
|
|
*/
|
|
"use client";
|
|
import {createContext,useContext,useMemo} from 'react';
|
|
import type {XYPosition} from '@xyflow/react';
|
|
import type {NodeDirection} from '@/lib/canvas';
|
|
import type {TextStyle} from '@/lib/canvas-elements';
|
|
import type {NodeTransform} from '@/lib/canvas-geometry';
|
|
import type {EditingLease} from './editing-lease';
|
|
import type {RenderedTarget} from '@/lib/canvas-bindings/compat-types';
|
|
import {CanvasOverlays} from './canvas-overlays';
|
|
/** 默认动作为空实现,便于节点展示独立挂载;可选手势钩子兼容旧版调用与新版EditingLease。
|
|
* rotating/resizing是Provider提供的当前模式标记,workspace用于构造导航,schema区分适配路径。 */
|
|
const defaultEditing = {
|
|
beginTransform: undefined as undefined | ((id: string) => boolean | void | EditingLease<NodeTransform>), finishTransform: undefined as undefined | (() => void), cancelTransform: undefined as undefined | (() => void),
|
|
endEditing: undefined as undefined | ((id: string) => void),
|
|
workspace: "", schema: 1 as 1 | 2,
|
|
// begin/select/edit分别供编辑会话、选择与正文更新使用;branch取得屏幕坐标以定位分支菜单。
|
|
begin: (_id?: string) => {}, select: (_id: string) => {}, edit: (_id: string, _text: string) => {},
|
|
branch: (_id: string, _direction: NodeDirection, _point: XYPosition) => {},
|
|
// 几何、文字样式、标题和分区属性分别走独立动作,避免组件直接合并整份节点数据。
|
|
transform: (_id: string, _transform: NodeTransform) => {},
|
|
style: (_id: string, _style: TextStyle, _fontSize?: number) => {},
|
|
rename: (_id: string, _title: string) => {},
|
|
frame: (_id: string, _patch: { title?: string; labelMode?: import("@/lib/canvas-elements").FrameLabelMode; presentation?: import("@/lib/canvas-v2").FramePresentation }) => {},
|
|
// 传元素id开启相应模式,null请求退出;是否确属当前元素在作用域包装中进一步检查。
|
|
rotating: null as string | null, rotate: (_id: string | null) => {},
|
|
resizing: null as string | null, resize: (_id: string | null) => {},
|
|
};
|
|
/** 这些生命周期钩子可缺省,其余基础动作保留默认接口形状。 */
|
|
type GestureHooks = 'beginTransform' | 'finishTransform' | 'cancelTransform' | 'endEditing';
|
|
/** 由具体画布注入实现;默认上下文不会自行创建一个编辑器实例。 */
|
|
export const NodeEditing = createContext<Omit<typeof defaultEditing, GestureHooks> & Partial<Pick<typeof defaultEditing, GestureHooks>>>(defaultEditing);
|
|
|
|
/** Bind legacy renderer actions to the lifetime and placement that rendered them. */
|
|
/** 捕获本次渲染目标的incarnation、ownerCanvasId与视图canvasId,再为每次动作读取最新状态。
|
|
* 捕获值代表界面当时展示的对象,最新值用来拒绝过期回调;不能只凭相同元素id就认为仍是同一对象。 */
|
|
export function useScopedNodeEditing(id:string,rendered?:RenderedTarget){
|
|
// 只有rendered.id匹配当前节点才采用incarnation;有新版scope却缺少合法rendered身份时拒绝编辑。
|
|
const editing=useContext(NodeEditing),scope=useContext(CanvasOverlays),incarnation=rendered?.id===id?rendered.incarnation:undefined,owner=rendered?.ownerCanvasId,canvas=rendered?.canvasId;
|
|
// 无新版scope直接返回旧接口;否则只在接口、视图作用域或目标身份变化时重建包装。
|
|
return useMemo(()=>{if(!scope)return editing;
|
|
// 动作当刻检查engine未关闭、视图仍存在且仍在原canvas、元素生命周期与owner未变。
|
|
// candidate必须是本节点(或允许的空参数),阻止一个节点的包装替其它目标发起操作。
|
|
const current=(candidate:string|null|undefined=id)=>{const durability=scope.engine.getDurability(),view=scope.engine.sessions.getSnapshot().views.get(scope.index.viewId),record=scope.engine.getSnapshot().document.elements.get(id);return !durability.closing&&!durability.closed&&!!view&&view.canvasId===canvas&&incarnation!==undefined&&record?.incarnation===incarnation&&record.ownerCanvasId===owner&&(candidate===undefined||candidate===null||candidate===id);};
|
|
// finish/cancel没有id参数,额外确认当前手势属于本视图,且目标列表含本id/incarnation。
|
|
const ownGesture=()=>scope.engine.sessions.getSnapshot().gesture?.viewId===scope.index.viewId&&scope.engine.sessions.getSnapshot().gesture?.targets.some(target=>target.id===id&&target.incarnation===incarnation);
|
|
// 拷贝展示信息,仅替换动作入口;文本/样式/几何等动作逐次检查,不缓存“曾经有效”的结论。
|
|
const scoped:typeof editing={...editing,
|
|
begin:candidate=>{if(current(candidate))editing.begin(candidate??id);},select:candidate=>{if(current(candidate))editing.select(candidate);},
|
|
edit:(candidate,text)=>{if(current(candidate))editing.edit(candidate,text);},rename:(candidate,title)=>{if(current(candidate))editing.rename(candidate,title);},
|
|
style:(candidate,style,size)=>{if(current(candidate))editing.style(candidate,style,size);},frame:(candidate,patch)=>{if(current(candidate))editing.frame(candidate,patch);},
|
|
transform:(candidate,change)=>{if(current(candidate))editing.transform(candidate,change);},branch:(candidate,direction,point)=>{if(current(candidate))editing.branch(candidate,direction,point);},
|
|
// 退出模式用null表示,但只准退出本节点当前拥有的rotate/resize模式,避免关闭其它节点的模式。
|
|
rotate:candidate=>{if(current(candidate)&&(candidate!==null||scope.engine.sessions.getSnapshot().views.get(scope.index.viewId)?.modes.get(id)==='rotate'))editing.rotate(candidate);},resize:candidate=>{if(current(candidate)&&(candidate!==null||scope.engine.sessions.getSnapshot().views.get(scope.index.viewId)?.modes.get(id)==='resize'))editing.resize(candidate);},
|
|
};
|
|
// 保持可选钩子的存在性:失效时beginTransform返回false,其余动作静默忽略。
|
|
// begin取得的租约由创建者管理;这里不把旧事件转给当前另一个手势。
|
|
if(editing.beginTransform)scoped.beginTransform=candidate=>current(candidate)?editing.beginTransform!(candidate):false;
|
|
if(editing.endEditing)scoped.endEditing=candidate=>{if(current(candidate))editing.endEditing!(candidate);};
|
|
// 结束手势要同时通过对象有效性与手势归属检查,防止迟到的pointerup结束新交互。
|
|
if(editing.finishTransform)scoped.finishTransform=()=>{if(current()&&ownGesture())editing.finishTransform!();};
|
|
if(editing.cancelTransform)scoped.cancelTransform=()=>{if(current()&&ownGesture())editing.cancelTransform!();};
|
|
// 包装只负责入口检查;事务合法性、持久化和失败报告仍由Provider对应的动作实现负责。
|
|
return scoped;
|
|
},[editing,scope,id,incarnation,owner,canvas]);
|
|
}
|