docs: explain managed 3d construction, scene extraction and camera updates
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
//! 构造器和字段仍属于 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::{
|
||||
@@ -17,22 +21,35 @@ 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",
|
||||
@@ -50,6 +67,7 @@ impl Constructor {
|
||||
}
|
||||
/// 取得本构造器对应的源工厂函数名称。
|
||||
pub fn function(self) -> &'static str {
|
||||
// 桥接函数路由按这些公开工厂名识别构造器,与 name 的名义类型标识一一对应。
|
||||
match self {
|
||||
Self::Vec3 => "vec3",
|
||||
Self::Triangle => "triangle3D",
|
||||
@@ -67,6 +85,7 @@ impl Constructor {
|
||||
}
|
||||
/// 反查源构造器名称对应的原生 3D 种类,不认识的名称返回空值。
|
||||
pub fn from_function(name: &str) -> Option<Self> {
|
||||
// 只在显式注册的构造器集合里反查,未知名字交给其他桥接分支处理。
|
||||
[
|
||||
Self::Vec3,
|
||||
Self::Triangle,
|
||||
@@ -99,6 +118,7 @@ impl Constructor {
|
||||
}
|
||||
/// 列出本构造器的参数类型名称,供签名安装与源调用检查使用。
|
||||
pub fn parameters(self) -> &'static [&'static str] {
|
||||
// 安装参数注解时按显式实参顺序提供类型;Group/Scene 的可变节点规则另由 variadic/arity 解释。
|
||||
match self {
|
||||
Self::Vec3 | Self::Triangle => &["Number", "Number", "Number"],
|
||||
Self::Geometry => &["ObjectList", "ObjectList"],
|
||||
@@ -177,6 +197,7 @@ fn invalid(message: &'static str) -> VmError {
|
||||
}
|
||||
/// 把源 Value 转换为受检查的托管对象引用,拒绝非对象输入。
|
||||
fn object_ref(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
// 构造与快照共用对象边界检查,只有真正托管对象才能交给 payload/列表读取。
|
||||
if let Value::Object(object) = value {
|
||||
Ok(*object)
|
||||
} else {
|
||||
@@ -189,6 +210,7 @@ fn number(value: &Value) -> Result<f32, VmError> {
|
||||
}
|
||||
/// 把当前源值或文本选择解码成此调用需要的文本表示。
|
||||
fn text(value: &Value) -> Result<String, VmError> {
|
||||
// 材质、背景和手势种类只接受源文本,不把对象 repr 隐式转换为策略或颜色。
|
||||
if let Value::Text(text) = value {
|
||||
Ok(text.clone())
|
||||
} else {
|
||||
@@ -202,12 +224,14 @@ fn set_number(
|
||||
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"));
|
||||
}
|
||||
@@ -238,12 +262,16 @@ pub fn allocate(
|
||||
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"));
|
||||
}
|
||||
@@ -254,16 +282,21 @@ pub fn allocate(
|
||||
))
|
||||
})
|
||||
.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"));
|
||||
}
|
||||
@@ -272,6 +305,7 @@ pub fn allocate(
|
||||
Ok(triangle)
|
||||
})
|
||||
.collect::<Result<Vec<_>, VmError>>()?;
|
||||
// 将两份复制后的数组组成无 UV 的自定义几何;顶点/三角预算在 valid 统一检查。
|
||||
let geometry = scene3d::Geometry {
|
||||
vertices,
|
||||
triangles,
|
||||
@@ -283,15 +317,20 @@ pub fn allocate(
|
||||
"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 {
|
||||
@@ -303,13 +342,17 @@ pub fn allocate(
|
||||
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)?;
|
||||
@@ -324,8 +367,11 @@ pub fn allocate(
|
||||
}
|
||||
// 孩子只接受 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
|
||||
@@ -338,12 +384,15 @@ pub fn allocate(
|
||||
// 托管 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)
|
||||
@@ -356,6 +405,7 @@ pub fn allocate(
|
||||
}
|
||||
// 世界宽高与 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])?)?;
|
||||
@@ -365,7 +415,9 @@ pub fn allocate(
|
||||
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.),
|
||||
@@ -384,6 +436,7 @@ pub fn allocate(
|
||||
set_number(cx, instance, name, value)?;
|
||||
}
|
||||
}
|
||||
// 控制器仅初始化转速与平移速度;不持有相机,源可替换控制器对象。
|
||||
NodeKind::Orbit => {
|
||||
set_number(cx, instance, "rotationSpeed", 0.005)?;
|
||||
set_number(cx, instance, "panSpeed", 1.)?;
|
||||
@@ -391,6 +444,7 @@ pub fn allocate(
|
||||
}
|
||||
// 只有可放置节点安装 TRS;相机用 position/target/up,方向光用方向向量,不复用网格欧拉角字段。
|
||||
if matches!(kind, NodeKind::Mesh | NodeKind::Group | NodeKind::Panel) {
|
||||
// Mesh/Group/Panel 统一从单位缩放、零位置/角度开始,世界矩阵在快照遍历时组合。
|
||||
for (name, value) in [
|
||||
("x", 0.),
|
||||
("y", 0.),
|
||||
@@ -410,7 +464,9 @@ pub fn allocate(
|
||||
|
||||
/// 快照字段读取器,同时记录实际观察到的对象/属性依赖。
|
||||
struct Reader<'a> {
|
||||
// 借用当前 VM 堆,Reader 只在同步快照/控制器调用内存活,不跨线程。
|
||||
store: &'a ObjectStore,
|
||||
// 精确对象/字段键集合,返回 RenderCache 建反向依赖;集合不 retain 身份。
|
||||
reads: HashSet<(ObjectRef, String)>,
|
||||
}
|
||||
impl Reader<'_> {
|
||||
@@ -448,6 +504,7 @@ impl Reader<'_> {
|
||||
));
|
||||
}
|
||||
};
|
||||
// 将相机所有输入复制为原生数值,后续 valid 同时验证观察基与投影近远范围。
|
||||
let camera = Camera {
|
||||
position: self.vector(object, ["x", "y", "z"])?,
|
||||
target: self.vector(object, ["targetX", "targetY", "targetZ"])?,
|
||||
@@ -456,6 +513,7 @@ impl Reader<'_> {
|
||||
far: self.number(object, "far")?,
|
||||
projection,
|
||||
};
|
||||
// 单字段有限不代表组合有效;位置=目标、退化 up 或非法投影范围必须整体拒绝。
|
||||
if !camera.valid() {
|
||||
return Err(invalid("invalid 3D camera basis or projection range"));
|
||||
}
|
||||
@@ -465,8 +523,11 @@ impl Reader<'_> {
|
||||
|
||||
/// 从场景中提取的有限 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 根。
|
||||
@@ -481,6 +542,7 @@ pub fn snapshot(
|
||||
scene: ObjectRef,
|
||||
camera: ObjectRef,
|
||||
) -> Result<Snapshot, VmError> {
|
||||
// 一次性提取入口无旧 Scene/BVH 可复用;pick 使用此路径获得本次实际字段读取。
|
||||
snapshot_reusing(store, scene, camera, None)
|
||||
}
|
||||
/// 遍历场景并记录字段依赖;如果世界数据未变则复用不可变场景和 BVH,只替换摄像机视图。
|
||||
@@ -490,19 +552,26 @@ pub fn snapshot_reusing(
|
||||
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(),
|
||||
@@ -512,12 +581,19 @@ pub fn snapshot_reusing(
|
||||
};
|
||||
// 显式进入/退出栈保存节点、父世界矩阵、退出标记与路径 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;
|
||||
@@ -527,11 +603,14 @@ pub fn snapshot_reusing(
|
||||
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(
|
||||
@@ -542,11 +621,15 @@ pub fn snapshot_reusing(
|
||||
} 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 不同,场景列表重排会改变路径身份。
|
||||
@@ -557,17 +640,22 @@ pub fn snapshot_reusing(
|
||||
}
|
||||
}
|
||||
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(),
|
||||
@@ -582,6 +670,7 @@ pub fn snapshot_reusing(
|
||||
// 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"));
|
||||
}
|
||||
@@ -589,10 +678,15 @@ pub fn snapshot_reusing(
|
||||
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]
|
||||
@@ -603,8 +697,10 @@ pub fn snapshot_reusing(
|
||||
"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 {
|
||||
@@ -641,17 +737,22 @@ pub fn snapshot_reusing(
|
||||
});
|
||||
}
|
||||
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"));
|
||||
}
|
||||
@@ -671,6 +772,7 @@ pub fn snapshot_reusing(
|
||||
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() {
|
||||
@@ -686,11 +788,15 @@ pub fn pick(
|
||||
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"));
|
||||
}
|
||||
@@ -698,8 +804,11 @@ pub fn pick(
|
||||
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);
|
||||
};
|
||||
@@ -708,19 +817,23 @@ pub fn pick(
|
||||
(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);
|
||||
};
|
||||
@@ -729,6 +842,7 @@ pub fn pick(
|
||||
.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))?;
|
||||
@@ -757,7 +871,9 @@ pub fn pick(
|
||||
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,
|
||||
@@ -777,8 +893,11 @@ pub fn pick(
|
||||
#[derive(Debug)]
|
||||
/// 指针拾取的中间结果,保留交点与所属实例信息供 Event/RayHit3D 使用。
|
||||
pub struct PointerPick {
|
||||
// 原生指针目标的托管 Mount,由调用方包装的 captures 保活。
|
||||
pub mount: ObjectRef,
|
||||
// 当前窗口桌面,用来构造 Event/RayHit3D 名义值与派发。
|
||||
pub desktop: ObjectRef,
|
||||
// 原输入的页面屏幕坐标,invoke 再按当前 Mount 投影执行精确拾取。
|
||||
pub point: crate::geometry::Point,
|
||||
}
|
||||
|
||||
@@ -790,38 +909,55 @@ pub fn control(
|
||||
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(
|
||||
@@ -830,27 +966,38 @@ pub fn control(
|
||||
);
|
||||
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"));
|
||||
}
|
||||
@@ -863,6 +1010,7 @@ pub fn control(
|
||||
(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);
|
||||
@@ -874,6 +1022,7 @@ pub fn control(
|
||||
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),
|
||||
@@ -894,8 +1043,11 @@ pub fn transform(
|
||||
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
|
||||
@@ -915,14 +1067,19 @@ pub fn transform(
|
||||
};
|
||||
// 先收集并验证全部轴再写入,避免验证到第二/三轴才发现越界时已修改前面轴。
|
||||
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.)
|
||||
@@ -932,14 +1089,18 @@ pub fn transform(
|
||||
));
|
||||
// 相机平移同步改 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)?;
|
||||
}
|
||||
@@ -956,12 +1117,16 @@ impl endor_core::NativeCallable for PointerPick {
|
||||
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)?;
|
||||
|
||||
Reference in New Issue
Block a user