Files
endorphin-tessera-gui/src/model/scene3d.rs
T

1136 lines
57 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.
//! 将 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> {
// 只在显式注册的构造器集合里反查,未知名字交给其他桥接分支处理。
[
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<scene3d::Geometry>);
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<ObjectRef, VmError> {
// 构造与快照共用对象边界检查,只有真正托管对象才能交给 payload/列表读取。
if let Value::Object(object) = value {
Ok(*object)
} else {
Err(invalid("3D field requires an object"))
}
}
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
fn number(value: &Value) -> Result<f32, VmError> {
crate::scroll::coordinate(value)
}
/// 把当前源值或文本选择解码成此调用需要的文本表示。
fn text(value: &Value) -> Result<String, VmError> {
// 材质、背景和手势种类只接受源文本,不把对象 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<ObjectRef, VmError> {
// 入口假设桥接工厂已按 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::<Node>(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::<Result<Vec<_>, 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::<Node>(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::<Result<Vec<_>, 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>(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::<Result<Vec<_>, _>>()?;
// 每个成员都必须是能放进场景的节点;相机和控制器不是可绘制组成孩子。
for child in &children {
// 检查 NodeKind 而非仅检查共同的 Node payload,因为相机/向量也使用该 payload。
if !matches!(
cx.store().payload::<Node>(*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<Value, VmError> {
// 读取前登记精确对象/属性依赖,HashSet 去重;返回给视觉缓存后,相关字段变化才能驱动重建。
self.reads.insert((object, name.into()));
self.store.get_attr(object, name)
}
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
fn number(&mut self, object: ObjectRef, name: &str) -> Result<f32, VmError> {
number(&self.value(object, name)?)
}
/// 连续读取三个数值字段,组成位置、旋转或缩放向量。
fn vector(&mut self, object: ObjectRef, names: [&str; 3]) -> Result<Vec3, VmError> {
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<Camera, VmError> {
// 先验证宿主相机种类,再按投影策略读取 fov 或 height;不把有同名字段的对象当相机。
let projection = match self.store.payload::<Node>(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<scene3d::View>,
Vec<(ObjectRef, String)>,
Vec<PanelMount>,
);
/// 将当前托管或可变状态提取为后续阶段使用的派生表示。
pub fn snapshot(
store: &ObjectStore,
scene: ObjectRef,
camera: ObjectRef,
) -> Result<Snapshot, VmError> {
// 一次性提取入口无旧 Scene/BVH 可复用;pick 使用此路径获得本次实际字段读取。
snapshot_reusing(store, scene, camera, None)
}
/// 遍历场景并记录字段依赖;如果世界数据未变则复用不可变场景和 BVH,只替换摄像机视图。
pub fn snapshot_reusing(
store: &ObjectStore,
scene: ObjectRef,
camera: ObjectRef,
reuse: Option<Arc<scene3d::Scene>>,
) -> Result<Snapshot, VmError> {
// 根必须是 Scene 标记,不能直接把 Mesh/Group 当作完整场景根使用。
if store.payload::<Node>(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::<Node>(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>(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<Value, VmError> {
// 布局未提交时不允许射线查询,避免临时 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::<Node>(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::<Node>(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<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 在当前 Mount 中重新射线拾取,构造含真实 mesh 命中的 click Event,再交托管事件系统分发。
fn invoke(
&self,
cx: &mut NativeContext<'_>,
_: &[Value],
) -> Result<endor_core::NativeResult, VmError> {
// VM 序列执行此适配器时重读当前 Mount payload,不使用窗口线程提交的旧业务快照。
let mount = cx
.store()
.payload::<crate::mount::Mount>(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)
}
}