//! 将 Endor 托管场景对象转换成可供渲染线程读取的 3D 数据。 //! 构造器和字段仍属于 VM;快照复制必要的数值、不可变网格和依赖,渲染器不读取托管堆。 //! 场景遍历组合局部变换、检查环和资源限制,并为 Panel3D 提取独立 UI 根。控制器更新相机;拾取结果再映射回托管节点。 //! //! 调用路径:bootstrap 用 Constructor 元数据安装类型/工厂,ui_bridge::calls 调用 allocate/transform/control。 //! RenderCache 调用 snapshot_reusing 生成 View 与面板孩子;Mount.pick3D 和原生指针适配器调用 pick。 //! 三维 renderer 只接收不可变原生数值;ObjectRef 读取依赖与 PanelMount.content 始终留在 VM 驱动侧。 //! //! Traced 3D objects and checked immutable snapshots. No managed handle enters //! the renderer; cameras and node transforms remain editable Endor attributes. use crate::{ object_list, scene3d::{ self, math::{Camera, Matrix, Projection, Vec3}, }, }; use endor_core::{NativeContext, ObjectPayload, ObjectRef, ObjectStore, Value, VmError}; use std::{collections::HashSet, sync::Arc}; #[derive(Debug, Clone, Copy)] /// 源 3D 类型的构造元数据入口,统一管理名称、参数、数量和可变子项。 pub enum Constructor { // 三轴数值对象,供顶点、交点、法线与重心权重使用。 Vec3, // 三角形顶点索引记录 a/b/c,组装 Geometry 时再验证整数与范围。 Triangle, // 复制顶点/三角形列表为不可变原生网格,不安装可放置节点标记。 Geometry, // 组合已有 Geometry3D 与不透明颜色,拥有几何的托管属性引用。 Mesh, // 创建长宽深盒网格,再沿 Mesh 路径保存几何与材质。 Box, // 可放置场景组合节点,孩子列表可包含共享节点。 Group, // 场景根,额外保存 ambient/background,不参与网格式 TRS。 Scene, // 方向光,用方向向量、颜色和强度描述,不使用网格欧拉角。 Light, // 透视相机,快照时读取 fov 作为投影参数。 Perspective, // 正交相机,快照时读取 height 作为可见世界高度。 Orthographic, // 默认轨道控制器,只保存可编辑速度,处理相机手势时才修改相机。 Orbit, // 世界平面承载普通 Block UI,世界尺寸和纹理分辨率分别保存。 Panel, } impl Constructor { /// 返回内部枚举或对象对应的稳定源名称。 pub fn name(self) -> &'static str { // bootstrap 用这些稳定名义类型名安装/查询桌面注册表,不能与小写工厂名混用。 match self { Self::Vec3 => "Vec3", Self::Triangle => "Triangle3D", Self::Geometry => "Geometry3D", Self::Mesh => "Mesh3D", Self::Box => "Box3D", Self::Group => "Group3D", Self::Scene => "Scene3D", Self::Light => "DirectionalLight", Self::Perspective => "PerspectiveCamera", Self::Orthographic => "OrthographicCamera", Self::Orbit => "OrbitController", Self::Panel => "Panel3D", } } /// 取得本构造器对应的源工厂函数名称。 pub fn function(self) -> &'static str { // 桥接函数路由按这些公开工厂名识别构造器,与 name 的名义类型标识一一对应。 match self { Self::Vec3 => "vec3", Self::Triangle => "triangle3D", Self::Geometry => "geometry3D", Self::Mesh => "mesh3D", Self::Box => "box3D", Self::Group => "group3D", Self::Scene => "scene3D", Self::Light => "directionalLight", Self::Perspective => "perspectiveCamera", Self::Orthographic => "orthographicCamera", Self::Orbit => "orbitController", Self::Panel => "panel3D", } } /// 反查源构造器名称对应的原生 3D 种类,不认识的名称返回空值。 pub fn from_function(name: &str) -> Option { // 只在显式注册的构造器集合里反查,未知名字交给其他桥接分支处理。 [ Self::Vec3, Self::Triangle, Self::Geometry, Self::Mesh, Self::Box, Self::Group, Self::Scene, Self::Light, Self::Perspective, Self::Orthographic, Self::Orbit, Self::Panel, ] .into_iter() .find(|kind| kind.function() == name) } /// 返回三维原生构造的显式最小/最大参数数,不含请求 Type;Group/Scene 支持最多4096子节点。 pub fn arity(self) -> (usize, usize) { // 构造器 arity 不含 NativeTyped 的类型首参,Group/Scene 允许零到 4096 个孩子,其他零参对象使用默认属性。 match self { Self::Vec3 | Self::Triangle => (3, 3), Self::Geometry | Self::Mesh => (2, 2), Self::Box => (4, 4), Self::Group | Self::Scene => (0, 4096), Self::Light => (5, 5), Self::Panel => (3, 3), _ => (0, 0), } } /// 列出本构造器的参数类型名称,供签名安装与源调用检查使用。 pub fn parameters(self) -> &'static [&'static str] { // 安装参数注解时按显式实参顺序提供类型;Group/Scene 的可变节点规则另由 variadic/arity 解释。 match self { Self::Vec3 | Self::Triangle => &["Number", "Number", "Number"], Self::Geometry => &["ObjectList", "ObjectList"], Self::Mesh => &["Geometry3D", "String"], Self::Box => &["Number", "Number", "Number", "String"], Self::Light => &["Number", "Number", "Number", "String", "Number"], Self::Panel => &["Number", "Number", "Block"], _ => &[], } } /// 标记构造器是否接收数量可变的子对象。 pub fn variadic(self) -> bool { matches!(self, Self::Group | Self::Scene) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// 托管 3D 节点的名义种类,防止仅有同名字段的任意对象冒充场景节点。 pub enum NodeKind { Vector, Triangle, Mesh, Group, Scene, Light, Perspective, Orthographic, Orbit, Panel, } #[derive(Debug)] /// 允许普通属性的 3D 标记 payload;数值字段仍由托管属性系统拥有。 pub struct Node(pub NodeKind); impl ObjectPayload for Node { /// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。 fn as_any(&self) -> &dyn std::any::Any { self } /// 返回运行时诊断/类型检查使用的稳定 payload 名称。 fn kind_name(&self) -> &'static str { "scene-3d-object" } /// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。 fn reference_assignment_only(&self) -> bool { true } /// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。 fn allows_attributes(&self) -> bool { true } } #[derive(Debug)] /// 托管不可变网格包装,顶点和三角形数值已经复制为原生 Arc 数据。 pub struct Geometry(pub Arc); impl ObjectPayload for Geometry { /// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。 fn as_any(&self) -> &dyn std::any::Any { self } /// 返回运行时诊断/类型检查使用的稳定 payload 名称。 fn kind_name(&self) -> &'static str { "geometry-3d" } /// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。 fn reference_assignment_only(&self) -> bool { true } /// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。 fn allows_attributes(&self) -> bool { true } } /// 统一构造本模块的参数错误,让调用方停止处理无效输入。 fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } /// 把源 Value 转换为受检查的托管对象引用,拒绝非对象输入。 fn object_ref(value: &Value) -> Result { // 构造与快照共用对象边界检查,只有真正托管对象才能交给 payload/列表读取。 if let Value::Object(object) = value { Ok(*object) } else { Err(invalid("3D field requires an object")) } } /// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。 fn number(value: &Value) -> Result { crate::scroll::coordinate(value) } /// 把当前源值或文本选择解码成此调用需要的文本表示。 fn text(value: &Value) -> Result { // 材质、背景和手势种类只接受源文本,不把对象 repr 隐式转换为策略或颜色。 if let Value::Text(text) = value { Ok(text.clone()) } else { Err(invalid("3D field requires text")) } } /// 把经过转换的数值写回指定托管字段,仍通过 VM/NativeContext 的检查入口。 fn set_number( cx: &mut NativeContext<'_>, object: ObjectRef, name: &str, value: f32, ) -> Result<(), VmError> { // 所有构造/控制器/变换写回都走 NativeContext 属性入口,保留类型检查与变化日志。 cx.set_attr(object, name, crate::native_visual::value(value)) } /// 解析颜色值并检查其满足当前原生绘制约定。 fn color(value: &str) -> Result<[f32; 3], VmError> { // 3D 不透明材质拒绝半透明颜色,转换为 [0,1] RGB 数值;普通二维 UI 的 alpha 不受这里限制。 let color = crate::document::parse_color(value).map_err(|_| invalid("invalid 3D color"))?; // 三维表面目前按不透明材质处理,非全 alpha 明确拒绝,不能静默丢透明通道。 if color.alpha != 255 { return Err(invalid("3D opaque material color requires full alpha")); } Ok([ color.red as f32 / 255., color.green as f32 / 255., color.blue as f32 / 255., ]) } /// 按构造器建立对应托管 3D 类型,检查几何输入并安装默认属性。 pub fn allocate( cx: &mut NativeContext<'_>, desktop: ObjectRef, definition: ObjectRef, constructor: Constructor, args: &[Value], ) -> Result { // 入口假设桥接工厂已按 arity/类型检查 args;Geometry 特殊地复制缓冲并提前返回,其余创建可编辑标记节点。 let kind = match constructor { Constructor::Vec3 => NodeKind::Vector, Constructor::Triangle => NodeKind::Triangle, Constructor::Mesh | Constructor::Box => NodeKind::Mesh, Constructor::Group => NodeKind::Group, Constructor::Scene => NodeKind::Scene, Constructor::Light => NodeKind::Light, Constructor::Perspective => NodeKind::Perspective, Constructor::Orthographic => NodeKind::Orthographic, Constructor::Orbit => NodeKind::Orbit, Constructor::Panel => NodeKind::Panel, // Geometry 是特殊返回路径,复制原生缓冲后立即附请求类型,不继续分配 Node 标记。 Constructor::Geometry => { // Geometry 构造只接受受控 ObjectList,逐成员验证 Vec3 payload 并复制数值;以后改源 Vec3 不会改已建几何。 // 第一 ObjectList 的每个成员都必须是 Vec3,逐字段读取后复制进原生 Vec3 数组。 let vertices = object_list::items(cx.store(), object_ref(&args[0])?)? .iter() .map(|&item| { // 短借用当前 ObjectStore,不在原生几何内保存成员 ObjectRef。 let p = cx.store(); // 名义字段形状不够,payload 种类必须确为 Vector,防止 Triangle/任意对象冒充顶点。 if p.payload::(item, "Vec3")?.0 != NodeKind::Vector { return Err(invalid("geometry vertices require Vec3")); } Ok(Vec3::new( number(&p.get_attr(item, "x")?)?, number(&p.get_attr(item, "y")?)?, number(&p.get_attr(item, "z")?)?, )) }) .collect::, VmError>>()?; // 第二 ObjectList 转为 u32 三元索引数组,值检查与整网格的下标范围检查分开执行。 let triangles = object_list::items(cx.store(), object_ref(&args[1])?)? .iter() .map(|&item| { // 成员必须为 Triangle 标记,不能因有 a/b/c 属性就接受其他对象。 if cx.store().payload::(item, "Triangle3D")?.0 != NodeKind::Triangle { return Err(invalid("geometry indices require Triangle3D")); } // 先准备三个原生索引槽,全部校验成功后再加入输出数组。 let mut triangle = [0; 3]; // 索引先从 Number 检查非负整数,再转 u32;是否越出顶点范围在整份 geometry.valid 校验。 for (index, name) in ["a", "b", "c"].into_iter().enumerate() { // 通过统一有限坐标数值解析读取索引,再要求非负且无小数部分。 let value = number(&cx.store().get_attr(item, name)?)?; // 负数/小数索引立即失败,不能通过 as u32 截断生成另一个三角形。 if value < 0. || value.fract() != 0. { return Err(invalid("triangle indices must be nonnegative integers")); } triangle[index] = value as u32; } Ok(triangle) }) .collect::, VmError>>()?; // 将两份复制后的数组组成无 UV 的自定义几何;顶点/三角预算在 valid 统一检查。 let geometry = scene3d::Geometry { vertices, triangles, uv: vec![], }; // 缓存为原生 Arc 前统一验证顶点/三角形预算与下标范围,避免渲染线程再接触源缓冲对象。 if !geometry.valid() { return Err(invalid( "invalid Geometry3D buffers or geometry budget exceeded", )); } // 只有完整网格合法才将原生 Arc 包装为托管 Geometry,之后改源列表不会改变它。 let object = cx.allocate(Geometry(Arc::new(geometry)))?; cx.attach_type(object, definition)?; return Ok(object); } }; // 其余构造器先分配可编辑 Node 标记,实际数值/引用由普通托管属性保存。 let instance = cx.allocate(Node(kind))?; cx.attach_type(instance, definition)?; // 按节点种类初始化必需字段;通用 TRS 在各专有字段之后统一安装。 match kind { // 这两种记录同为三数输入,只是公开字段名不同;Geometry 消费时再施加索引专用约束。 NodeKind::Vector | NodeKind::Triangle => { // 按构造种类选择 xyz 或 abc,再与已由桥接检查数量的 args 逐项配对。 for (name, value) in (if kind == NodeKind::Vector { ["x", "y", "z"] } else { ["a", "b", "c"] }) .into_iter() .zip(args) { set_number(cx, instance, name, number(value)?)?; } } // Mesh 与 Box 最终都保存 Geometry 引用及颜色文本,但网格来源不同。 NodeKind::Mesh => { // Box 现场生成 cuboid,普通 Mesh 使用调用方给定几何,两路径合并后统一校验材质。 let (geometry, material) = if matches!(constructor, Constructor::Box) { // 三轴尺寸先转为有限 f32,再拒绝负值;零值允许生成退化面。 let (w, h, d) = (number(&args[0])?, number(&args[1])?, number(&args[2])?); // Box 允许零尺寸产生退化面,后续光栅/拾取跳过退化三角形;负尺寸在构造入口拒绝。 if w < 0. || h < 0. || d < 0. { return Err(invalid("Box3D dimensions must be nonnegative")); } // 生成盒网格后为它安装 Geometry3D 名义类型,作为 Mesh.geometry 的强属性引用。 let geometry = cx.allocate(Geometry(Arc::new(scene3d::Geometry::cuboid(w, h, d))))?; crate::ui_bridge::attach_desktop_type(cx, geometry, "Geometry3D", desktop)?; (geometry, text(&args[3])?) } else { (object_ref(&args[0])?, text(&args[1])?) }; cx.store().payload::(geometry, "Geometry3D")?; color(&material)?; cx.set_attr(instance, "geometry", Value::Object(geometry))?; cx.set_attr(instance, "color", Value::Text(material))?; } // 孩子只接受 Mesh/Group/Light/Panel 标记,不允许把相机、控制器或任意 Object 放进场景组成。 NodeKind::Group | NodeKind::Scene => { // Group/Scene 的可变实参必须全为对象,逐项 payload 检查后才包装列表。 let children = args.iter().map(object_ref).collect::, _>>()?; // 每个成员都必须是能放进场景的节点;相机和控制器不是可绘制组成孩子。 for child in &children { // 检查 NodeKind 而非仅检查共同的 Node payload,因为相机/向量也使用该 payload。 if !matches!( cx.store().payload::(*child, "3D node")?.0, NodeKind::Mesh | NodeKind::Group | NodeKind::Light | NodeKind::Panel ) { return Err(invalid( "Scene3D/Group3D children require meshes, groups or lights", )); } } // 托管 children 列表拥有子节点;Node payload 自身只是标记,属性拥有边由 VM 正常追踪。 let children = object_list::collection(cx, children, desktop)?; cx.set_attr(instance, "children", Value::Object(children))?; // 只有场景根初始化环境光和背景,普通 Group 不隐式提供这些全局场景字段。 if kind == NodeKind::Scene { set_number(cx, instance, "ambient", 0.25)?; cx.set_attr(instance, "background", Value::Text("#F1F3F6".into()))?; } } // 方向分量初始按数值写入;零方向与强度范围等在提取快照时完整检查。 NodeKind::Light => { // 前三实参依次写方向,后两实参分别检查颜色和强度数值。 for (name, value) in ["directionX", "directionY", "directionZ"] .into_iter() .zip(args) { set_number(cx, instance, name, number(value)?)?; } color(&text(&args[3])?)?; cx.set_attr(instance, "color", args[3].clone())?; set_number(cx, instance, "intensity", number(&args[4])?)?; } // 世界宽高与 UI 像素分辨率独立:初始纹理 512×320,content 是普通 Block 图,稍后参与独立有限布局。 NodeKind::Panel => { // Panel 的第三参数是普通 UI 根,构造时先验证其 Block 图再安装 content 强引用。 let content = object_ref(&args[2])?; crate::block::validate(cx.store(), content, None)?; set_number(cx, instance, "width", number(&args[0])?)?; set_number(cx, instance, "height", number(&args[1])?)?; set_number(cx, instance, "uiWidth", 512.)?; set_number(cx, instance, "uiHeight", 320.)?; cx.set_attr(instance, "content", Value::Object(content))?; cx.set_attr(instance, "background", Value::Text("#FFFFFF".into()))?; } // 两种相机共用位置/target/up/near/far 默认字段,快照按种类只取实际投影参数。 NodeKind::Perspective | NodeKind::Orthographic => { // 为相机建立完整非退化初始观察基,保留 fov/height 以支持对应投影。 for (name, value) in [ ("x", 0.), ("y", 0.), ("z", 5.), ("targetX", 0.), ("targetY", 0.), ("targetZ", 0.), ("upX", 0.), ("upY", 1.), ("upZ", 0.), ("near", 0.1), ("far", 1000.), ("fov", 50.), ("height", 5.), ] { set_number(cx, instance, name, value)?; } } // 控制器仅初始化转速与平移速度;不持有相机,源可替换控制器对象。 NodeKind::Orbit => { set_number(cx, instance, "rotationSpeed", 0.005)?; set_number(cx, instance, "panSpeed", 1.)?; } } // 只有可放置节点安装 TRS;相机用 position/target/up,方向光用方向向量,不复用网格欧拉角字段。 if matches!(kind, NodeKind::Mesh | NodeKind::Group | NodeKind::Panel) { // Mesh/Group/Panel 统一从单位缩放、零位置/角度开始,世界矩阵在快照遍历时组合。 for (name, value) in [ ("x", 0.), ("y", 0.), ("z", 0.), ("rotationX", 0.), ("rotationY", 0.), ("rotationZ", 0.), ("scaleX", 1.), ("scaleY", 1.), ("scaleZ", 1.), ] { set_number(cx, instance, name, value)?; } } Ok(instance) } /// 快照字段读取器,同时记录实际观察到的对象/属性依赖。 struct Reader<'a> { // 借用当前 VM 堆,Reader 只在同步快照/控制器调用内存活,不跨线程。 store: &'a ObjectStore, // 精确对象/字段键集合,返回 RenderCache 建反向依赖;集合不 retain 身份。 reads: HashSet<(ObjectRef, String)>, } impl Reader<'_> { /// 读取字段并登记依赖,使后续字段变更能够使 3D 快照失效。 fn value(&mut self, object: ObjectRef, name: &str) -> Result { // 读取前登记精确对象/属性依赖,HashSet 去重;返回给视觉缓存后,相关字段变化才能驱动重建。 self.reads.insert((object, name.into())); self.store.get_attr(object, name) } /// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。 fn number(&mut self, object: ObjectRef, name: &str) -> Result { number(&self.value(object, name)?) } /// 连续读取三个数值字段,组成位置、旋转或缩放向量。 fn vector(&mut self, object: ObjectRef, names: [&str; 3]) -> Result { Ok(Vec3::new( self.number(object, names[0])?, self.number(object, names[1])?, self.number(object, names[2])?, )) } /// 读取并校验透视或正交摄像机字段,生成不持有 VM 引用的投影描述。 fn camera(&mut self, object: ObjectRef) -> Result { // 先验证宿主相机种类,再按投影策略读取 fov 或 height;不把有同名字段的对象当相机。 let projection = match self.store.payload::(object, "3D camera")?.0 { NodeKind::Perspective => Projection::Perspective { fov: self.number(object, "fov")?, }, NodeKind::Orthographic => Projection::Orthographic { height: self.number(object, "height")?, }, _ => { return Err(invalid( "Viewport3D requires a PerspectiveCamera or OrthographicCamera", )); } }; // 将相机所有输入复制为原生数值,后续 valid 同时验证观察基与投影近远范围。 let camera = Camera { position: self.vector(object, ["x", "y", "z"])?, target: self.vector(object, ["targetX", "targetY", "targetZ"])?, up: self.vector(object, ["upX", "upY", "upZ"])?, near: self.number(object, "near")?, far: self.number(object, "far")?, projection, }; // 单字段有限不代表组合有效;位置=目标、退化 up 或非法投影范围必须整体拒绝。 if !camera.valid() { return Err(invalid("invalid 3D camera basis or projection range")); } Ok(camera) } } /// 从场景中提取的有限 UI 根,携带稳定面板 key 和逻辑排版尺寸。 pub struct PanelMount { // 此面板出现路径的稳定 key,交给 Block 组成 slot 及纹理映射,不等于面板业务身份。 pub key: u64, // 仍在 VM 侧参与独立有限布局的 Block 根,只作为返回的借用身份,不进入 renderer。 pub content: ObjectRef, // 面板 UI 的逻辑像素分辨率,与世界平面的 width/height 无关。 pub size: crate::layout::Size, } // 三部分分别是可跨线程的不可变 View、VM 内缓存用的属性依赖、仍由 VM 驱动布局的面板 Block 根。 pub type Snapshot = ( Arc, Vec<(ObjectRef, String)>, Vec, ); /// 将当前托管或可变状态提取为后续阶段使用的派生表示。 pub fn snapshot( store: &ObjectStore, scene: ObjectRef, camera: ObjectRef, ) -> Result { // 一次性提取入口无旧 Scene/BVH 可复用;pick 使用此路径获得本次实际字段读取。 snapshot_reusing(store, scene, camera, None) } /// 遍历场景并记录字段依赖;如果世界数据未变则复用不可变场景和 BVH,只替换摄像机视图。 pub fn snapshot_reusing( store: &ObjectStore, scene: ObjectRef, camera: ObjectRef, reuse: Option>, ) -> Result { // 根必须是 Scene 标记,不能直接把 Mesh/Group 当作完整场景根使用。 if store.payload::(scene, "Scene3D")?.0 != NodeKind::Scene { return Err(invalid("Viewport3D requires a Scene3D")); } // 建立本轮精确字段观察器,数值快照与依赖来源在同一次遍历中形成。 let mut reader = Reader { store, reads: HashSet::new(), }; // 相机先独立复制/校验,复用旧世界场景也不能复用过时的相机值。 let camera = reader.camera(camera)?; // 读取场景环境光,数值解析只保证有限范围,下一步再限为 [0,1]。 let ambient = reader.number(scene, "ambient")?; // 拒绝非法环境光系数,不以渲染时钳色掩盖错误源参数。 if !(0.0..=1.0).contains(&ambient) { return Err(invalid("Scene3D.ambient expects 0 through 1")); } // 背景使用同一不透明三维颜色解码,与普通二维 UI alpha 规则区分。 let background = color(&text(&reader.value(scene, "background")?)?)?; // 构建本轮完整世界描述,暂不建立加速结构;最后若值相等可以复用旧 Scene/BVH。 let mut output = scene3d::Scene { meshes: Vec::new(), lights: Vec::new(), ambient, background, acceleration: Arc::default(), }; // 显式进入/退出栈保存节点、父世界矩阵、退出标记与路径 key;path 仅追踪祖先,因此允许共享子树多次出现。 let mut work = vec![(scene, Matrix::default(), false, 0u64)]; // 收集面板 key/UI 根/逻辑尺寸,顺序要与输出 Mesh 中的面板对应。 let mut panels = Vec::new(); // 当前祖先链用于拒绝组成环,退出后移除,允许共享业务节点在其他路径出现。 let mut path = HashSet::new(); // 实例预算按展开出现次数,重复业务对象同样消耗绘制工作。 let mut visits = 0; // 累计所有普通 Mesh 出现的三角数,Arc 共享不能绕开场景工作预算。 let mut triangles = 0; // 累计普通 Mesh 出现顶点数;面板与整体预算另由 View.valid 等检查。 let mut vertices = 0; // 显式 DFS 同时传递父世界矩阵与路径 key,不用宿主递归栈遍历深图。 while let Some((object, parent, finish, key)) = work.pop() { // 当前子树已完整展开,移除祖先标记以允许后续兄弟再次使用同一对象。 if finish { path.remove(&object); continue; } visits += 1; // 预算按展开出现次数,重复共享同一网格也消耗实例/顶点/三角形工作,不能只数不同对象身份。 if visits > 4096 { return Err(VmError::ExecutionLimit("3D scene occurrences")); } // 同一身份只在祖先路径重复时为环,不能全局去重而漏掉共享实例。 if !path.insert(object) { return Err(invalid("cyclic 3D scene graph")); } work.push((object, parent, true, key)); // 每项必须是托管 Node 标记,再按具体 NodeKind 限制可进入场景图的节点。 let kind = store.payload::(object, "3D node")?.0; // 仅可放置的 Mesh/Group/Panel 读取自身 TRS,其他种类继承父矩阵。 let world = if matches!(kind, NodeKind::Mesh | NodeKind::Group | NodeKind::Panel) { // 局部先缩放、旋转、平移,再乘父 world;Scene/Light 不新增 TRS,方向光仍受祖先 Group 变换。 parent.compose(Matrix::trs( reader.vector(object, ["x", "y", "z"])?, reader.vector(object, ["rotationX", "rotationY", "rotationZ"])?, reader.vector(object, ["scaleX", "scaleY", "scaleZ"])?, )) } else { parent }; // Group/Scene 压孩子,Mesh/Panel 产生绘制实例,Light 产生照明;其他类型明确拒绝。 match kind { NodeKind::Scene | NodeKind::Group => { // 通过 Reader 记录 children 属性引用,items 再验证它确为 ObjectList。 let list = object_ref(&reader.value(object, "children")?)?; // 逆序压栈保留列表原始遍历次序,每个子出现都继承当前 world。 for (index, child) in object_list::items(store, list)?.iter().enumerate().rev() { use std::hash::{Hash, Hasher}; // 路径哈希从父 key/下标/子身份构造,面板后续显式检测碰撞。 let mut hasher = std::collections::hash_map::DefaultHasher::new(); // 路径 key 包含父 key、当前孩子索引和托管身份,区分同一 Panel 节点在不同路径的出现。 // 此 key 与普通 Block slot 不同,场景列表重排会改变路径身份。 key.hash(&mut hasher); index.hash(&mut hasher); child.hash(&mut hasher); work.push((*child, world, false, hasher.finish())); } } NodeKind::Mesh => { // Mesh.geometry 字段读取建立依赖,随后要求其 payload 是不可变 Geometry。 let geometry = object_ref(&reader.value(object, "geometry")?)?; // 克隆原生 Arc 保活数值缓冲,renderer 不需要托管 geometry 句柄。 let geometry = store.payload::(geometry, "Geometry3D")?.0.clone(); // 每次 mesh 出现都累计完整几何,Arc 共享只节约存储,不减少本帧实例绘制工作预算。 triangles += geometry.triangles.len(); vertices += geometry.vertices.len(); // 顶点工作超过场景预算立即拒绝,不因为几何缓冲共享就少计实例成本。 if vertices > 262_144 { return Err(VmError::ExecutionLimit("3D scene vertices")); } // 三角工作量同样按实例累计,用独立上限防止大网格或大量共享网格超预算。 if triangles > scene3d::MAX_TRIANGLES { return Err(VmError::ExecutionLimit("3D scene triangles")); } // 材质字段每轮提取为 RGB,颜色改变可通过精确依赖驱动 Mesh 快照更新。 let material = color(&text(&reader.value(object, "color")?)?)?; output.meshes.push(scene3d::Mesh { identity: object.to_string(), geometry, world, color: material, panel: None, }); } NodeKind::Panel => { // Arrangement slots cross Endor's signed integer API. A raw // u64 hash can turn negative when native Bounds are created. // Bounds.slot 经有符号整数 API 传递,清最高位避免变负;仍显式检测碰撞,不能假设哈希必然唯一。 let key = key & i64::MAX as u64; // 清最高位后仍需按已收集的出现 key 检测碰撞;不能让两块 UI 共享同一个纹理/组成身份。 if panels.iter().any(|panel: &PanelMount| panel.key == key) { return Err(invalid("colliding 3D UI occurrence identities")); } // 有限面板根数量另限 64;每个面板还有独立像素分辨率限制和递归纹理总预算。 if panels.len() >= 64 { return Err(VmError::ExecutionLimit("3D UI panels")); } // 读取世界平面宽高,必须为正;它们决定场景几何,不是纹理像素尺寸。 let width = reader.number(object, "width")?; // 世界高度与宽度独立,不能以 uiHeight 替代。 let height = reader.number(object, "height")?; // UI 横向分辨率用有限整数逻辑像素,稍后作为面板 Block 布局宽度。 let ui_width = reader.number(object, "uiWidth")?; // UI 纵向分辨率单独检查整数和 1..2048 范围。 let ui_height = reader.number(object, "uiHeight")?; // 同一检查同时约束正世界尺寸与两轴 UI 整数分辨率,拒绝零面积面板。 if width <= 0. || height <= 0. || ![ui_width, ui_height] .iter() .all(|size| (1.0..=2048.0).contains(size) && size.fract() == 0.) { return Err(invalid( "Panel3D requires positive world dimensions and integer UI dimensions from 1 through 2048", )); } // 取当前 Panel.content 的真实 Block 引用,并验证可以沿共享转发解析。 let content = object_ref(&reader.value(object, "content")?)?; crate::block::resolve(store, content)?; // 面板底色也为三维不透明颜色,用于独立表面的清屏。 let background = color(&text(&reader.value(object, "background")?)?)?; // 面板平面以中心为原点,顶点 y 向上,UV 左上为 (0,0);两三角形共用四个角点。 let geometry = Arc::new(scene3d::Geometry { vertices: vec![ Vec3::new(-width / 2., height / 2., 0.), Vec3::new(width / 2., height / 2., 0.), Vec3::new(width / 2., -height / 2., 0.), Vec3::new(-width / 2., -height / 2., 0.), ], triangles: vec![[0, 1, 2], [0, 2, 3]], uv: vec![[0., 0.], [1., 0.], [1., 1.], [0., 1.]], }); output.meshes.push(scene3d::Mesh { identity: object.to_string(), geometry, world, color: [1., 1., 1.], panel: Some(scene3d::Panel { key, width: ui_width as u32, height: ui_height as u32, background, surface: None, }), }); // 同时把普通 UI 根交给宿主布局,scene Mesh 先只带分辨率/背景,surface 由后续 panel::compose 装配。 panels.push(PanelMount { key, content, size: crate::layout::Size { width: ui_width, height: ui_height, }, }); } NodeKind::Light => { // 方向光数量独立限制为 32,不由网格实例预算代替。 if output.lights.len() >= 32 { return Err(VmError::ExecutionLimit("3D scene lights")); } // 先读源方向向量,随后通过父线性变换并归一,不能让平移改变方向。 let direction = reader.vector(object, ["directionX", "directionY", "directionZ"])?; // 用两点之差撤销父矩阵平移,仅保留线性部分对方向的影响,再归一化;零方向拒绝。 let direction = (world.point(direction) - world.point(Vec3::default())) .normalized() .ok_or(invalid("light direction cannot be zero"))?; // 方向光颜色转原生 RGB,与强度分量分别检查。 let color = color(&text(&reader.value(object, "color")?)?)?; // 有限值还需限在 [0,32],避免把任意世界数值当作照明系数。 let intensity = reader.number(object, "intensity")?; // 超范围强度报告源错误,不能只在像素输出时饱和掩盖。 if !(0.0..=32.0).contains(&intensity) { return Err(invalid("light intensity expects 0 through 32")); } output.lights.push(scene3d::Light { direction, color, intensity, }); } _ => return Err(invalid("invalid node in 3D scene graph")), } } // 完整世界值相等时复用旧 Scene/BVH,单独换 camera 生成新 View;不相等才重建世界空间索引。 let scene = if let Some(scene) = reuse.filter(|scene| **scene == output) { scene } else { output.acceleration = Arc::new(scene3d::spatial::Index::build(&output.meshes)); Arc::new(output) }; // 将最终世界 Arc 与本轮相机组合为 View;renderer/拾取共享同一套原生场景描述。 let view = Arc::new(scene3d::View { scene, camera }); // 提取各项之后仍做完整视图校验,覆盖累计世界变换、光照、面板及整体资源不变量。 if !view.valid() { return Err(invalid("invalid 3D scene snapshot")); } Ok((view, reader.reads.into_iter().collect(), panels)) } /// 把视口中的指针射线结果转换成 RayHit3D,关联实际托管网格和交点数据。 pub fn pick( cx: &mut NativeContext<'_>, desktop: ObjectRef, mount: &crate::mount::Mount, point: crate::geometry::Point, ) -> Result { // 布局未提交时不允许射线查询,避免临时 Bounds 与旧视口/面板投影混用。 if mount.layout.is_some() { return Err(invalid("pick3D requires a committed viewport")); } // 先沿 Mount.block 的共享赋值解析当前描述,而非信任挂载时的旧快照。 let (_, data) = crate::block::resolve(cx.store(), mount.block)?; // 用当前托管属性覆盖副本,再验证实际种类确为 Viewport3D。 let data = crate::block::visual_data(cx.store(), data)?; // 普通 Block 或二维视口没有三维射线协议,明确拒绝而非返回任意 Nil。 if !matches!(data.kind, crate::block::Kind::Viewport3D { .. }) { return Err(invalid("pick3D requires a Viewport3D")); } // 先做出现/面板遮挡和裁剪命中,未命中直接 Nil;即使场景射线有交点也不能点穿外层遮挡。 if !mount.geometry.contains(point) { return Ok(Value::Nil); } // 为计算当前盒模型 inset 提取原生视觉根,射线归一化必须使用实际内容区域。 let node = crate::block::snapshot(cx.store(), mount.block)?; // padding/border 从有效视觉样式取得,不能把整个边框盒宽高直接作为相机视口。 let inset = crate::layout::insets(&node); // 先逆投影面板/二维 view 到布局坐标;遮挡或不可逆时返回无命中。 let Some(point) = mount.geometry.logical_point(point) else { return Ok(Value::Nil); }; // 从布局盒扣 inset 得到实际内容视口,再把逻辑点归一化;零面积视口没有有效投影。 let (width, height) = ( (mount.geometry.width - inset.horizontal()).max(0.), (mount.geometry.height - inset.vertical()).max(0.), ); // 零内容面积没有可定义的视口归一化坐标,提前返回 Nil 避免除零。 if width <= 0. || height <= 0. { return Ok(Value::Nil); } // 本次重新提取场景与相机,并保留实际字段读取对象用于关联原托管 Mesh。 let (view, reads, _) = snapshot( cx.store(), data.scene3d.ok_or(invalid("missing Scene3D"))?, data.camera.ok_or(invalid("missing 3D camera"))?, )?; // 减去 Mount 布局原点与内容 inset,再归一到视口;宽高比用于相机投影。 let hit = view.pick( (point.x - mount.geometry.x - inset.left) / width, (point.y - mount.geometry.y - inset.top) / height, width / height, ); // 射线没有穿过可见几何时正常返回 Nil,不创建伪造 RayHit3D。 let Some(hit) = hit else { return Ok(Value::Nil); }; // 原生 Hit.identity 只带文本,通过本次实际读取的托管身份重新关联节点;渲染器本身从不解引用 VM 对象。 let mesh = reads .iter() .find_map(|(object, _)| (object.to_string() == hit.identity).then_some(*object)) .ok_or(invalid("picked mesh is unavailable"))?; // 创建本桌面的名义 RayHit3D,mesh/node 属性强持有原业务对象。 let result = cx.new_object()?; crate::ui_bridge::attach_desktop_type(cx, result, "RayHit3D", desktop)?; cx.set_attr(result, "mesh", Value::Object(mesh))?; cx.set_attr(result, "node", Value::Object(mesh))?; cx.set_attr( result, "instance", Value::Number((hit.instance as i64).into()), )?; cx.set_attr( result, "distance", crate::native_visual::value(hit.distance), )?; cx.set_attr( result, "triangle", Value::Number((hit.triangle as i64).into()), )?; // 交点、法线、重心权重都包装为名义 Vec3,结果对象通过普通属性强持有它们。 for (name, vector) in [ ("point", hit.point), ("normal", hit.normal), ( "barycentric", Vec3::new(hit.barycentric[0], hit.barycentric[1], hit.barycentric[2]), ), ] { // 结果向量字段须为本桌面 Vec3 名义类型,不能只返回相似的普通 xyz 记录。 let definition = crate::ui_bridge::desktop_type(cx.store(), "Vec3", desktop)?; // 按相同构造规则生成交点/法线/重心记录,结果属性负责持有这些返回对象。 let vector = allocate( cx, desktop, definition, Constructor::Vec3, &[ crate::native_visual::value(vector.x), crate::native_visual::value(vector.y), crate::native_visual::value(vector.z), ], )?; cx.set_attr(result, name, Value::Object(vector))?; } Ok(Value::Object(result)) } #[derive(Debug)] /// 指针拾取的中间结果,保留交点与所属实例信息供 Event/RayHit3D 使用。 pub struct PointerPick { // 原生指针目标的托管 Mount,由调用方包装的 captures 保活。 pub mount: ObjectRef, // 当前窗口桌面,用来构造 Event/RayHit3D 名义值与派发。 pub desktop: ObjectRef, // 原输入的页面屏幕坐标,invoke 再按当前 Mount 投影执行精确拾取。 pub point: crate::geometry::Point, } /// The default controller is replaceable; camera data has no dependency on it. /// 默认 OrbitController 根据已解码的 CameraGesture 更新相机,处理轨道、平移和缩放;可由源控制器替换。 pub fn control( cx: &mut NativeContext<'_>, controller: ObjectRef, camera_ref: ObjectRef, gesture: ObjectRef, ) -> Result<(), VmError> { // 只处理默认 OrbitController 标记;自定义源控制器由桥接协议调用自己的 update。 if cx.store().payload::(controller, "OrbitController")?.0 != NodeKind::Orbit { return Err(invalid("invalid orbit controller")); } // 控制器复用字段解码器以取得一致的数值检查;这里的 reads 只为本次读取使用,不返回视觉缓存。 let mut reader = Reader { store: cx.store(), reads: HashSet::new(), }; // 先复制并验证相机观察基,再基于已知有效状态计算新位置/target。 let camera = reader.camera(camera_ref)?; // 手势逻辑横位移,由窗口/面板映射后的 CameraGesture 提供。 let dx = reader.number(gesture, "dx")?; // 手势逻辑纵位移,平移计算会显式处理屏幕 y 向下的方向。 let dy = reader.number(gesture, "dy")?; // 策略字符串决定 orbit/pan/zoom,未知手势不能默认为某种运动。 let kind = text(&reader.value(gesture, "kind")?)?; // 默认轨道控制围绕 target 使用 position-target 偏移和半径;相机有效性保证观察方向不退化。 let offset = camera.position - camera.target; // position−target 的长度为轨道半径,相机有效性已排除零观察方向。 let radius = offset.length(); // 先只在原生数值副本中计算,最后统一检查世界预算并写回。 let mut position = camera.position; // orbit 通常固定目标,pan 同时移动目标;变量副本与源属性暂时分离。 let mut target = camera.target; // 分支各自检查速度/倍率,计算结束再共享写回位置与目标。 match kind.as_str() { // 围绕相机 target 旋转位置,使用源 up 向量建立轨道基,不强制世界固定 y 轴。 "scene-orbit" => { // 旋转速度单位为手势像素到弧度系数,必须在 [0,1] 内。 let speed = reader.number(controller, "rotationSpeed")?; // 非法速度拒绝整个手势,不先修改某一轴再报错。 if !(0.0..=1.0).contains(&speed) { return Err(invalid("rotationSpeed expects 0 through 1")); } // 归一相机 up,若退化则无法建立稳定轨道水平面。 let up = camera .up .normalized() .ok_or(invalid("camera up cannot be zero"))?; // 将偏移分解为 up 方向和水平面方向,用水平旋转更新 yaw、asin(vertical) 更新 pitch。 let vertical = (offset.dot(up) / radius).clamp(-1., 1.); // 去除 up 分量后归一,位于极点时无法定义 yaw 水平基,明确要求更换 up。 let horizontal = (offset - up * offset.dot(up)) .normalized() .ok_or(invalid("orbit at pole requires a different up vector"))?; // 横位移乘速度形成 yaw 弧度,sin_cos 用同一角度提供旋转系数。 let (sine, cosine) = (dx * speed).sin_cos(); // 用 up×horizontal 生成绕 up 的正交旋转分量,保留水平单位方向。 let horizontal = horizontal * cosine + up.cross(horizontal) * sine; // 俯仰限制在两极内侧 0.01 弧度,避免绕极点时水平基退化并导致轨道跳变。 let pitch = (vertical.asin() + dy * speed).clamp( -std::f32::consts::FRAC_PI_2 + 0.01, std::f32::consts::FRAC_PI_2 - 0.01, ); position = target + horizontal * (radius * pitch.cos()) + up * (radius * pitch.sin()); } // 沿当前相机 right/up 平移,位置和目标加同一 delta,保持朝向与半径。 "scene-pan" => { // 平移速度可取 [0,100],与旋转速度使用不同范围和单位。 let speed = reader.number(controller, "panSpeed")?; // 速度超范围时拒绝,不能把任意源数字当成无限世界平移系数。 if !(0.0..=100.0).contains(&speed) { return Err(invalid("panSpeed expects 0 through 100")); } // 手势视口高至少按 1 处理,避免像素→世界比例计算除零。 let height = reader.number(gesture, "height")?.max(1.); // 把逻辑像素位移换成世界单位:透视按 target 距离处的可见高度,正交按固定投影高度。 let units = match camera.projection { // 透视按当前 target 距离截面的可见高度,把屏幕位移换算为世界单位。 Projection::Perspective { fov } => { 2. * radius * (fov.to_radians() / 2.).tan() / height } // 正交可见高度固定,世界单位/像素与轨道距离无关。 Projection::Orthographic { height: extent } => extent / height, }; // 使用相机实际观察基的 right/up,不把屏幕平移误映射成世界固定 xyz。 let basis = camera.basis().ok_or(invalid("invalid camera basis"))?; // 横位移沿 right,屏幕向下 dy 对应负 up,最后再乘用户 panSpeed。 let delta = basis.right * (dx * units * speed) + basis.up * (-dy * units * speed); // 平移同时移动 position 和 target,保留观察方向与轨道半径;屏幕 y 向下所以 up 分量取负。 position = position + delta; target = target + delta; } // 倍率大于一拉近:透视缩短轨道距离,正交则缩小投影可见高度。 "scene-zoom" => { // 倍率从 CameraGesture 读取,必须为正且处于约定 [0.1,10]。 let factor = reader.number(gesture, "factor")?; // 拒绝零/负/过大倍率,后续无需为反向或无限缩放定义额外语义。 if !(0.1..=10.0).contains(&factor) { return Err(invalid("zoom factor expects 0.1 through 10")); } // 正交缩放只改投影高度,透视缩放改变到 target 的距离;倍率大于一均表现为拉近。 if let Projection::Orthographic { height } = camera.projection { set_number( cx, camera_ref, "height", (height / factor).clamp(0.001, 1_000_000.), )?; } else { // 透视新距离夹在近裁面两倍与 far 边界之间,再沿原 offset 方向缩放。 let next = (radius / factor).clamp(camera.near * 2., camera.far.max(camera.near * 2.)); position = target + offset * (next / radius); } } _ => return Err(invalid("unsupported 3D camera gesture")), } // 写回前校验整个计算结果,拒绝超世界范围;控制器通过普通属性更新让后续快照依赖观察变化。 if !position.valid() || !target.valid() { return Err(invalid("camera movement exceeds world budget")); } // 位置与 target 逐轴经 NativeContext 写回,变化日志让下一视觉快照观察到更新。 for (name, value) in [ ("x", position.x), ("y", position.y), ("z", position.z), ("targetX", target.x), ("targetY", target.y), ("targetZ", target.z), ] { set_number(cx, camera_ref, name, value)?; } Ok(()) } /// 对允许的 3D 节点累计平移或角度旋转;相机平移同时更新 target,旋转值归一到 [0,360)。 pub fn transform( cx: &mut NativeContext<'_>, owner: ObjectRef, args: &[Value], rotate: bool, ) -> Result<(), VmError> { // 桥接 translate/rotate 入口先根据 payload 判定可放置目标,不按字段形状接受普通 Object。 let kind = cx.store().payload::(owner, "3D transform target")?.0; // 相机可做位置平移,但本接口不以网格欧拉角方式旋转相机。 let camera = matches!(kind, NodeKind::Perspective | NodeKind::Orthographic); // 限制 Mesh/Group/Panel/相机这组明确目标,额外拒绝 rotate+camera 组合。 if !matches!( kind, NodeKind::Mesh | NodeKind::Group | NodeKind::Panel | NodeKind::Perspective | NodeKind::Orthographic ) || rotate && camera { return Err(invalid("unsupported 3D transform target")); } // 旋转增量作用欧拉角,平移增量作用位置;相机不接受本接口的欧拉角旋转。 let names = if rotate { ["rotationX", "rotationY", "rotationZ"] } else { ["x", "y", "z"] }; // 先收集并验证全部轴再写入,避免验证到第二/三轴才发现越界时已修改前面轴。 let mut updates = Vec::new(); // 按固定 xyz 顺序读取三轴增量,桥接调用方已经检查了实参个数。 for (index, name) in names.into_iter().enumerate() { // 每个增量先按带符号有限坐标规则解析,不能因最终角度归一而接受 NaN。 let delta = number(&args[index])?; // 把增量加到原字段副本,仍未写入源对象。 let value = number(&cx.store().get_attr(owner, name)?)? + delta; // 相加后也需检查有限性;平移值还必须留在世界范围,旋转随后归一角度。 if !value.is_finite() || !rotate && value.abs() > 1_000_000. { return Err(invalid("3D transform exceeds world budget")); } updates.push(( name, // 只有旋转应用 rem_euclid(360),平移仍保留带符号世界坐标。 if rotate { // 角度归一到 [0,360),支持负增量而不使用产生负余数的普通取模。 value.rem_euclid(360.) } else { value }, )); // 相机平移同步改 target,保留朝向;不把“移动摄像机”意外变成绕固定目标转动。 if camera { // 相机平移还需同步相应 target 轴,不能移动位置却把观察目标固定不动。 let name = ["targetX", "targetY", "targetZ"][index]; // 读取 target 旧值加相同增量,与 position 一起先收集验证。 let value = number(&cx.store().get_attr(owner, name)?)? + delta; // target 越界时也拒绝全部更新,不能只回滚相机位置而保留一部分目标写入。 if value.abs() > 1_000_000. { return Err(invalid("camera target exceeds world budget")); } updates.push((name, value)); } } // 所有轴验证通过后才执行普通字段写入;参数检查阶段不会产生部分轴位移。 for (name, value) in updates { set_number(cx, owner, name, value)?; } Ok(()) } impl endor_core::NativeCallable for PointerPick { /// 输入 Mount 与点击点已捕获,拾取/分发准备步骤不再接受实参。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// 在当前 Mount 中重新射线拾取,构造含真实 mesh 命中的 click Event,再交托管事件系统分发。 fn invoke( &self, cx: &mut NativeContext<'_>, _: &[Value], ) -> Result { // VM 序列执行此适配器时重读当前 Mount payload,不使用窗口线程提交的旧业务快照。 let mount = cx .store() .payload::(self.mount, "Mount")? .clone(); // 先从当前几何/场景做精确射线拾取,结果保留原业务 Mesh 身份或 Nil。 let hit = pick(cx, self.desktop, &mount, self.point)?; // 点击 Event 使用同桌面的名义类型,避免拾取后转为无契约的临时记录。 let definition = crate::ui_bridge::desktop_type(cx.store(), "Event", self.desktop)?; // 创建真实 click Event,再写入 hit 字段并交统一事件派发处理传播与监听器。 let event = crate::event_dispatch::new_event(cx, definition, "click")?; // 点击事件把精确拾取值放到 hit(未击中则 Nil),仍以 Viewport 的 Block/Mount 作为事件传播目标。 cx.set_attr(event, "hit", hit)?; crate::event_dispatch::dispatch(cx, mount.block, event, Some(self.mount), self.desktop) } }