Files
endorphin-tessera-gui/src/render/source_paint.rs
T

622 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 执行源 paint 覆盖并解码受检查的 PaintCommand。
//! 所有绘制回调位于只读域;临时返回对象可分配,但已有模型不可写。命令数量、类型、坐标和颜色在合入帧前检查。
//! 源命令与原生命令按 Mount 绘制顺序合并,随后装配世界 UI 面板。挂起绘制同样需要显式恢复或取消。
//!
//! Source paint dispatch produces checked immutable commands. One read-only VM
//! resumable call sequences all overrides, sharing one instruction/frame budget.
use crate::{
document::{Frame, RenderCommand},
layout::{self, Size},
mount::Mount,
object_list,
ui_bridge::{MAX_DIMENSION, UiNode, attach_desktop_type},
};
use anyhow::{Result, bail};
use endor_core::{
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectRef, ObjectStore,
Value, Vm, VmError, WeakObjectRef,
};
use std::{
collections::{BTreeMap, HashMap},
sync::{Arc, Mutex},
};
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
// 字段解码、名义类型检查和原生驱动适配器共用此构造,错误回到只读 paint 调用链。
fn invalid(message: &'static str) -> VmError {
VmError::InvalidArgument(message)
}
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
// named_type、decode 和 Collect 提取对象字段时调用;不把标量转换成临时对象来接受错误输入。
fn object(value: Value) -> Result<ObjectRef, VmError> {
// 只有真实托管身份可交给 ObjectStore 查询;具体名义类型由使用这个身份的后续步骤检查。
if let Value::Object(object) = value {
Ok(object)
} else {
Err(invalid("paint must return managed objects"))
}
}
/// 读取指定命令字段并要求其值为 Text;不隐式字符串化其他类型。
// decode 读取 kind/color/text 时调用;属性不存在的错误由 get_attr 原样传播。
fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<String, VmError> {
if let Value::Text(value) = store.get_attr(object, name)? {
Ok(value)
} else {
Err(invalid("paint field must be text"))
}
}
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
// decode 的几何、字号和换行尺寸都经此入口;name 同时选择坐标可为负的规则。
fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmError> {
// 先要求 VM Number,再转换成原生 f32;不接受仅在文本上可解析的字符串。
let Value::Number(value) = store.get_attr(object, name)? else {
return Err(invalid("paint field must be a Number"));
};
// x/y 允许负数以表达相对原点的绘制;其余长度必须非负。所有数值都必须有限且在尺寸预算内。
let value = value
.to_string()
.parse::<f32>()
.map_err(|_| invalid("invalid paint number"))?;
if !value.is_finite()
|| value.abs() > MAX_DIMENSION as f32
|| (!matches!(name, "x" | "y") && value < 0.0)
{
return Err(invalid("paint number exceeds bounds"));
}
Ok(value)
}
/// 从当前 desktop 的类型注册表取得绘制结果必须符合的托管类型。
// Collect 和 decode 使用当前桌面的类型注册表,避免用其他窗口的名义类型接受返回值。
fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<ObjectRef, VmError> {
// desktop.types 本身必须为托管注册表对象;缺失类型不能静默采用结构匹配。
let registry = object(store.get_attr(desktop, "types")?)?;
object(
store
.dict_get(registry, &Value::Text(name.into()))?
.ok_or(invalid("missing paint type"))?,
)
}
/// 把源方法返回的托管绘制值解码成受限 Rust 命令,检查名义类型、字段和绘制预算。
// Collect 对返回列表逐项调用;成功值已脱离 VM 对象字段,只有图片/场景等不可变资源引用被保留。
fn decode(
store: &ObjectStore,
command: ObjectRef,
expected: ObjectRef,
) -> Result<RenderCommand, VmError> {
// 使用当前 desktop 注册的 PaintCommand 名义类型;字段形状相似的普通对象也不能冒充命令。
if !store.is_instance(command, expected)? {
return Err(invalid("paint commands must be PaintCommand instances"));
}
// 所有变体先读取本地绘制原点;不会自动加 Mount 原点,源方法已收到布局 Bounds。
let x = number(store, command, "x")?;
let y = number(store, command, "y")?;
// kind 选择受支持的封闭命令集合,未知文本在 match 末尾返回错误。
let kind = text(store, command, "kind")?;
// 场景和图片命令持有独立资源,无需读取通用颜色字段。
let color = if matches!(kind.as_str(), "scene3d" | "image") {
String::new()
} else {
text(store, command, "color")?
};
// 逐变体读取必需字段,随后再统一 validate;不能只凭 PaintCommand 名义类型跳过字段检查。
let result = match kind.as_str() {
// 图像共享 graphics 模块的资源、尺寸、旋转和透明度解码规则,不在这里重复实现。
"image" => crate::bridge::graphics::decode_image(store, command)?,
"scene3d" => {
// 这里仅接受宿主生成的 PaintView payload,克隆原生 Arc,不把活动的 VM 场景图传到绘制线程。
let snapshot = object(store.get_attr(command, "scene")?)?;
RenderCommand::Scene3D {
x,
y,
width: number(store, command, "width")?,
height: number(store, command, "height")?,
view: store
.payload::<crate::scene3d::PaintView>(snapshot, "immutable 3D paint snapshot")?
.0
.clone(),
}
}
"roundedRect" | "strokeRoundedRect" => {
// 圆角填充与描边共享宽高半径;只有描边分支另外要求 strokeWidth。
let width = number(store, command, "width")?;
let height = number(store, command, "height")?;
let radius = number(store, command, "radius")?;
// 单项数值合法还不足以保证右/下边界合法;额外检查原点与尺寸相加后的范围。
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
return Err(invalid("paint rectangle exceeds bounds"));
}
// 同一组几何字段决定两个不同命令变体;填充不能因为缺少 strokeWidth 而失败。
if kind == "roundedRect" {
RenderCommand::RoundedRect {
x,
y,
width,
height,
radius,
color,
}
} else {
RenderCommand::StrokeRoundedRect {
x,
y,
width,
height,
radius,
color,
stroke_width: number(store, command, "strokeWidth")?,
}
}
}
"text" => {
let size = number(store, command, "size")?;
// size 是离散文字等级而非任意像素字号,只接受 1 到 8 的整数。
if size.fract() != 0.0 || !(1.0..=8.0).contains(&size) {
return Err(invalid("paint text size must be an integer from 1 to 8"));
}
// 两个换行边界必须同时出现或同时缺失;只缺一个时传播属性错误,防止半定义文本框。
let wrap_bounds = match (
store.get_attr(command, "wrapWidth"),
store.get_attr(command, "wrapHeight"),
) {
// 两个字段都缺失代表没有显式换行边界的文本;单个缺失或其他属性错误不属于该情况。
(Err(VmError::AttributeNotFound(_)), Err(VmError::AttributeNotFound(_))) => None,
// 两个字段存在后重新按数值规则读取,拒绝负长度或非 Number,而非只判断存在性。
(Ok(_), Ok(_)) => Some([
number(store, command, "wrapWidth")?,
number(store, command, "wrapHeight")?,
]),
(Err(error), _) | (_, Err(error)) => return Err(error),
};
RenderCommand::Text {
x,
y,
value: text(store, command, "text")?,
size: size as u32,
color,
wrap_bounds,
}
}
_ => return Err(invalid("unsupported paint command kind")),
};
// 变体字段解码后仍走统一命令校验,覆盖颜色格式、文本以及组合几何合法性。
result
.validate()
.map_err(|_| invalid("invalid paint command geometry, text or color"))?;
Ok(result)
}
#[derive(Debug, Default)]
/// 按出现 ID 收集已检查的源绘制覆盖,随后与原生默认命令合并。
struct PaintOutput {
// 按出现 ID 收集覆盖;同一个业务 Block 在多个 Mount 中可产生不同绘制结果。
commands: BTreeMap<u64, Vec<RenderCommand>>,
// 全轮源命令累计数,不能靠拆分多个源 DisplayList 绕过 10000 条总预算。
count: usize,
}
// Prepare、Advance、Collect 和 Work 共享纯原生输出;锁只保护短时记账,不跨源方法调用。
type Output = Arc<Mutex<PaintOutput>>;
#[derive(Debug)]
/// 检查源 paint 返回列表并将命令保存到当前出现的绘制结果。
struct Collect {
// 当前驱动所属桌面,用它查 DisplayList/PaintCommand 类型,并由原生函数 roots 保持存活。
desktop: ObjectRef,
// 当前出现 ID 是输出键,不是 Block 身份;一个 Block 多次出现允许返回不同画面。
mount: u64,
output: Output,
}
impl NativeCallable for Collect {
/// 接收当前出现位置 paint 的唯一 DisplayList 结果。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
/// 验证返回名义类型并完整解码所有 PaintCommand,检查全轮命令预算后才把该出现位置结果写入输出表。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
// collect_and_continue 把源 paint 的唯一返回值传给收集器;参数数量或外层类型错误都停止本轮。
let [Value::Object(result)] = args else {
return Err(invalid("paint must return a DisplayList"));
};
// 外层必须属于本桌面 DisplayList;普通含 commands 字段的对象不等同于名义绘制列表。
if !cx.store().is_instance(
*result,
named_type(cx.store(), self.desktop, "DisplayList")?,
)? {
return Err(invalid("paint must return a DisplayList"));
}
// 先验证外层 DisplayList,再取其受控 ObjectList;所有成员解码成功后才写入 output。
let commands = object(cx.store().get_attr(*result, "commands")?)?;
let expected = named_type(cx.store(), self.desktop, "PaintCommand")?;
// 先取得受控列表中的对象身份,再逐项解码并 collect;任何单项失败都不会提交当前 Mount 的部分输出。
let commands = object_list::items(cx.store(), commands)?
.iter()
.map(|command| decode(cx.store(), *command, expected))
.collect::<Result<Vec<_>, _>>()?;
// 仅收集结果时持锁,不在调用源 paint 期间持有结果锁;失败不会插入半个命令列表。
let mut output = self
.output
.lock()
.map_err(|_| invalid("paint output poisoned"))?;
// 累计的是所有源覆盖的命令数,不能按 Mount 分拆规避一万条上限。
if output.count + commands.len() > 10_000 {
return Err(invalid("paint command budget exceeded"));
}
// 预算通过后才更新计数并保存完整列表;返回 Nil 只是辅助调用链结果,不是绘制命令。
output.count += commands.len();
output.commands.insert(self.mount, commands);
Ok(NativeResult::Return(Value::Nil))
}
}
#[derive(Debug)]
/// 为当前出现准备 paint 方法、几何和类型检查上下文。
struct Prepare {
desktop: ObjectRef,
// 按帧绘制顺序保存出现 ID 和弱身份;驱动不会因等待绘制而强持有全部 Mount。
mounts: Vec<(u64, WeakObjectRef)>,
output: Output,
}
impl NativeCallable for Prepare {
/// 零参数驱动入口,待绘制 Mount 和输出容器已保存在适配器。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 建立共享游标与固定外层字节码循环,让 VM 每轮只准备一个出现位置的 paint 调用。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// 构造一个共享游标驱动;每轮只准备当前出现,避免一次保留所有出现的临时实参。
let advance = cx.allocate(ContextNativeFunction::new(
"next paint occurrence",
Advance {
desktop: self.desktop,
mounts: self.mounts.clone().into(),
next: Mutex::new(0),
output: self.output.clone(),
},
vec![self.desktop],
))?;
// 固定 VM 字节码循环反复调用 advance,true 继续、false 结束;所有源方法共享同一次执行预算。
let function = crate::vm_sequence::repeat_call(cx, advance, "source paint", self.desktop)?;
Ok(NativeResult::Invoke {
function,
arguments: vec![],
})
}
}
#[derive(Debug)]
/// 安排下一个出现的绘制调用,保持原始绘制顺序和共享 VM 预算。
struct Advance {
desktop: ObjectRef,
// Prepare 移交的固定顺序列表,在 VM 切片之间保持不变;游标只向前推进。
mounts: Arc<[(u64, WeakObjectRef)]>,
// 下一个待准备出现的下标;不是已输出命令数,跳过原生 paint 的出现也占一项。
next: Mutex<usize>,
output: Output,
}
impl NativeCallable for Advance {
/// 由外层循环无参调用,下一出现位置由内部游标决定。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 短锁内取并推进游标,随后验证/准备当前 Mount 的方法与几何;用临时调用链收集结果,完成后才进入下一位置。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// 锁只保护取游标与递增;离开此块后释放锁,再做 VM 分配和源调用。
let occurrence = {
let mut next = self
.next
.lock()
.map_err(|_| invalid("paint cursor poisoned"))?;
// 复制当前项后只在存在该项时递增,列表结束后保持游标不再增长。
let occurrence = self.mounts.get(*next).copied();
*next += usize::from(occurrence.is_some());
occurrence
};
// false 是 repeat_call 的停止信号;有出现时即使跳过原生方法也返回 true 继续遍历。
let Some((id, weak)) = occurrence else {
return Ok(NativeResult::Return(Value::Bool(false)));
};
// 进入本次出现时再升级弱引用;失效视为本帧上下文错误,不跳过后继续发布不完整画面。
let mount = cx
.upgrade(weak)?
.ok_or(invalid("paint Mount has expired"))?;
// 克隆原生 Mount 描述后再分配临时 Bounds,避免持有跨 VM 可变操作的 store 借用。
let data = cx.store().payload::<Mount>(mount, "Mount")?.clone();
// 身份仍存活不代表仍挂载;已经脱离出现树的 Mount 不能继续绘制。
if !data.mounted {
return Err(invalid("paint Mount is detached"));
}
// 从当前业务 Block 的类型解析 paint,支持源程序覆盖方法;不把旧布局快照当成方法定义缓存。
let definition = cx
.store()
.type_of(data.block)?
.ok_or(invalid("untyped paint target"))?;
// lookup_method 沿类型查找方法;缺失时明确报错,原生默认方法由下方 is_intrinsic 分支识别。
let function = cx
.store()
.lookup_method(definition, "paint")?
.ok_or(invalid("missing paint method"))?;
// 原生默认 paint 留给 finish 生成;仅执行实际覆盖的源方法,避免重复绘制同一出现。
if crate::native_visual::is_intrinsic(cx.store(), function, "paint", self.desktop)? {
return Ok(NativeResult::Return(Value::Bool(true)));
}
// 把已提交 Mount 几何封装为本轮临时 Bounds;新分配参数可初始化,业务模型仍处于只读域。
let bounds = cx.new_object()?;
attach_desktop_type(cx, bounds, "Bounds", self.desktop)?;
// 按布局绝对坐标填充 x/y/width/height;这些临时属性可初始化,不表示源方法能修改已有业务模型。
for (name, value) in [
("x", data.geometry.x),
("y", data.geometry.y),
("width", data.geometry.width),
("height", data.geometry.height),
] {
cx.set_attr(bounds, name, Value::Number((value as f64).into()))?;
}
// 收集器只捕获桌面根和当前出现 ID;输出是原生共享容器,不增加业务图的强引用。
let collector = cx.allocate(ContextNativeFunction::new(
"collect source paint",
Collect {
desktop: self.desktop,
mount: id,
output: self.output.clone(),
},
vec![self.desktop],
))?;
// 辅助帧先执行 paint(block, mount, bounds),收集其结果,再返回 true 推进外层游标。
let function = crate::vm_sequence::collect_and_continue(
cx,
collector,
function,
vec![
Value::Object(data.block),
Value::Object(mount),
Value::Object(bounds),
],
"paint occurrence",
self.desktop,
)?;
// 交回 VM 执行这个零参数辅助函数,源方法及收集器仍由 VM 调用栈驱动,不在 Rust 中递归解释。
Ok(NativeResult::Invoke {
function,
arguments: vec![],
})
}
}
/// 源绘制完成后的未发布结果:已装配帧、业务读取依赖和调度切片数。
pub struct Draft {
// 已经合并命令的原生帧;Draft 仍需桥接层提交,完成计算不等于发布成功。
pub frame: Frame,
// 保留源绘制的业务读取依赖,供下次写入后判定是否必须重绘。
pub reads: Vec<endor_core::ReadDependency>,
// 包括 start 的第一片和每次 resume;供桥接层记录实际绘制切片数。
pub slices: usize,
}
#[must_use = "pending paint must be resumed or cancelled on its owning VM"]
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
pub enum Progress {
// 运行时暂存此句柄,后续在原 VM 继续或取消,不能当作可提交帧。
Pending(Box<Work>),
// 只读调用结束且命令装配完成;UI bridge 仍需更新依赖,endor_host 再发布文档。
Complete(Box<Draft>),
}
/// Only unpublished native commands and an owning prepare root cross a pause.
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
pub struct Work {
// 所属窗口的桌面根,用于完成时过滤宿主记账读取;prepare 的 VM roots 维持其生命周期。
desktop: ObjectRef,
// 跨切片保留的入口根;完成或取消都必须在所属 VM 中显式释放。
prepare: ObjectRef,
// 先前切片已收集的不可变原生命令;恢复续体时延续同一输出,不从头重跑已完成覆盖。
output: Output,
// 布局阶段的不可变视觉根,finish 通过它补齐未被源覆盖的原生绘制。
root: Option<Arc<UiNode>>,
// 尚未发布的布局帧,装配命令只修改这个工作副本。
frame: Frame,
slices: usize,
}
impl Work {
/// 继续当前绘制续体,保留其先前收集的命令和所属窗口上下文。
// ui_bridge::resume_paint_frame 传入原 VM 和切片预算,endor_host 保存返回的新 Pending 或处理 Complete。
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
// 恢复保留原执行预算,slice 只决定本次让出的指令配额,不能重置整个绘制上限。
self.slices += 1;
// 调用 VM 恢复同一个只读续体,结果统一交 accept 处理,避免开始/恢复使用不同收尾路径。
let progress = vm.resume_read_only_call(slice);
self.accept(vm, progress)
}
/// 关闭未完成绘制续体并释放其临时根,不发布半完成命令列表。
// ui_bridge 的窗口切换、输入替换或退役清理路径调用;消费 Work,禁止继续恢复同一个句柄。
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
// 先执行取消和 release 两个操作,再依次传播错误;取消失败也仍尝试释放入口根。
let cancelled = vm.cancel_read_only_call();
let released = vm.release(self.prepare);
cancelled?;
released?;
Ok(())
}
/// 区分源绘制续体是挂起还是完成,保留工作或结束只读域并生成 Draft。
fn accept(
self,
vm: &mut Vm,
progress: Result<endor_core::ExecutionProgress, VmError>,
) -> Result<Progress> {
// 三种结果分别保留 Work、生成 Draft 或清理后返回诊断;只有 Complete 才进入 finish。
match progress {
// 挂起时工作句柄继续持有 frame/output/prepare,不能发布部分输出。
Ok(endor_core::ExecutionProgress::Pending { .. }) => {
Ok(Progress::Pending(Box::new(self)))
}
Ok(endor_core::ExecutionProgress::Complete {
value,
dependencies,
..
}) => {
// VM 完成后先释放入口与驱动返回值,再读取纯原生收集结果;返回值本身不作为绘制命令。
vm.release(self.prepare)?;
vm.release_value(value)?;
finish(vm, self, dependencies)
}
Err(error) => {
// 趁 VM 诊断信息仍可用先生成错误,再取消只读执行并清理临时根。
let diagnostic = crate::endor_host::runtime_error(vm, error);
self.cancel(vm)?;
Err(diagnostic)
}
}
}
}
/// 建立本轮只读绘制驱动和结果收集器;源覆盖与原生默认绘制最终保持同一出现顺序。
// ui_bridge::begin_paint_frame 在布局完成后调用;mounts 顺序来自 frame,root/frame 均是未发布的本轮草稿。
pub fn start(
vm: &mut Vm,
desktop: ObjectRef,
mounts: Vec<(u64, WeakObjectRef)>,
root: Option<Arc<UiNode>>,
frame: Frame,
slice: usize,
) -> Result<Progress> {
// 禁止零切片和嵌套只读/挂起调用;同一 VM 同时只能由一个续体驱动。
anyhow::ensure!(slice > 0, "paint slice must be positive");
anyhow::ensure!(
!vm.has_suspended_call() && !vm.object_store().is_read_only(),
"cannot start paint while another read-only execution is active"
);
// 为整轮创建一个结果容器,跨所有 Mount 和所有挂起切片共享。
let output = Arc::new(Mutex::new(PaintOutput::default()));
// 分配有根的驱动入口;显式捕获 desktop,Work 负责完成或取消后释放入口根。
let prepare = vm.allocate(ContextNativeFunction::new(
"prepare source paint",
Prepare {
desktop,
mounts,
output: output.clone(),
},
vec![desktop],
))?;
// 在进入 VM 前绑定输入草稿、输出和入口根;即使第一片马上完成也走同一个 Work::accept。
let work = Work {
desktop,
prepare,
output,
root,
frame,
slices: 1,
};
// 限制总帧深与总指令数,slice 只切分调度时间;源方法不能借多次 resume 重置总预算。
let result = vm.start_read_only_call(
prepare,
&[],
endor_core::ExecutionLimits {
max_frames: 128,
max_instructions: 1_000_000,
},
slice,
);
work.accept(vm, result)
}
/// 按 Mount 合并源覆盖和原生命令,补齐投影/裁剪表,再递归装配世界 UI 面板。
// 仅由 Work::accept 在 VM Complete 后调用,此时 prepare 已释放;输出容器中只有成功解码的完整覆盖。
fn finish(vm: &mut Vm, mut work: Work, reads: Vec<endor_core::ReadDependency>) -> Result<Progress> {
// 加锁读取整轮收集结果;finish 不再调用源 paint,锁不会跨越新的源执行。
let output = work
.output
.lock()
.map_err(|_| anyhow::anyhow!("paint output poisoned"))?;
// 是否有源覆盖以 map 是否为空判断;源方法返回空命令列表也算覆盖,会抑制该出现的默认绘制。
let native_only = output.commands.is_empty();
// 没有根时没有节点投影可合并,直接交回已有帧并清空绘制读取,不展开默认命令。
if work.root.is_none() {
return Ok(Progress::Complete(Box::new(Draft {
frame: work.frame,
reads: Vec::new(),
slices: work.slices,
})));
}
// 根已在上面检查存在;借用 Arc 内节点建立索引,不把原始 UiNode 所有权转入新命令。
let root = work.root.as_deref();
// 保存桌面身份供末尾依赖过滤,frame 则可单独可变借用以写入新命令表。
let desktop = work.desktop;
let frame = &mut work.frame;
// 建立 Block ID 到不可变节点的索引;最终合并顺序仍以 frame.mounts 为准,不能按 map 排序。
let mut nodes = HashMap::new();
// 显式栈遍历视觉树,节点按 Block 文本身份索引;此遍历顺序不决定最终绘制顺序。
let mut pending: Vec<_> = root.into_iter().collect();
// 每个节点入表并展开其子节点,稍后每个 Mount 用 block_id 查回该节点的默认视觉。
while let Some(node) = pending.pop() {
nodes.insert(node.block_id.as_str(), node);
pending.extend(node.children().iter().map(|node| node.as_ref()));
}
// 四个输出数组同步增长:命令、裁剪、二维变换、出现归属;不能单独过滤其中某一表。
let mut commands = Vec::new();
let mut clips = Vec::new();
let mut views = Vec::new();
let mut owners = Vec::new();
// 最终顺序由布局帧的出现序列决定,既保留层叠顺序,也允许同一 Block 对应多个 Mount。
for mount in &frame.mounts {
// 记录本出现追加前的长度,用差值为新增命令同步扩展 clip/view/owner 三张平行表。
let before = commands.len();
// 有源覆盖则使用其整个结果(包括空列表);否则用节点和 Mount 几何生成原生默认命令。
if let Some(custom) = output.commands.get(&mount.id) {
commands.extend(custom.iter().cloned());
} else {
// 无源覆盖时必须找到原生视觉节点;缺失说明布局/视觉投影不一致,不能默默漏画。
let node = nodes
.get(mount.block_id.as_str())
.ok_or_else(|| anyhow::anyhow!("paint projection has no node"))?;
commands.extend(layout::paint_commands(
node,
mount.x,
mount.y,
Size {
width: mount.width,
height: mount.height,
},
));
}
// 每条新增命令继承当前出现的裁剪、变换和 Mount ID,后续面板装配依赖这些平行元数据。
clips.extend(std::iter::repeat_n(mount.clip, commands.len() - before));
views.extend(std::iter::repeat_n(mount.view, commands.len() - before));
owners.extend(std::iter::repeat_n(mount.id, commands.len() - before));
// 混合原生与源命令后再检查总预算,前面的源输出预算不能覆盖这里新增的默认命令。
if commands.len() > 10_000 {
bail!("paint command budget exceeded");
}
}
// 全部 Mount 成功合并后再替换草稿帧的旧命令与元数据;尚未发布给窗口。
frame.commands = commands;
frame.command_clips = clips;
frame.command_views = views;
frame.command_mounts = owners;
// 统一命令和投影元数据就绪后,再把世界 UI 面板装配进对应 3D 场景。
crate::scene3d::panel::compose(frame)?;
// 类型注册表读取属于宿主检查,过滤它避免把内部类型查找当成业务 paint 依赖。
let registry = object(vm.get_attr(desktop, "types")?)?;
// 排除宿主桌面、类型注册表、Mount 以及已经死亡的临时身份,只保留业务读取。
// 纯原生帧不需要保留这次驱动的读取记录。
let reads = reads
.into_iter()
.filter(|dependency| {
// 对每条读取取所属对象:无源覆盖时全部丢弃,否则剔除宿主桌面、注册表、Mount 和已死亡身份。
let object = dependency.object();
!native_only
&& object != desktop
&& object != registry
&& vm
.object(object)
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
})
.collect();
// 返回完整但未发布的草稿;桥接层接着记录依赖和切片数,运行时再完成帧提交。
Ok(Progress::Complete(Box::new(Draft {
frame: work.frame,
reads,
slices: work.slices,
})))
}