Files
endorphin-tessera-gui/src/bridge/ui_bridge/page_input.rs
T

333 lines
19 KiB
Rust

//! 把页面、2D 视口和 3D 视口的相机输入转换成托管对象更新。
//! 输入携带具体出现的 Mount;嵌套世界 UI 先做逆投影,避免把屏幕位移直接当成场景坐标。
//!
//! UiBridge::dispatch 按输入 kind 分流到下面三个方法:整页、局部二维视口和局部三维场景分别处理。
//! 它们只在所属 VM worker 上修改托管相机属性或调用 controller.update,不直接改 GUI 已发布帧。
//! 姿态变化随后由正常依赖/快照刷新观察;任何新帧仍需走 layout/paint/commit 的完整发布路径。
use super::*;
impl UiBridge {
/// dispatch 的 scene-pan/orbit/zoom 分支调用,把命中出现位置的输入包装成 CameraGesture 交给控制器。
pub(super) fn dispatch_scene_view(
&self,
vm: &mut Vm,
input: &crate::events::Input,
) -> Result<()> {
// 锁内只读取当前发布版本与弱 Mount 身份,离开锁后才进入 VM 操作,避免持 session 锁调用用户代码。
let (weak, desktop) = {
// 当前窗口已由外层选定;本锁内同时读版本和 Mount,避免把旧几何与新弱身份混用。
let state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
anyhow::ensure!(
state.published_version == input.layout_version,
"obsolete 3D camera view"
);
(
*state
.mounts
.active
.get(&input.mount)
.ok_or_else(|| anyhow::anyhow!("3D viewport unavailable"))?,
state.desktop,
)
};
// upgrade 返回临时拥有根,下方即使任一步失败也通过闭包外的 release 清理。
let mount = vm
.upgrade(weak)?
.ok_or_else(|| anyhow::anyhow!("3D viewport Mount expired"))?;
// 不能直接 ? 返回整个方法,否则错误时跳过 Mount 根释放;用闭包暂存处理结果。
let result = (|| {
// 除 session 发布版本外,再检查目标自身仍挂接、版本匹配且确为 3D 视口,拒绝旧出现操作。
let occurrence = vm.payload::<crate::mount::Mount>(mount, "Mount")?.clone();
anyhow::ensure!(
occurrence.mounted
&& occurrence.version == input.layout_version
&& occurrence.geometry.scene3d_viewport,
"obsolete 3D viewport"
);
// resolve 跟随 Block 的共享替换关系,再读取实际视觉状态,不能只信任旧 payload。
let (_, data) = block::resolve(vm.object_store(), occurrence.block)?;
// visual_data 合并当前可见属性,后面的相机/控制器来自模型而非原生画面的旧快照。
let data = block::visual_data(vm.object_store(), data)?;
// 没有显式相机就无法解释手势,拒绝更新而不是猜一个默认相机改变场景语义。
let camera = data
.camera
.ok_or_else(|| anyhow::anyhow!("missing 3D camera"))?;
// 显式配置 controller 才能接收 3D 手势;没有控制器的场景仍可显示,但不能自动修改相机。
let controller = data
.controller
.ok_or_else(|| anyhow::anyhow!("3D controller disabled"))?;
// 同一视觉对象的边框/内边距由快照解析,手势锚点与可用尺寸都应扣除这部分。
let node = block::snapshot(vm.object_store(), occurrence.block)?;
// inset 将外框换算成视口内容盒,避免控制器把边框尺寸算成场景宽高。
let inset = crate::layout::insets(&node);
// 几何来自具体 Mount,两个窗口中共享同一场景仍可能有不同视图投影。
let geometry = &occurrence.geometry;
// 外层二维视图逆变换用于锚点定位;没有视图变换时使用恒等变换。
let outer = geometry.view.unwrap_or_default();
// delta 的第一分量还会用作缩放倍率;缺失说明输入信封不满足相机事件要求。
let delta = input
.delta
.ok_or_else(|| anyhow::anyhow!("3D camera delta required"))?;
// 缩放使用倍率而非二维位移;旋转/平移通过 logical_delta 处理嵌套二维/面板投影。
// 不可逆映射时忽略本次连续增量,不在无效坐标上继续更新相机。
let local_delta = if input.kind == "scene-zoom" {
// 缩放不产生平移 dx/dy,只把原第一分量写到 gesture.factor。
[0., 0.]
} else {
// 平移/旋转先撤销外层投影;映射失败表示本帧增量无法可靠应用。
let Some(delta) = geometry.logical_delta(input.point, delta) else {
return Ok(());
};
delta
};
// 可用锚点先映回所在视口空间;无法取得时使用内容左上角,传给控制器的是扣除 inset 的局部点。
let point = input
.point
.and_then(|p| geometry.view_point(crate::geometry::Point::new(p[0], p[1]), false))
.map(|p| outer.inverse(p))
.unwrap_or(crate::geometry::Point::new(
geometry.x + inset.left,
geometry.y + inset.top,
));
// 将输入封装为名义 CameraGesture 临时对象;不把原生 Input 或窗口指针直接交给源控制器。
let gesture = vm.new_object()?;
// 临时手势也有独立拥有根;把附类型/写字段/控制器错误暂存,外层始终 release(gesture)。
let result = (|| {
vm.attach_type(
gesture,
desktop_type(vm.object_store(), "CameraGesture", desktop)?,
)?;
vm.set_attr(gesture, "kind", Value::Text(input.kind.clone()))?;
for (name, value) in [
("dx", local_delta[0]),
("dy", local_delta[1]),
// factor 复用原 delta 第一项,仅缩放种类按倍率解释;dx/dy 已经转换为逻辑位移。
("factor", delta[0]),
("width", (geometry.width - inset.horizontal()).max(0.)),
("height", (geometry.height - inset.vertical()).max(0.)),
("pointX", point.x - geometry.x - inset.left),
("pointY", point.y - geometry.y - inset.top),
] {
// 全部数值通过统一语言数值编码写属性,控制器读取的是普通托管字段。
vm.set_attr(gesture, name, crate::native_visual::value(value))?;
}
// 绑定 controller.update 保留接收者语义,再带 camera/gesture 两个显式参数进行有界调用。
let function = vm.bind_attr(controller, "update")?;
// callable 校验在 VM 执行边界继续完成,非对象方法值直接作为契约错误返回。
let result = match function {
Value::Object(function) => vm.call_with_limits(
function,
&[Value::Object(camera), Value::Object(gesture)],
endor_core::ExecutionLimits {
max_frames: 128,
max_instructions: 1_000_000,
},
),
_ => Err(invalid("controller.update is not callable")),
};
// 先保存调用结果再释放绑定方法,错误路径同样清理;成功返回值也不保留为业务状态。
vm.release_value(function)?;
vm.release_value(result?)?;
Ok(())
})();
// 临时手势无论控制器成功或失败均释放,然后返回先前处理结果。
vm.release(gesture)?;
result
})();
// upgrade 得到的拥有根在闭包执行后统一释放,内部任一步失败也不会遗留该临时根。
vm.release(mount)?;
result
}
/// dispatch 的 viewport-pan/zoom 分支调用,逆投影具体 CanvasViewport 输入并改写其 Camera2D 属性。
pub(super) fn dispatch_viewport_view(
&self,
vm: &mut Vm,
input: &crate::events::Input,
) -> Result<()> {
// 二维视口同样先校验发布版本,再从活动表捕获原 Mount,不能因目标消失自动换成另一个视口。
let weak = {
// 锁内只检查发布版本并复制弱身份;下面读取相机和写 VM 属性时不持有会话锁。
let state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
anyhow::ensure!(
state.published_version == input.layout_version,
"obsolete viewport view"
);
*state
.mounts
.active
.get(&input.mount)
.ok_or_else(|| anyhow::anyhow!("viewport is no longer active"))?
};
// 临时根保证输入处理期间 Mount 仍可读取,方法尾部无论成功失败都释放。
let mount = vm
.upgrade(weak)?
.ok_or_else(|| anyhow::anyhow!("viewport Mount expired"))?;
// 闭包集中实际处理,使所有 ? 都先回到统一释放路径。
let result = (|| {
// 弱身份能升级不代表仍挂载/版本有效,先验证具体出现上下文。
let occurrence = vm.payload::<crate::mount::Mount>(mount, "Mount")?;
anyhow::ensure!(
occurrence.mounted
&& occurrence.version == input.layout_version
&& occurrence.geometry.canvas_viewport,
"obsolete viewport occurrence"
);
// 复制几何和 Block 身份,结束 occurrence 对 VM 的借用后才能改写相机。
let geometry = occurrence.geometry.clone();
// Block 只用于找相机/边距,输入定位仍来自 Mount 的几何。
let block = occurrence.block;
// 共享替换后的实际视觉状态才包含当前相机引用。
let (_, data) = block::resolve(vm.object_store(), block)?;
// 托管属性覆盖 payload 缺省值,保持与布局/绘制同一视觉读取约定。
let data = block::visual_data(vm.object_store(), data)?;
// 局部视口必须有 Camera2D,不把整页 Surface 当作相机替代。
let camera = data
.camera
.ok_or_else(|| anyhow::anyhow!("viewport has no camera"))?;
// 读取已校验的相机快照作为本次基准;共享相机用户会通过普通属性变更观察到相同更新。
let old = crate::viewport::camera(vm.object_store(), camera)?;
// 该分量在 zoom 时表示倍率,在 pan 时表示外部逻辑位移,随后分支分别解释。
let delta = input
.delta
.ok_or_else(|| anyhow::anyhow!("viewport input has no delta"))?;
// 基于旧快照计算三个新值,完成验证后才依次回写托管属性。
let (mut x, mut y, mut zoom) = (old.x, old.y, old.zoom);
if input.kind == "viewport-zoom" {
// 缩放必须有锚点,不能让缺失指针导致隐式绕视口原点缩放。
let pointer = input
.point
.ok_or_else(|| anyhow::anyhow!("zoom requires anchor"))?;
// 先用屏幕/视图坐标做外框检查,再逆投影到布局逻辑坐标。
let pointer = crate::geometry::Point::new(pointer[0], pointer[1]);
anyhow::ensure!(
geometry.contains(pointer),
"zoom anchor is outside viewport"
);
// 外框命中不保证投影可逆;None 作为错误返回,不能继续使用原屏幕数值。
let pointer = geometry
.logical_point(pointer)
.ok_or_else(|| anyhow::anyhow!("zoom anchor cannot be projected"))?;
// 当前 Block 快照给出布局边距,用它取得正确内容盒的缩放锚点。
let node = block::snapshot(vm.object_store(), block)?;
// inset 与布局模块共用同一规则,避免相机手势与绘制对内容原点理解不同。
let inset = crate::layout::insets(&node);
// 已验证指针落在出现外框还不够;扣除 inset 后必须落入实际内容盒,禁止用边框作为缩放锚点。
let local = [
pointer.x - geometry.x - inset.left,
pointer.y - geometry.y - inset.top,
];
anyhow::ensure!(
local[0] >= 0.0
&& local[1] >= 0.0
&& local[0] < geometry.width - inset.horizontal()
&& local[1] < geometry.height - inset.vertical(),
"zoom anchor is outside content viewport"
);
// 以实际钳制后的新 zoom 反求 pan,保持锚点下世界位置:old_pan + local/old_zoom = new_pan + local/new_zoom。
zoom = (old.zoom * delta[0]).clamp(0.01, 100.0);
x += local[0] / old.zoom - local[0] / zoom;
y += local[1] / old.zoom - local[1] / zoom;
} else {
// 平移没有锚点保持公式,只需逆投影增量再除相机自身缩放。
let Some(delta) = geometry.logical_delta(input.point, delta) else {
return Ok(());
};
// 屏幕/父层逻辑位移先撤销外层投影,再除本视口 zoom 转为相机世界偏移,不能直接加原像素增量。
x += delta[0] / zoom;
y += delta[1] / zoom;
}
// 把世界偏移限制回模型坐标预算,再走 set_attr 变更追踪,不直接改原生渲染快照。
x = x.clamp(-MAX_DIMENSION as f32, MAX_DIMENSION as f32);
y = y.clamp(-MAX_DIMENSION as f32, MAX_DIMENSION as f32);
vm.set_attr(camera, "x", crate::native_visual::value(x))?;
vm.set_attr(camera, "y", crate::native_visual::value(y))?;
vm.set_attr(camera, "zoom", crate::native_visual::value(zoom))?;
Ok(())
})();
// release 发生在 result? 之前,保证投影/属性写入错误不会泄漏升级得到的 Mount。
vm.release(mount)?;
result
}
/// dispatch 的 page-pan/zoom 分支调用,兼容旧 Page 表面相机;独立 CanvasViewport 不走此方法。
pub(super) fn dispatch_page_view(
&self,
vm: &mut Vm,
input: &crate::events::Input,
) -> Result<()> {
// 兼容 Page 相机不通过 Mount 定位,而是读取当前活动页面及其 Surface 副本;发布版本仍必须一致。
let (page, mut surface, viewport) = {
// 只复制当前已发布页的相机与真实视口,不持锁执行下面的坐标计算或 VM 属性写入。
let state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
anyhow::ensure!(
state.published_version == input.layout_version,
"obsolete page view"
);
(
state
.active
.ok_or_else(|| anyhow::anyhow!("no active Page"))?,
state.surface.clone(),
state.layout.viewport,
)
};
// 外层信封校验之后仍要求本路径必需的增量字段,防止不完整调用静默改变相机。
let delta = input
.delta
.ok_or_else(|| anyhow::anyhow!("page view requires delta"))?;
if input.kind == "page-zoom" {
// 旧 Page 模式只允许 Canvas 缩放,有限文档或 Window 页面保持各自尺寸语义。
anyhow::ensure!(
surface.mode == crate::page::Mode::Canvas,
"zoom requires canvas mode"
);
// 以光标为缩放锚点,先要求它存在,再检查可用视口范围。
let pointer = input
.point
.ok_or_else(|| anyhow::anyhow!("zoom requires anchor"))?;
// 转为页面几何共用的 Point,便于调用 Surface::to_world 做缩放前逆变换。
let pointer = crate::geometry::Point::new(pointer[0], pointer[1]);
anyhow::ensure!(
pointer.x >= 0.0
&& pointer.y >= 0.0
&& viewport.is_none_or(|v| pointer.x < v.width && pointer.y < v.height),
"zoom anchor is outside viewport"
);
// 先保留旧缩放下锚点对应的世界点,再调整 zoom 并解新 pan,避免缩放时内容从光标下漂移。
let world = surface.to_world(pointer);
surface.zoom = (surface.zoom * delta[0]).clamp(0.1, 4.0);
surface.pan = crate::geometry::Point::new(
world.x - pointer.x / surface.zoom,
world.y - pointer.y / surface.zoom,
);
} else {
// 平移按当前 zoom 换成世界距离,不直接把屏幕像素累加进世界 pan。
surface.pan.x += delta[0] / surface.zoom;
surface.pan.y += delta[1] / surface.zoom;
}
// 有限页面按当前内容/视口钳制;Window 会重置导航,Canvas 则保留自由 pan/zoom。
if let Some(viewport) = viewport {
surface.clamp(viewport);
}
// 内容模式 clamp 之外再加通用坐标预算;Canvas 可自由漫游,但也不能无限增长偏移。
let limit = MAX_DIMENSION as f32;
surface.pan.x = surface.pan.x.clamp(-limit, limit);
surface.pan.y = surface.pan.y.clamp(-limit, limit);
// 最终回写活动 Page 的普通托管属性,后续刷新重新提取 Surface;此函数不直接发布原生帧。
vm.set_attr(page, "panX", crate::native_visual::value(surface.pan.x))?;
vm.set_attr(page, "panY", crate::native_visual::value(surface.pan.y))?;
vm.set_attr(page, "zoom", crate::native_visual::value(surface.zoom))?;
Ok(())
}
}