From 0431510d0e5c13550ca4d6f2ffbe08ba5e5ac00d Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 18:45:46 +0800 Subject: [PATCH] docs: explain scene composition and 3D panel coordinate flows --- src/render/scene.rs | 12 ++++++++++++ src/render/scene3d/math.rs | 30 ++++++++++++++++++++++++------ src/render/scene3d/panel.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/render/scene.rs b/src/render/scene.rs index 28afd4a..b5e5bb5 100644 --- a/src/render/scene.rs +++ b/src/render/scene.rs @@ -12,10 +12,13 @@ use crate::{ #[derive(Clone, Debug, Default)] /// 宿主管理区域的几何布局,供窗口事件分辨页面与工具命中。 pub struct SceneLayout { + // 开发工具的逻辑坐标布局;页面业务对象的命中几何仍由 RenderDocument 管理。 pub developer_tools: DeveloperToolsLayout, } /// 组合页面、开发工具和侧栏的设备像素显示列表,同时返回供命中使用的宿主布局。 +/// shell/app 的窗口绘制路径调用此入口;width/height 是整窗设备像素,scale 将逻辑坐标换成设备坐标。 +/// 返回的显示列表交给窗口渲染,layout 则保留同帧开发工具命中区域,避免输入使用另一帧的几何。 pub fn build_scene( width: u32, height: u32, @@ -32,8 +35,10 @@ pub fn build_scene( .iter() .find(|tab| tab.active) .and_then(|tab| tab.developer_tool.as_deref()); + // 先切换当前工具,再通知文档变化;工具绘制由此读取与活动标签一致的状态。 developer_tools_state.select(tool); developer_tools_state.document_updated(document); + // left 是侧栏占用的逻辑宽度,surface 保存业务页面的 pan/zoom;二者属于不同坐标层。 let left = sidebar.left(); let surface = &document.frame.surface; // width/height 为设备像素,sidebar.left 为逻辑单位,先乘 scale 得到页面可用裁剪区域。 @@ -56,6 +61,7 @@ pub fn build_scene( (left - surface.pan.x * zoom) * scale, -surface.pan.y * zoom * scale, )); + // 从空列表开始按背景、页面、工具、侧栏的顺序叠加;后加入的命令绘制在前面内容之上。 let mut display_list = DisplayList::default(); // 全窗只做一次 Clear,后续跳过页面列表自身 Clear,防止其覆盖侧栏或突破页面裁剪。 display_list.push(DrawCommand::Clear( @@ -63,12 +69,15 @@ pub fn build_scene( .unwrap_or(crate::system_ui::theme::BG), )); display_list.push(DrawCommand::PushClip(content)); + // 按页面原有顺序复制命令;页面自身的嵌套裁剪仍处于 content 这个外层裁剪之内。 for command in page.commands() { + // Clear 是整目标缓冲清屏操作,只有非 Clear 命令可以并入已建立背景的窗口列表。 if !matches!(command, DrawCommand::Clear(_)) { display_list.push(command.clone()); } } display_list.push(DrawCommand::PopClip); + // 工具先在扣除侧栏的独立画布内构建,宽度用饱和减法防止窄窗口发生无符号下溢。 let (mut developer_list, mut developer_layout) = developer_tools::build( width.saturating_sub((left * scale) as u32), height, @@ -89,6 +98,7 @@ pub fn build_scene( ); chrome.scale(scale); display_list.extend(chrome); + // 把绘制输出与这次构建产生的交互布局绑定返回,调用方无需重新计算工具位置。 DisplayScene { display_list, layout: SceneLayout { @@ -99,6 +109,8 @@ pub fn build_scene( /// 整窗合成结果:显示列表与同帧管理交互几何一起返回。 pub struct DisplayScene { + // 已完成缩放与平移的整窗绘制命令,供 shell 的渲染路径消费。 pub display_list: DisplayList, + // 保留逻辑坐标供宿主输入路由使用,不能直接当成设备像素命中区域。 pub layout: SceneLayout, } diff --git a/src/render/scene3d/math.rs b/src/render/scene3d/math.rs index 8cb3cd1..aa8cbbf 100644 --- a/src/render/scene3d/math.rs +++ b/src/render/scene3d/math.rs @@ -6,6 +6,7 @@ use std::ops::{Add, Mul, Sub}; #[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)] /// 三维数学向量;构造不校验分量,需在输入边界显式调用 valid,不是托管 Vec3 对象。 +// model/scene3d 用它读取场景变换,原生光栅化、网格拾取和 panel 坐标映射共同复用此类型。 pub struct Vec3 { pub x: f32, pub y: f32, @@ -48,21 +49,21 @@ impl Vec3 { } impl Add for Vec3 { type Output = Self; - /// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。 + /// 按分量相加;摄像机射线用它叠加前向与视平面偏移。 fn add(self, b: Self) -> Self { Self::new(self.x + b.x, self.y + b.y, self.z + b.z) } } impl Sub for Vec3 { type Output = Self; - /// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。 + /// 按分量相减;观察基与三角形求交用它得到相对位置和边向量。 fn sub(self, b: Self) -> Self { Self::new(self.x - b.x, self.y - b.y, self.z - b.z) } } impl Mul for Vec3 { type Output = Self; - /// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。 + /// 每个分量乘同一个标量;归一化、射线步进和光照计算用它调整向量长度。 fn mul(self, b: f32) -> Self { Self::new(self.x * b, self.y * b, self.z * b) } @@ -70,9 +71,10 @@ impl Mul for Vec3 { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] /// 仿射 4×4 变换矩阵,支持局部到世界的组合与求逆。 +// model/scene3d 从 TRS 构造并组合世界矩阵;panel 使用其逆矩阵将射线交点还原为局部 UI 位置。 pub struct Matrix(pub [f32; 16]); impl Default for Matrix { - /// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。 + /// 返回单位矩阵,供场景遍历的根变换使用;与局部矩阵组合时保持原变换。 fn default() -> Self { Self([ 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., @@ -121,7 +123,9 @@ impl Matrix { pub fn compose(self, local: Self) -> Self { // 行主序存储,点视作列向量;self.compose(local) = self × local,即先应用 local 再应用 self。 let mut out = [0.; 16]; + // 逐行逐列计算输出的 16 个元素;k 遍历 self 的行与 local 的列,累加四项点积。 for row in 0..4 { + // row/col 都属于输出矩阵;不能交换两个乘数,否则父子变换顺序会反转。 for col in 0..4 { out[row * 4 + col] = (0..4) .map(|k| self.0[row * 4 + k] * local.0[k * 4 + col]) @@ -141,10 +145,11 @@ impl Matrix { /// 由平移、角度旋转和缩放构造局部仿射矩阵。 pub fn trs(position: Vec3, rotation: Vec3, scale: Vec3) -> Self { // 输入欧拉角单位为度,先转换为弧度。组合为 T × Rz × Ry × Rx × S,列分别乘对应缩放轴。 + // 分别缓存三个轴的正弦和余弦,后面的矩阵展开式复用这些值。 let (sx, cx) = rotation.x.to_radians().sin_cos(); let (sy, cy) = rotation.y.to_radians().sin_cos(); let (sz, cz) = rotation.z.to_radians().sin_cos(); - // Local X, then Y, then Z rotation; scale first, translation last. + // 点先缩放,再依次绕局部 X、Y、Z 旋转,最后加平移;这里直接展开组合矩阵以避免临时矩阵。 Self([ cz * cy * scale.x, (cz * sy * sx - sz * cx) * scale.y, @@ -177,16 +182,21 @@ pub enum Projection { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] /// 原生摄像机描述,包括观察基、近远面和投影参数;宽高比由视口提供。 +// 场景发布时由 model 验证参数;View 渲染/拾取与 panel 映射随后使用同一份相机描述。 pub struct Camera { + // 世界空间眼睛位置;透视投影的全部拾取射线从这里出发。 pub position: Vec3, + // 世界空间观察目标,用 target-position 确定前向,而不是直接保存欧拉角。 pub target: Vec3, + // 提供上方向参考,basis 会重新正交化;与前向平行时无法形成观察基。 pub up: Vec3, + // 近远面按相机正向深度裁剪;valid 要求 0 < near < far。 pub near: f32, pub far: f32, pub projection: Projection, } impl Default for Camera { - /// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。 + /// 返回从世界 z=5 观察原点的透视相机,供缺省场景与原生测试构造使用。 fn default() -> Self { Self { position: Vec3::new(0., 0., 5.), @@ -211,7 +221,9 @@ impl Camera { // forward 指向观察目标;up 输入只提供大致上方向,叉积后重新正交化。 // 位置等于目标、up 为零或与前向平行时,normalized 返回 None。 let forward = (self.target - self.position).normalized()?; + // 前向叉上方向产生右向;再次归一化,消除输入 up 长度对后续投影的影响。 let right = forward.cross(self.up).normalized()?; + // 用已确定的右向和前向重建垂直上向,让三个基向量彼此正交。 let up = right.cross(forward).normalized()?; Some(Basis { right, up, forward }) } @@ -227,6 +239,7 @@ impl Camera { && self.near > 0. && self.far > self.near && self.far <= 1_000_000. + // 投影类型决定最后的合法性条件;透视角度受限以避免极端 tan,正交高度必须为正。 && match self.projection { Projection::Perspective { fov } => fov.is_finite() && (1.0..=175.0).contains(&fov), Projection::Orthographic { height } => { @@ -242,6 +255,7 @@ impl Camera { } /// 从视口归一化坐标生成透视或正交射线;宽高比由具体视口提供。 pub fn ray(self, u: f32, v: f32, aspect: f32) -> Option { + // View::pick 和 panel::Mapping 的逆投影都从这里生成射线;退化观察基直接向上传播 None。 let basis = self.basis()?; // u/v 通常是视口内 [0,1],映射到 [-1,1] 并翻转屏幕 y。 // 此函数不钳制输入,捕获拖动可以从视口外继续生成射线;aspect 由调用方保证有效。 @@ -276,6 +290,7 @@ pub struct Ray { } impl Ray { /// 求射线与三角形相交距离和重心坐标,拒绝退化三角形及射线起点后方的交点。 + // View::pick 遍历网格三角形时调用,使用返回的射线参数选最近命中,重心权重还可插值纹理坐标。 pub fn triangle(self, a: Vec3, b: Vec3, c: Vec3) -> Option<(f32, [f32; 3])> { // Möller–Trumbore 求交:使用两条三角形边与叉积解重心系数,不先构造平面方程。 let ab = b - a; @@ -286,6 +301,7 @@ impl Ray { if determinant.abs() < 1e-7 { return None; } + // 复用行列式倒数以解重心系数;t 此处是从顶点 a 指向射线起点的向量。 let inverse = 1.0 / determinant; let t = self.origin - a; // u 是顶点 b 的重心权重,必须位于 [0,1];v 是 c 的权重,a 的权重由 1-u-v 得到。 @@ -293,8 +309,10 @@ impl Ray { if !(0.0..=1.0).contains(&u) { return None; } + // 第二次叉积用于同时求顶点 c 的权重和射线参数,避免另外求解完整线性方程组。 let q = t.cross(ab); let v = self.direction.dot(q) * inverse; + // v 为负或 b/c 权重和超过 1 都表示交点落在三角形外,即使它仍位于三角形所在平面。 if v < 0. || u + v > 1. { return None; } diff --git a/src/render/scene3d/panel.rs b/src/render/scene3d/panel.rs index 9474fe4..9f88795 100644 --- a/src/render/scene3d/panel.rs +++ b/src/render/scene3d/panel.rs @@ -21,16 +21,20 @@ pub struct Mapping { pub owner: u64, // 面板在场景中的稳定标识,不能仅凭 Block 身份选择某个具体面板出现。 pub key: u64, + // 保存这次出现所使用的不可变场景和相机;命中必须与生成画面的视图一致。 pub view: Arc, + // [x,y,width,height] 是所属二维空间中的视口布局矩形,宽高也决定射线的 aspect。 pub rect: [f32; 4], // 外层二维相机变换;逆映射先处理它,再进入 3D 摄像机射线计算。 pub outer: Transform, + // 当前层的二维裁剪,在逆 outer 之前检查;None 表示这一层没有额外矩形裁剪。 pub clip: Option<[f32; 4]>, // 嵌套面板的外层链;反投影从父到子,正投影从子到父。 pub parent: Option>, } impl Mapping { /// 沿父面板逆投影到当前平面的 UI 像素;要求可见时使用最近射线命中拒绝被遮挡的目标。 + // RenderDocument 的面板坐标转换调用这里服务命中与事件派发;clipped=false 用于捕获后的连续拖动。 pub fn texture_point(&self, point: Point, clipped: bool) -> Option { // 先把最外层窗口坐标逐层反投影为父面板像素,再检查当前面板所属空间的裁剪。 let point = if let Some(parent) = &self.parent { @@ -45,19 +49,24 @@ impl Mapping { { return None; } + // 先撤销当前层二维平移/缩放,后面的 rect 才能按未变换布局坐标归一化。 let point = self.outer.inverse(point); let [x, y, width, height] = self.rect; + // 零或负视口没有可用面积,不能计算归一化位置或宽高比。 if width <= 0. || height <= 0. { return None; } // rect 是 3D 视口在当前二维空间的位置,归一化坐标传给该视口相机;不是面板纹理坐标。 let (u, v) = ((point.x - x) / width, (point.y - y) / height); + // key 标识当前出现的面板网格;找不到时不退回其他面板,以免把事件发给错误对象。 let mesh = self.view.scene.meshes.iter().find(|mesh| { mesh.panel .as_ref() .is_some_and(|panel| panel.key == self.key) })?; + // panel 提供实际纹理像素宽高;它与世界几何尺寸分开,缩放网格不会改变 UI 的像素坐标。 let panel = mesh.panel.as_ref()?; + // 普通命中要求当前可见;捕获手势跳过这一步,使指针离开面板或被遮挡后仍能继续拖动。 if clipped { // 指针落在平面投影内还不够:最近三角形必须属于本面板,不能点穿前方网格。 let hit = self.view.pick(u, v, width / height)?; @@ -71,6 +80,7 @@ impl Mapping { } // 捕获拖动可以不要求指针仍位于面板内,但仍使用原面板平面的连续逆映射。 let ray = self.view.camera.ray(u, v, width / height)?; + // 平面中心与两条世界轴共同确定相交平面;不直接把局部 z 轴当作非均匀缩放后的法线。 let center = mesh.world.point(Vec3::default()); let normal = (mesh.world.point(Vec3::new(1., 0., 0.)) - center) .cross(mesh.world.point(Vec3::new(0., 1., 0.)) - center); @@ -100,6 +110,7 @@ impl Mapping { } /// 把当前面板的 UI 像素正向投影到外层页面空间,供坐标查询和交互测试使用。 + // model/mount 的坐标查询使用此正投影,将面板内布局位置还原到外层页面供调用者定位。 pub fn project_point(&self, point: Point) -> Option { let mesh = self.view.scene.meshes.iter().find(|mesh| { mesh.panel @@ -107,6 +118,7 @@ impl Mapping { .is_some_and(|panel| panel.key == self.key) })?; let panel = mesh.panel.as_ref()?; + // 面板几何由矩形网格构造路径保证顶点顺序;这里读取局部宽高,再与纹理分辨率换算。 let width = mesh.geometry.vertices[1].x - mesh.geometry.vertices[0].x; let height = mesh.geometry.vertices[0].y - mesh.geometry.vertices[2].y; // 正向路径与 texture_point 对称:纹理像素先转局部平面点,再进入世界空间。 @@ -115,6 +127,7 @@ impl Mapping { (0.5 - point.y / panel.height as f32) * height, 0., )); + // 世界点先转换成相机的右/上/前坐标,后续用正向深度决定裁剪和透视除数。 let camera = self.view.camera; let local = camera.view(camera.basis()?, world); // 点在近远面之外时无可用投影;此方法仅算位置,不检查前方遮挡或二维 clip。 @@ -127,6 +140,7 @@ impl Mapping { Projection::Perspective { fov } => local.z * (fov.to_radians() / 2.).tan(), Projection::Orthographic { height } => height / 2., }; + // 归一化投影恢复为视口逻辑坐标,最后施加所属二维层的 outer 变换。 let point = self.outer.point(Point::new( x + (local.x / (half * w / h) + 1.) * w / 2., y + (1. - local.y / half) * h / 2., @@ -152,23 +166,28 @@ struct Entry { /// Collect a panel's ordinary paint subtree into its texture. Nested 3D /// commands are assembled recursively before their containing texture. /// 按面板所属视口和稳定 key 分组绘制命令,递归将内部 UI 装配进 3D 面板表面。 +/// source_paint 在生成各 Mount 命令后调用;输入的 command_clips、command_views、command_mounts 必须与 commands 对齐,归属 ID 必须能查到 Mount。 pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> { use crate::document::RenderCommand; use std::collections::{HashMap, HashSet}; + // 按 Mount ID 建立查找表,让每条命令直接取到所属面板映射,避免逐条扫描完整 Mount 列表。 let mounts: HashMap<_, _> = frame.mounts.iter().map(|mount| (mount.id, mount)).collect(); // 分组键包含视口 owner 和面板 key,防止同一场景在两个视口出现时互相混入绘制命令。 let mut groups = HashMap::<(u64, u64), Vec>::new(); let mut roots = Vec::new(); // 每条命令都有出现归属;据此分流为窗口根命令或某个世界面板的内部命令。 for (index, command) in frame.commands.iter().enumerate() { + // owner 是该条绘制命令的 Mount ID;面板分组使用 mount.panel 内的外层视口 owner。 let owner = frame.command_mounts[index]; let mount = mounts[&owner]; + // 克隆命令并携带同索引裁剪、二维变换,移动到纹理后仍保持原始绘制语义。 let entry = Entry { command: command.clone(), clip: frame.command_clips[index], view: frame.command_views[index], owner, }; + // 面板后代收进对应纹理分组;不属于面板的命令保留为根层,二者都维持输入相对顺序。 if let Some(panel) = &mount.panel { groups .entry((panel.owner, panel.key)) @@ -207,6 +226,7 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> { path: &mut HashSet, depth: usize, ) -> anyhow::Result { + // 仅本次条目可变;没有面板的普通命令可原样返回,不需要复制整个场景。 let mut entry = entry; if let RenderCommand::Scene3D { view, .. } = &mut entry.command && view.scene.meshes.iter().any(|mesh| mesh.panel.is_some()) @@ -218,8 +238,10 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> { ); // 只构造新的原生快照,不写回源 Scene3D;同一业务场景仍可被其他视口共享。 let mut scene = (*view.scene).clone(); + // 每个面板取得自己分组的 UI 命令;普通网格没有纹理子树,直接跳过。 for mesh in &mut scene.meshes { if let Some(panel) = &mut mesh.panel { + // 一张表面保存三组平行数据;每个子条目装配成功后同时追加,保持索引对应。 let mut surface = super::UiSurface { commands: Vec::new(), clips: Vec::new(), @@ -232,9 +254,11 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> { surface.clips.push(child.clip); surface.views.push(child.view); } + // 即使分组为空也发布空表面,避免继承源快照中与本次出现无关的旧绘制内容。 panel.surface = Some(Arc::new(surface)); } } + // 新场景替换本条目引用,相机保持不变;其他命令持有的 Arc 不受影响。 *view = Arc::new(View { scene: Arc::new(scene), camera: view.camera, @@ -244,12 +268,14 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> { } Ok(entry) } + // 所有根条目共用临时祖先集合,每条递归返回时删除标记;depth 从根的 0 开始计算。 let mut path = HashSet::new(); // 面板内部命令已经移入 UiSurface,根帧只保留外层命令;四张平行元数据表同步重建。 frame.commands.clear(); frame.command_clips.clear(); frame.command_views.clear(); frame.command_mounts.clear(); + // 递归错误会在清空之后返回,此函数本身不保证出错时恢复原帧;调用方需处理整帧失败。 for entry in roots { let entry = assemble(entry, &groups, &mut path, 0)?; frame.commands.push(entry.command); @@ -261,11 +287,14 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> { } /// 为一个面板分配有限像素表面,并调用普通 UI 软件渲染;当前实现不会按面板内容独立缓存纹理。 +/// 软件光栅化的面板纹理路径调用;本函数每次产生新像素数组,复用策略由外层调用方决定。 pub fn texture(panel: &super::Panel) -> Result, &'static str> { // 先向统一像素预算申请有限纹理面积,再分配背景缓冲;嵌套面板同样受该预算约束。 crate::software_renderer::reserve_pixels(panel.width as usize * panel.height as usize)?; + // u32 缓冲按纹理宽高铺满背景;没有 UiSurface 时直接返回这张纯背景纹理。 let mut pixels = vec![super::rgb(panel.background); panel.width as usize * panel.height as usize]; + // 有 UI 子树才生成显示列表并绘制;解析或渲染错误向上返回,不把半成品作为成功纹理。 if let Some(surface) = &panel.surface { // 将普通 UI 表面包装为临时文档复用现有显示列表和软件渲染路径;比例 1 对应纹理像素。 let document = crate::document::RenderDocument { @@ -278,6 +307,7 @@ pub fn texture(panel: &super::Panel) -> Result, &'static str> { }, ..Default::default() }; + // 比例为 1,保持子 UI 坐标和纹理像素一一对应;无效命令转换成纹理路径的简短错误。 let list = document .display_list(1.) .map_err(|_| "invalid 3D UI texture commands")?;