docs: explain Tessera modules and execution flows in Chinese
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
//! 有限布局盒子的尺寸策略。区分 auto、固定值和 fill,并处理 min/max 对约束与最终尺寸的限制。
|
||||
//! margin、border、padding 的空间分配由布局层结合本模块完成;场景世界范围不被当成无限布局盒子。
|
||||
//!
|
||||
//! Native border-box sizing. An unbounded axis cannot resolve `fill`.
|
||||
use crate::{layout::Size, ui_bridge::MAX_DIMENSION};
|
||||
use anyhow::Result;
|
||||
use endor_core::{Value, VmError};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
/// 一个布局轴的尺寸策略:随内容、固定值或占用有限可用空间。
|
||||
pub enum Dimension {
|
||||
#[default]
|
||||
Auto,
|
||||
@@ -12,6 +16,7 @@ pub enum Dimension {
|
||||
}
|
||||
|
||||
impl Dimension {
|
||||
/// 把内部尺寸策略投影为相应的源属性表示。
|
||||
pub fn value(self) -> Value {
|
||||
match self {
|
||||
Self::Auto => Value::Text("auto".into()),
|
||||
@@ -20,6 +25,7 @@ impl Dimension {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(value: &Value) -> Result<Self, VmError> {
|
||||
match value {
|
||||
Value::Text(text) if text == "auto" => Ok(Self::Auto),
|
||||
@@ -43,6 +49,7 @@ impl Dimension {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// border-box 尺寸策略与双轴上下限,实际求解仍要结合父级约束。
|
||||
pub struct Sizing {
|
||||
pub width: Dimension,
|
||||
pub height: Dimension,
|
||||
@@ -53,6 +60,7 @@ pub struct Sizing {
|
||||
}
|
||||
|
||||
impl Default for Sizing {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: Dimension::Auto,
|
||||
@@ -67,6 +75,7 @@ impl Default for Sizing {
|
||||
|
||||
impl Sizing {
|
||||
/// Constrain descendants using the actual finite box before wrapping text.
|
||||
/// 根据固定/auto/fill 与 min/max 规则细化父级提供的可用约束。
|
||||
pub fn available(self, mut constraints: [f32; 4]) -> [f32; 4] {
|
||||
constraints[2] = constraints[2].min(self.max_width);
|
||||
constraints[3] = constraints[3].min(self.max_height);
|
||||
@@ -81,7 +90,9 @@ impl Sizing {
|
||||
constraints
|
||||
}
|
||||
|
||||
/// 结合内容期望尺寸、轴策略和父级约束求最终盒子尺寸;无界轴不能解析 fill。
|
||||
pub fn resolve(self, intrinsic: Size, constraints: [f32; 4]) -> Result<Size> {
|
||||
/// 针对单个尺寸轴应用约束与边界规则,供双轴尺寸求解复用。
|
||||
fn axis(
|
||||
policy: Dimension,
|
||||
intrinsic: f32,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//! 面向设备像素的绘制指令集合。它承载矩形、文字、裁剪与 3D 表面等原语,不直接操作 VM。
|
||||
//! 平移和缩放统一作用于命令几何;文字样式和裁剪也要使用一致的设备比例。
|
||||
//!
|
||||
use crate::geometry::{Point, Rect};
|
||||
use crate::text::TextStyle;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 设备绘制使用的 RGBA 字节通道,区别于源代码的颜色文本。
|
||||
pub struct Color {
|
||||
pub red: u8,
|
||||
pub green: u8,
|
||||
@@ -10,6 +14,7 @@ pub struct Color {
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// 从 RGB 通道构造不透明颜色值。
|
||||
pub const fn rgb(red: u8, green: u8, blue: u8) -> Self {
|
||||
Self {
|
||||
red,
|
||||
@@ -19,6 +24,7 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 RGBA 通道构造带透明度的颜色值。
|
||||
pub const fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
|
||||
Self {
|
||||
red,
|
||||
@@ -30,6 +36,7 @@ impl Color {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// 原生画布最终顺序执行的绘制原语,已经使用对应设备比例。
|
||||
pub enum DrawCommand {
|
||||
Scene3D {
|
||||
rect: Rect,
|
||||
@@ -64,6 +71,7 @@ pub enum DrawCommand {
|
||||
}
|
||||
|
||||
impl DrawCommand {
|
||||
/// 返回平移后的绘制命令,并同步移动命令中的相关几何。
|
||||
pub fn translated(mut self, delta: Point) -> Self {
|
||||
match &mut self {
|
||||
Self::WrappedText { rect, .. }
|
||||
@@ -85,11 +93,13 @@ impl DrawCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 有序绘制原语容器;命令顺序同时约束叠放和裁剪栈行为。
|
||||
pub struct DisplayList {
|
||||
commands: Vec<DrawCommand>,
|
||||
}
|
||||
|
||||
impl DisplayList {
|
||||
/// 按设备比例缩放命令,文字、边框和矩形不能分别使用不一致比例。
|
||||
pub fn scale(&mut self, scale: f32) {
|
||||
for command in &mut self.commands {
|
||||
let rect = match command {
|
||||
@@ -129,10 +139,12 @@ impl DisplayList {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 按绘制顺序追加一条原语,后加入的内容在合成顺序中位于后面。
|
||||
pub fn push(&mut self, command: DrawCommand) {
|
||||
self.commands.push(command);
|
||||
}
|
||||
|
||||
/// 平移当前显示列表中的命令,供整块页面或管理面板放入窗口坐标。
|
||||
pub fn translate(&mut self, delta: Point) {
|
||||
for command in &mut self.commands {
|
||||
match command {
|
||||
@@ -153,10 +165,12 @@ impl DisplayList {
|
||||
}
|
||||
}
|
||||
|
||||
/// 借用按绘制顺序排列的原生原语,不复制命令集合。
|
||||
pub fn commands(&self) -> &[DrawCommand] {
|
||||
&self.commands
|
||||
}
|
||||
|
||||
/// 按原顺序合并另一组绘制命令,不在这里重排层次。
|
||||
pub fn extend(&mut self, other: DisplayList) {
|
||||
self.commands.extend(other.commands);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
//! 共享的二维点和矩形值。命中判断在调用方完成坐标系转换后使用,矩形本身不隐式处理相机。
|
||||
//!
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 二维坐标值;具体处于逻辑、局部还是设备空间由调用上下文决定。
|
||||
pub struct Point {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
/// 轴对齐矩形,保存原点和宽高,用于布局、裁剪与命中。
|
||||
pub struct Rect {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
@@ -19,6 +24,7 @@ pub struct Rect {
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
|
||||
Self {
|
||||
x,
|
||||
@@ -28,6 +34,7 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断给定点是否位于目标范围;需要相机转换的类型会先转换再判断。
|
||||
pub fn contains(self, point: Point) -> bool {
|
||||
point.x >= self.x
|
||||
&& point.x <= self.x + self.width
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 原生布局求解器:自底向上测量期望尺寸,再自顶向下分配位置和实际尺寸。
|
||||
//! Row/Column/Overlay、边距、边框、内边距、对齐和弹性空间都在有限约束内计算;过大的尺寸和坐标返回错误。
|
||||
//! 缓存按组件形状、子项尺寸和约束判断可复用性。frame 先在克隆状态中求解,成功才替换运行状态,防止失败污染已发布几何。
|
||||
//!
|
||||
//! Iterative measure/arrange/paint over derived visual data. Every occurrence
|
||||
//! has its own geometry; measurement caches never become semantic object state.
|
||||
use crate::{
|
||||
@@ -11,6 +15,7 @@ use std::collections::{HashMap, HashSet};
|
||||
const MAX_LAYOUT_VISITS: usize = 10_000;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
/// 上右下左四边的空间占用,用于 margin、padding 和 border 计算。
|
||||
pub struct Insets {
|
||||
pub top: f32,
|
||||
pub right: f32,
|
||||
@@ -19,12 +24,15 @@ pub struct Insets {
|
||||
}
|
||||
|
||||
impl Insets {
|
||||
/// 返回该边距集合在水平方向占据的总空间。
|
||||
pub fn horizontal(self) -> f32 {
|
||||
self.left + self.right
|
||||
}
|
||||
/// 返回该边距集合在垂直方向占据的总空间。
|
||||
pub fn vertical(self) -> f32 {
|
||||
self.top + self.bottom
|
||||
}
|
||||
/// 在内容尺寸外加上对应四边占用,得到外层测量尺寸。
|
||||
fn outer(self, size: Size) -> Size {
|
||||
Size {
|
||||
width: size.width + self.horizontal(),
|
||||
@@ -36,6 +44,7 @@ impl Insets {
|
||||
/// Alignment inside a finite cross-axis content box. Overflow remains start
|
||||
/// aligned so that content cannot become inaccessible beyond the leading edge.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
/// 交叉轴对齐规则;Stretch 需要结合子项是否自动尺寸决定实际拉伸。
|
||||
pub enum CrossAlign {
|
||||
#[default]
|
||||
Start,
|
||||
@@ -45,6 +54,7 @@ pub enum CrossAlign {
|
||||
}
|
||||
|
||||
impl CrossAlign {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Start => "start",
|
||||
@@ -54,6 +64,7 @@ impl CrossAlign {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(value: &str) -> Result<Self, endor_core::VmError> {
|
||||
match value {
|
||||
"start" => Ok(Self::Start),
|
||||
@@ -66,6 +77,7 @@ impl CrossAlign {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据交叉轴对齐方式计算有限剩余空间中的偏移。
|
||||
fn offset(self, available: f32, occupied: f32) -> f32 {
|
||||
let spare = (available - occupied).max(0.0);
|
||||
match self {
|
||||
@@ -77,6 +89,7 @@ impl CrossAlign {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
/// 容器主轴的内容对齐或剩余间隔分配方式。
|
||||
pub enum MainAlign {
|
||||
#[default]
|
||||
Start,
|
||||
@@ -85,6 +98,7 @@ pub enum MainAlign {
|
||||
SpaceBetween,
|
||||
}
|
||||
impl MainAlign {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Start => "start",
|
||||
@@ -93,6 +107,7 @@ impl MainAlign {
|
||||
Self::SpaceBetween => "space-between",
|
||||
}
|
||||
}
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(value: &str) -> Result<Self, endor_core::VmError> {
|
||||
match value {
|
||||
"start" => Ok(Self::Start),
|
||||
@@ -108,6 +123,7 @@ impl MainAlign {
|
||||
|
||||
/// Main-axis constraints interpreted independently by each parent occurrence.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// 主轴伸缩策略与限值,不能用一个宽度常量替代完整分配规则。
|
||||
pub struct FlexPolicy {
|
||||
pub basis: Option<f32>,
|
||||
pub min: f32,
|
||||
@@ -115,6 +131,7 @@ pub struct FlexPolicy {
|
||||
pub shrink: f32,
|
||||
}
|
||||
impl Default for FlexPolicy {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
basis: None,
|
||||
@@ -126,12 +143,14 @@ impl Default for FlexPolicy {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 逻辑宽高值,不隐含窗口像素比例或相机缩放。
|
||||
pub struct Size {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 影响原生测量的缓存形状,避免把颜色等纯绘制变化视为尺寸变化。
|
||||
enum Shape {
|
||||
Sized(Box<Shape>, crate::box_model::Sizing, [f32; 4]),
|
||||
Text(String, u32, f32),
|
||||
@@ -141,6 +160,7 @@ enum Shape {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
/// 结构出现身份键,区分同一业务对象在不同父位置的展示。
|
||||
struct Key {
|
||||
parent: Option<u64>,
|
||||
slot: u64,
|
||||
@@ -148,6 +168,7 @@ struct Key {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
/// 布局缓存、稳定出现身份与统计。构建在临时副本上完成,成功后才写回。
|
||||
pub struct LayoutRuntime {
|
||||
sizes: HashMap<String, (Shape, Size)>,
|
||||
ids: HashMap<Key, u64>,
|
||||
@@ -161,6 +182,7 @@ pub struct LayoutRuntime {
|
||||
pub page_mode: Option<crate::page::Mode>,
|
||||
}
|
||||
|
||||
/// 迭代遍历中的一个出现,保存父子索引、约束与稳定 ID。
|
||||
struct Item<'a> {
|
||||
node: &'a UiNode,
|
||||
parent: Option<usize>,
|
||||
@@ -169,6 +191,7 @@ struct Item<'a> {
|
||||
constraints: [f32; 4],
|
||||
}
|
||||
|
||||
/// 检查尺寸非负、有限且不超过布局上限。
|
||||
fn bounded(value: f32) -> Result<f32> {
|
||||
if !value.is_finite() || !(0.0..=MAX_DIMENSION as f32).contains(&value) {
|
||||
bail!("UI layout exceeds the 1000000 coordinate/size limit");
|
||||
@@ -176,6 +199,7 @@ fn bounded(value: f32) -> Result<f32> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// 检查允许有正负方向的坐标仍在安全布局范围内。
|
||||
fn coordinate(value: f32) -> Result<f32> {
|
||||
if !value.is_finite() || value.abs() > MAX_DIMENSION as f32 {
|
||||
bail!("UI coordinate exceeds the 1000000 limit");
|
||||
@@ -183,6 +207,7 @@ fn coordinate(value: f32) -> Result<f32> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// 从页面模式和当前视口生成根节点约束;无视口时使用明确的无界轴哨兵。
|
||||
pub fn root_constraints(viewport: Option<Size>) -> [f32; 4] {
|
||||
let size = viewport.unwrap_or(Size {
|
||||
width: MAX_DIMENSION as f32,
|
||||
@@ -191,6 +216,7 @@ pub fn root_constraints(viewport: Option<Size>) -> [f32; 4] {
|
||||
[0.0, 0.0, size.width, size.height]
|
||||
}
|
||||
|
||||
/// 从父级上限扣除内边距;无界哨兵保持无界,不因减法被误认为有限。
|
||||
fn content_limit(available: f32, inset: f32) -> f32 {
|
||||
if available >= MAX_DIMENSION as f32 {
|
||||
available
|
||||
@@ -198,6 +224,7 @@ fn content_limit(available: f32, inset: f32) -> f32 {
|
||||
(available - inset).max(0.0)
|
||||
}
|
||||
}
|
||||
/// 把父盒子的可用尺寸传递给子项,扣除容器内边距并考虑主轴是否可延伸。
|
||||
pub fn child_constraints(node: &UiNode, parent: [f32; 4]) -> [f32; 4] {
|
||||
let parent = node.sizing.available(parent);
|
||||
if let UiKind::Overlay { .. } = node.kind {
|
||||
@@ -239,6 +266,7 @@ pub fn child_constraints(node: &UiNode, parent: [f32; 4]) -> [f32; 4] {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从子项可用范围扣除外边距,并使最小值不超过调整后的最大值。
|
||||
pub fn margin_constraints(child: &UiNode, mut constraints: [f32; 4]) -> [f32; 4] {
|
||||
for (axis, margin) in [child.margin.horizontal(), child.margin.vertical()]
|
||||
.into_iter()
|
||||
@@ -254,6 +282,7 @@ pub fn margin_constraints(child: &UiNode, mut constraints: [f32; 4]) -> [f32; 4]
|
||||
|
||||
/// Main-axis fill first measures against a finite offer, then participates in
|
||||
/// weighted allocation. The unlimited-axis sentinel is never made finite by padding.
|
||||
/// 根据具体子项尺寸策略和所在位置细化约束;世界 UI 根使用面板的有限像素尺寸。
|
||||
pub fn occurrence_constraints(
|
||||
parent: &UiNode,
|
||||
child: &UiNode,
|
||||
@@ -281,6 +310,7 @@ pub fn occurrence_constraints(
|
||||
margin_constraints(child, base)
|
||||
}
|
||||
|
||||
/// 生成求解开始时的子项约束,供后续测量和分配迭代使用。
|
||||
pub fn initial_child_constraints(
|
||||
parent: &UiNode,
|
||||
child: &UiNode,
|
||||
@@ -301,6 +331,7 @@ pub fn initial_child_constraints(
|
||||
}
|
||||
/// Scroll cross-axis constraints come from this occurrence's actual viewport,
|
||||
/// including a source parent's allocation rather than only constructor size.
|
||||
/// 根据已分配父尺寸重新细化子项约束,使实际安排能够反向影响换行等测量。
|
||||
pub fn arranged_child_constraints(node: &UiNode, available: [f32; 4], bounds: Size) -> [f32; 4] {
|
||||
if let UiKind::Scroll { axis, .. } = node.kind {
|
||||
let padding = insets(node);
|
||||
@@ -322,6 +353,7 @@ pub fn arranged_child_constraints(node: &UiNode, available: [f32; 4], bounds: Si
|
||||
child_constraints(node, available)
|
||||
}
|
||||
|
||||
/// 将期望尺寸限制在调用方给出的最小/最大边界内。
|
||||
pub fn constrained(size: Size, constraints: [f32; 4]) -> Size {
|
||||
Size {
|
||||
width: size.width.clamp(constraints[0], constraints[2]),
|
||||
@@ -330,6 +362,7 @@ pub fn constrained(size: Size, constraints: [f32; 4]) -> Size {
|
||||
}
|
||||
|
||||
/// Effective content inset, including borders, in border-box coordinates.
|
||||
/// 计算布局使用的有效内边距和边框占用,兼容旧的统一 padding。
|
||||
pub fn insets(node: &UiNode) -> Insets {
|
||||
let fallback = match node.kind {
|
||||
UiKind::Row { padding, .. }
|
||||
@@ -347,6 +380,7 @@ pub fn insets(node: &UiNode) -> Insets {
|
||||
}
|
||||
}
|
||||
|
||||
/// 提取容器方向与间距等原生布局配置,叶节点不参与容器规则。
|
||||
fn container(node: &UiNode) -> Option<(bool, Insets, f32)> {
|
||||
match &node.kind {
|
||||
UiKind::Row { gap, .. } => Some((true, insets(node), *gap)),
|
||||
@@ -358,10 +392,12 @@ fn container(node: &UiNode) -> Option<(bool, Insets, f32)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据容器方向选择主轴,供 flex 和对齐共享同一判断。
|
||||
pub fn main_axis(node: &UiNode) -> Option<usize> {
|
||||
container(node).map(|(row, _, _)| usize::from(!row))
|
||||
}
|
||||
|
||||
/// 根据形状键计算期望尺寸,包括文字换行、容器子项和盒模型占用。
|
||||
fn measure_shape(shape: &Shape) -> Result<Size> {
|
||||
let size = match shape {
|
||||
Shape::Sized(shape, sizing, constraints) => {
|
||||
@@ -419,6 +455,7 @@ fn measure_shape(shape: &Shape) -> Result<Size> {
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// 建立影响测量的形状键,将布局属性、子项测量结果和约束组合起来。
|
||||
fn shape(node: &UiNode, children: Vec<Size>, constraints: [f32; 4]) -> Result<Shape> {
|
||||
let available = node.sizing.available(constraints);
|
||||
let shape = match &node.kind {
|
||||
@@ -481,6 +518,7 @@ fn shape(node: &UiNode, children: Vec<Size>, constraints: [f32; 4]) -> Result<Sh
|
||||
})
|
||||
}
|
||||
|
||||
/// 执行原生节点测量并应用布局约束,作为源协议的原生实现。
|
||||
pub fn measure_native(node: &UiNode, children: Vec<Size>, constraints: [f32; 4]) -> Result<Size> {
|
||||
measure_shape(&shape(node, children, constraints)?)
|
||||
}
|
||||
@@ -488,6 +526,7 @@ pub fn measure_native(node: &UiNode, children: Vec<Size>, constraints: [f32; 4])
|
||||
/// Private derived scratch space for one occurrence during layout. Contains no
|
||||
/// managed references; source code cannot mutate it through heap attributes.
|
||||
#[derive(Debug, Default)]
|
||||
/// 只读布局调用期间提供给 Mount 查询的派生上下文,不作为业务状态保留。
|
||||
pub struct Context {
|
||||
pub node: Option<std::sync::Arc<UiNode>>,
|
||||
pub children: Vec<Size>,
|
||||
@@ -495,6 +534,7 @@ pub struct Context {
|
||||
pub bounds: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
/// 为单个原生节点按已分配几何生成内容命令;容器背景/边框与整树遍历由外层组织。
|
||||
pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option<RenderCommand> {
|
||||
let size_for_text = size;
|
||||
let inset = insets(node);
|
||||
@@ -537,6 +577,7 @@ pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option<RenderC
|
||||
}
|
||||
}
|
||||
|
||||
/// 从一个节点及其分配几何生成原生绘制命令,包括背景、边框与内容。
|
||||
pub fn paint_commands(node: &UiNode, x: f32, y: f32, size: Size) -> Vec<RenderCommand> {
|
||||
let mut commands: Vec<_> = paint_native(node, x, y, size).into_iter().collect();
|
||||
let border = node.border;
|
||||
@@ -573,6 +614,7 @@ pub fn paint_commands(node: &UiNode, x: f32, y: f32, size: Size) -> Vec<RenderCo
|
||||
/// Allocate main-axis space with bounded growth and basis-weighted shrinking.
|
||||
/// Preferred sizes come from this occurrence's first measurement in the solve;
|
||||
/// remeasurement must not turn a prior allocation into a new intrinsic basis.
|
||||
/// 判断子项主轴是否采用 fill,从而参与剩余空间分配。
|
||||
fn main_fill(child: &UiNode, row: bool) -> bool {
|
||||
(if row {
|
||||
child.sizing.width
|
||||
@@ -581,6 +623,7 @@ fn main_fill(child: &UiNode, row: bool) -> bool {
|
||||
}) == crate::box_model::Dimension::Fill
|
||||
}
|
||||
|
||||
/// 取出主轴 flex/尺寸策略共同约束下的最小和最大分配范围。
|
||||
fn main_limits(child: &UiNode, row: bool) -> (f32, f32) {
|
||||
let sizing = child.sizing;
|
||||
let (min, max) = if row {
|
||||
@@ -594,6 +637,7 @@ fn main_limits(child: &UiNode, row: bool) -> (f32, f32) {
|
||||
)
|
||||
}
|
||||
|
||||
/// 按权重分配主轴剩余空间,尊重每项 min/max;达到上限的项退出后重新分配剩余量。
|
||||
pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size], preferred: &[Size]) -> Vec<Size> {
|
||||
if matches!(node.kind, UiKind::Overlay { .. }) {
|
||||
let inset = insets(node);
|
||||
@@ -752,6 +796,7 @@ pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size], preferred: &[S
|
||||
output
|
||||
}
|
||||
|
||||
/// 根据最终分配尺寸计算子项原点,处理容器方向、对齐、间距与外边距。
|
||||
pub fn arrange_native(
|
||||
node: &UiNode,
|
||||
x: f32,
|
||||
@@ -887,25 +932,31 @@ pub fn arrange_native(
|
||||
}
|
||||
|
||||
impl LayoutRuntime {
|
||||
/// 从页面模式和当前视口生成根节点约束;无视口时使用明确的无界轴哨兵。
|
||||
pub fn root_constraints(&self) -> [f32; 4] {
|
||||
self.page_mode.map_or_else(
|
||||
|| root_constraints(self.viewport),
|
||||
|mode| mode.constraints(self.viewport),
|
||||
)
|
||||
}
|
||||
/// 撤销指定业务组件的测量缓存,下一次遇到该节点重新求值。
|
||||
pub fn invalidate(&mut self, block: &str) {
|
||||
self.sizes.remove(block);
|
||||
}
|
||||
/// 提高新 Mount ID 的分配下限,避免取消/重试后复用已经暴露过的出现身份。
|
||||
pub fn reserve_identity_floor(&mut self, floor: u64) {
|
||||
self.next_id = self.next_id.max(floor);
|
||||
}
|
||||
/// 返回出现 ID 的当前分配高水位,供桥接层在取消/重试后继续保持唯一性。
|
||||
pub fn identity_high_watermark(&self) -> u64 {
|
||||
self.next_id
|
||||
}
|
||||
|
||||
/// 清空结构出现身份表;业务对象和源状态不因此被复制。
|
||||
pub fn clear_occurrences(&mut self) {
|
||||
self.ids.clear();
|
||||
}
|
||||
/// 在临时 LayoutRuntime 中生成完整几何,成功后才替换现有缓存和身份状态。
|
||||
pub fn frame(&mut self, root: &UiNode) -> Result<Frame> {
|
||||
// Stage all cache/identity changes; a rejected frame leaves the last
|
||||
// valid occurrence registry intact and never publishes partial geometry.
|
||||
@@ -916,6 +967,7 @@ impl LayoutRuntime {
|
||||
}
|
||||
|
||||
/// Allocate stable occurrence identities without measuring or publishing geometry.
|
||||
/// 先建立稳定的出现身份和父子关系,但不把此阶段当成最终测量结果。
|
||||
pub fn structure(&mut self, root: &UiNode) -> Result<Frame> {
|
||||
let mut next = self.clone();
|
||||
let frame = next.build(root, true)?;
|
||||
@@ -923,6 +975,7 @@ impl LayoutRuntime {
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// 以形状、子项尺寸和约束查缓存;未命中才执行测量并更新本轮统计。
|
||||
pub fn measure_node(
|
||||
&mut self,
|
||||
node: &UiNode,
|
||||
@@ -943,6 +996,7 @@ impl LayoutRuntime {
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// 迭代展开出现树、计算约束和几何、产生绘制命令;结构准备与完整布局共用此驱动。
|
||||
fn build(&mut self, root: &UiNode, structural: bool) -> Result<Frame> {
|
||||
self.measured = 0;
|
||||
self.arranged = 0;
|
||||
@@ -1091,10 +1145,12 @@ impl LayoutRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 在临时 LayoutRuntime 中生成完整几何,成功后才替换现有缓存和身份状态。
|
||||
pub fn frame(root: &UiNode) -> Result<Frame> {
|
||||
LayoutRuntime::default().frame(root)
|
||||
}
|
||||
|
||||
/// 为指定逻辑视口建立临时布局运行时并求完整帧。
|
||||
pub fn frame_with_viewport(root: &UiNode, viewport: Size) -> Result<Frame> {
|
||||
LayoutRuntime {
|
||||
viewport: Some(viewport),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 渲染模块入口。排版、源视觉协议、相机投影和最终光栅化各自独立,通过不可变帧衔接。
|
||||
//!
|
||||
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
|
||||
pub(crate) mod box_model;
|
||||
pub(crate) mod display_list;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 让原生组件也通过 Endor 的 measure/arrange/paint 协议被调用。
|
||||
//! 将 Constraints、Size、Bounds 与 PaintCommand 在托管值和 Rust 值之间转换;原生方法与源覆盖方法共用调用契约。
|
||||
//!
|
||||
//! Native visual protocol entry points. Values are ordinary managed Endor
|
||||
//! objects; geometry is scoped to a live occurrence, never stored on a Block.
|
||||
use crate::{
|
||||
@@ -12,9 +15,11 @@ use endor_core::{
|
||||
NativeContext, NativeResult, ObjectRef, ObjectStore, Value, Vm, VmError,
|
||||
};
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||||
fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
if let Value::Object(object) = value {
|
||||
Ok(*object)
|
||||
@@ -22,6 +27,7 @@ fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
Err(invalid("visual protocol requires objects"))
|
||||
}
|
||||
}
|
||||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||||
pub(crate) fn number(
|
||||
cx: &NativeContext<'_>,
|
||||
object: ObjectRef,
|
||||
@@ -40,9 +46,11 @@ pub(crate) fn number(
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
/// 将原生布局计算结果封装为协议需要的托管 Value。
|
||||
pub(crate) fn value(value: f32) -> Value {
|
||||
Value::Number((value as f64).into())
|
||||
}
|
||||
/// 新建指定名义类型的托管记录,并逐项设置字段,作为视觉协议的结果返回。
|
||||
pub(crate) fn record(
|
||||
cx: &mut NativeContext<'_>,
|
||||
desktop: ObjectRef,
|
||||
@@ -56,6 +64,7 @@ pub(crate) fn record(
|
||||
}
|
||||
Ok(object)
|
||||
}
|
||||
/// 在约束提供的视口内执行原生布局,从根出现提取宽高,再按协议约束限制结果。
|
||||
fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size, VmError> {
|
||||
let frame = layout::frame_with_viewport(
|
||||
node,
|
||||
@@ -75,6 +84,7 @@ fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size,
|
||||
})
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
|
||||
enum Query {
|
||||
Measure,
|
||||
Arrange,
|
||||
@@ -82,12 +92,14 @@ enum Query {
|
||||
HitTest,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||||
struct Method {
|
||||
query: Query,
|
||||
desktop: ObjectRef,
|
||||
signature: CallSignature,
|
||||
}
|
||||
/// Only this actual implementation can use the matching intrinsic fast path.
|
||||
/// 判断方法是否为匹配协议的宿主原生实现,决定能否安全使用快速路径。
|
||||
pub(crate) fn is_intrinsic(
|
||||
store: &ObjectStore,
|
||||
function: ObjectRef,
|
||||
@@ -122,13 +134,16 @@ pub(crate) fn is_intrinsic(
|
||||
}
|
||||
|
||||
impl NativeCallable for Method {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(3))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [owner, context, input] = args else {
|
||||
return Err(invalid("visual method expects a Mount and an input value"));
|
||||
@@ -352,15 +367,18 @@ impl NativeCallable for Method {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Point 的矩形/圆形命中辅助方法,保存结果类型和具体几何判定种类。
|
||||
struct PointMethod {
|
||||
expected: ObjectRef,
|
||||
circle: bool,
|
||||
signature: CallSignature,
|
||||
}
|
||||
impl NativeCallable for PointMethod {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(if self.circle {
|
||||
4
|
||||
@@ -369,6 +387,7 @@ impl NativeCallable for PointMethod {
|
||||
}))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let Some(Value::Object(point)) = args.first() else {
|
||||
return Err(invalid("Point method requires a Point"));
|
||||
@@ -419,6 +438,7 @@ impl NativeCallable for PointMethod {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub fn install(
|
||||
vm: &mut Vm,
|
||||
roots: &mut Vec<ObjectRef>,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 旧 Page.mode 的兼容空间策略,以及页面级的正逆相机变换。
|
||||
//! 新组合应优先使用普通布局、ScrollView 和独立场景视口;保留这里是为了兼容已有 window/vertical/horizontal/canvas 页面。
|
||||
//!
|
||||
//! Page space is independent of the native window. The camera is applied once
|
||||
//! for painting and inverted once for input; layout and Mounts stay in world units.
|
||||
use crate::{geometry::Point, layout::Size};
|
||||
@@ -6,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
/// 旧 Page 的有限或自由空间模式,新场景视口不依赖它表达布局方向。
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Window,
|
||||
@@ -15,6 +19,7 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Window => "window",
|
||||
@@ -24,6 +29,7 @@ impl Mode {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(name: &str) -> Result<Self, VmError> {
|
||||
match name {
|
||||
"window" => Ok(Self::Window),
|
||||
@@ -36,6 +42,7 @@ impl Mode {
|
||||
}
|
||||
}
|
||||
|
||||
/// 为旧页面模式生成有限轴和可延伸轴的根约束。
|
||||
pub fn constraints(self, viewport: Option<Size>) -> [f32; 4] {
|
||||
let Some(size) = viewport else {
|
||||
return crate::layout::root_constraints(None);
|
||||
@@ -51,6 +58,7 @@ impl Mode {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
/// 页面级兼容相机状态和有限内容范围,支持绘制与命中正逆变换。
|
||||
pub struct Surface {
|
||||
pub mode: Mode,
|
||||
pub page: String,
|
||||
@@ -60,11 +68,13 @@ pub struct Surface {
|
||||
pub extent: Size,
|
||||
}
|
||||
|
||||
/// 生成页面相机计算所需的单位比例或边界默认值。
|
||||
fn unit() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
impl Default for Surface {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: Mode::Window,
|
||||
@@ -77,6 +87,7 @@ impl Default for Surface {
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(mode: Mode) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
@@ -85,6 +96,7 @@ impl Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把页面视图坐标反算为旧 Page 相机下的世界坐标。
|
||||
pub fn to_world(&self, point: Point) -> Point {
|
||||
Point::new(
|
||||
point.x / self.zoom + self.pan.x,
|
||||
@@ -92,6 +104,7 @@ impl Surface {
|
||||
)
|
||||
}
|
||||
|
||||
/// 把旧页面世界坐标变换到可见视图坐标。
|
||||
pub fn to_view(&self, point: Point) -> Point {
|
||||
Point::new(
|
||||
(point.x - self.pan.x) * self.zoom,
|
||||
@@ -99,6 +112,7 @@ impl Surface {
|
||||
)
|
||||
}
|
||||
|
||||
/// 判断给定点是否位于目标范围;需要相机转换的类型会先转换再判断。
|
||||
pub fn contains(&self, point: [f32; 2], viewport: Option<Size>) -> bool {
|
||||
let point = self.to_view(Point::new(point[0], point[1]));
|
||||
point.x >= 0.0
|
||||
@@ -106,6 +120,7 @@ impl Surface {
|
||||
&& viewport.is_none_or(|size| point.x < size.width && point.y < size.height)
|
||||
}
|
||||
|
||||
/// 只对有限页面按实际内容范围钳制偏移;自由画布不按内容边缘停止导航。
|
||||
pub fn clamp(&mut self, viewport: Size) {
|
||||
match self.mode {
|
||||
Mode::Window => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 合成整个原生窗口的显示列表:页面画面、宿主侧栏和开发工具绘制。
|
||||
//! 窗口逻辑尺寸与设备像素比例在这里衔接。页面内容先应用其视角并裁剪,宿主控件的区域和命中布局另外返回。
|
||||
//!
|
||||
use crate::{
|
||||
activity::Activity,
|
||||
developer_tools::{self, DeveloperToolsLayout, Workspace as DeveloperToolsState},
|
||||
@@ -7,10 +10,12 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 宿主管理区域的几何布局,供窗口事件分辨页面与工具命中。
|
||||
pub struct SceneLayout {
|
||||
pub developer_tools: DeveloperToolsLayout,
|
||||
}
|
||||
|
||||
/// 组合页面、开发工具和侧栏的设备像素显示列表,同时返回供命中使用的宿主布局。
|
||||
pub fn build_scene(
|
||||
width: u32,
|
||||
height: u32,
|
||||
@@ -84,6 +89,7 @@ pub fn build_scene(
|
||||
}
|
||||
}
|
||||
|
||||
/// 整窗合成结果:显示列表与同帧管理交互几何一起返回。
|
||||
pub struct DisplayScene {
|
||||
pub display_list: DisplayList,
|
||||
pub layout: SceneLayout,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 有界 CPU 3D 渲染器。使用相同摄像机与世界变换进行视锥裁剪、深度测试、纹理采样和射线拾取。
|
||||
//! 不可变 View 可以缓存光栅结果;相机变化会产生新视图。当前不是 GPU 后端,像素和光栅工作预算用于限制分配与循环工作量。
|
||||
//! 面板 UI 先渲染为有限纹理,再以透视校正 UV 映射进世界。与相机相关的投影宽高比由具体视口提供。
|
||||
//!
|
||||
//! Software 3D surfaces share the application's existing CPU compositor. Mesh
|
||||
//! visibility and picking use the same camera and transformed world triangles.
|
||||
pub mod math;
|
||||
@@ -11,6 +15,7 @@ pub const MAX_TRIANGLES: usize = 65_536;
|
||||
pub const MAX_VERTICES: usize = 32_768;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 不可变顶点、三角形和可选 UV;多个网格可共享同一数值缓冲。
|
||||
pub struct Geometry {
|
||||
pub vertices: Vec<Vec3>,
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
@@ -18,6 +23,7 @@ pub struct Geometry {
|
||||
pub uv: Vec<[f32; 2]>,
|
||||
}
|
||||
impl Geometry {
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(&self) -> bool {
|
||||
self.vertices.len() <= MAX_VERTICES
|
||||
&& (self.uv.is_empty()
|
||||
@@ -31,6 +37,7 @@ impl Geometry {
|
||||
.flatten()
|
||||
.all(|&index| (index as usize) < self.vertices.len())
|
||||
}
|
||||
/// 生成以原点为中心的盒体顶点和三角形索引;位置和旋转由网格世界矩阵另行描述。
|
||||
pub fn cuboid(width: f32, height: f32, depth: f32) -> Self {
|
||||
let (x, y, z) = (width / 2., height / 2., depth / 2.);
|
||||
Self {
|
||||
@@ -63,6 +70,7 @@ impl Geometry {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 一次场景实例:几何、世界矩阵、颜色与可选 UI 面板,身份用于拾取回关联。
|
||||
pub struct Mesh {
|
||||
pub identity: String,
|
||||
pub geometry: Arc<Geometry>,
|
||||
@@ -72,12 +80,14 @@ pub struct Mesh {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 面板内普通 UI 的绘制命令及其裁剪/二维变换,逻辑排版不随外部相机改变。
|
||||
pub struct UiSurface {
|
||||
pub commands: Vec<crate::document::RenderCommand>,
|
||||
pub clips: Vec<Option<[f32; 4]>>,
|
||||
pub views: Vec<Option<crate::viewport::Transform>>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 世界平面的有限 UI 纹理描述,包含像素尺寸、背景与内部显示内容。
|
||||
pub struct Panel {
|
||||
pub key: u64,
|
||||
pub width: u32,
|
||||
@@ -86,12 +96,14 @@ pub struct Panel {
|
||||
pub surface: Option<Arc<UiSurface>>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 方向光的原生数值,方向和强度在场景快照阶段校验。
|
||||
pub struct Light {
|
||||
pub direction: Vec3,
|
||||
pub color: [f32; 3],
|
||||
pub intensity: f32,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
/// 不含摄像机的世界实例和光照集合;BVH 是可丢弃的派生缓存。
|
||||
pub struct Scene {
|
||||
pub meshes: Vec<Mesh>,
|
||||
pub lights: Vec<Light>,
|
||||
@@ -101,6 +113,7 @@ pub struct Scene {
|
||||
pub acceleration: Arc<spatial::Index>,
|
||||
}
|
||||
impl PartialEq for Scene {
|
||||
/// 比较会影响场景结果的数据;加速索引属于派生缓存,不参与场景语义相等。
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.meshes == other.meshes
|
||||
&& self.lights == other.lights
|
||||
@@ -109,21 +122,26 @@ impl PartialEq for Scene {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 显式相机与共享世界的组合,一个 Scene 可由多个独立 View 观察。
|
||||
pub struct View {
|
||||
pub scene: Arc<Scene>,
|
||||
pub camera: Camera,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 只读绘制结果使用的名义包装,让源 PaintCommand 可以携带不可变 3D 快照。
|
||||
pub struct PaintView(pub Arc<View>);
|
||||
impl endor_core::ObjectPayload for PaintView {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"immutable-3d-paint"
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 射线与实际三角形的精确交点,含实例身份、距离、法线和重心坐标。
|
||||
pub struct Hit {
|
||||
pub instance: usize,
|
||||
pub identity: String,
|
||||
@@ -135,10 +153,13 @@ pub struct Hit {
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(&self) -> bool {
|
||||
thread_local! { static DEPTH:std::cell::Cell<usize>=const { std::cell::Cell::new(0) }; }
|
||||
// 验证可能递归进入面板内的 3D;作用域结束时归还深度,错误返回也不泄漏计数。
|
||||
struct Guard;
|
||||
impl Drop for Guard {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
DEPTH.with(|depth| depth.set(depth.get() - 1));
|
||||
}
|
||||
@@ -219,6 +240,7 @@ impl View {
|
||||
&& (0.0..=32.0).contains(&light.intensity)
|
||||
})
|
||||
}
|
||||
/// 按包围盒候选发射射线,再检查三角形;距离相同时保留与绘制顺序一致的实例。
|
||||
pub fn pick(&self, u: f32, v: f32, aspect: f32) -> Option<Hit> {
|
||||
if !self.camera.valid()
|
||||
|| !(0.0..=1.0).contains(&u)
|
||||
@@ -278,13 +300,16 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把有限颜色通道转换成最终像素使用的字节。
|
||||
fn channel(value: f32) -> u32 {
|
||||
(value.clamp(0., 1.) * 255.).round() as u32
|
||||
}
|
||||
/// 将三个颜色通道打包为软件像素缓冲使用的 RGB 值。
|
||||
pub fn rgb(color: [f32; 3]) -> u32 {
|
||||
channel(color[0]) << 16 | channel(color[1]) << 8 | channel(color[2])
|
||||
}
|
||||
|
||||
/// 某个不可变视图在特定分辨率和宽高比下的光栅结果。
|
||||
struct CachedSurface {
|
||||
view: Arc<View>,
|
||||
width: u32,
|
||||
@@ -293,11 +318,13 @@ struct CachedSurface {
|
||||
pixels: Arc<[u32]>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
/// 限制条目数和总像素数的表面缓存;不承担场景业务所有权。
|
||||
struct SurfaceCache {
|
||||
entries: std::collections::VecDeque<CachedSurface>,
|
||||
pixels: usize,
|
||||
}
|
||||
|
||||
/// 按不可变视图身份、分辨率和投影比例查光栅缓存;未命中时在锁外绘制,支持嵌套世界 UI。
|
||||
pub fn surface(
|
||||
view: &Arc<View>,
|
||||
width: u32,
|
||||
@@ -341,11 +368,13 @@ pub fn surface(
|
||||
/// Homogeneous frustum planes expressed in view space. Clipping before division
|
||||
/// handles triangles crossing the near plane without huge screen coordinates.
|
||||
#[derive(Clone, Copy)]
|
||||
/// 裁剪过程中的相机空间顶点,位置与 UV 必须共同插值。
|
||||
struct Vertex {
|
||||
position: Vec3,
|
||||
uv: [f32; 2],
|
||||
}
|
||||
impl Vertex {
|
||||
/// 按裁剪交点比例同时插值位置和 UV,保持新顶点的几何与纹理坐标一致。
|
||||
fn lerp(self, other: Self, t: f32) -> Self {
|
||||
Self {
|
||||
position: self.position + (other.position - self.position) * t,
|
||||
@@ -356,6 +385,7 @@ impl Vertex {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 生成相机空间六个视锥平面,供三角形裁剪与 BVH 可见性查询共用。
|
||||
fn frustum_planes(camera: Camera, aspect: f32) -> [[f32; 4]; 6] {
|
||||
let (x, y, slope) = match camera.projection {
|
||||
Projection::Perspective { fov } => {
|
||||
@@ -373,6 +403,7 @@ fn frustum_planes(camera: Camera, aspect: f32) -> [[f32; 4]; 6] {
|
||||
[0., -1., y * slope, y * (1. - slope)],
|
||||
]
|
||||
}
|
||||
/// 在透视除法前逐平面裁剪多边形,并同步插值 UV,避免跨近面顶点投影到极大坐标。
|
||||
fn clip(mut polygon: Vec<Vertex>, camera: Camera, aspect: f32) -> Vec<Vertex> {
|
||||
for [a, b, c, d] in frustum_planes(camera, aspect) {
|
||||
if polygon.is_empty() {
|
||||
@@ -402,9 +433,11 @@ fn clip(mut polygon: Vec<Vertex>, camera: Camera, aspect: f32) -> Vec<Vertex> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// 把当前绘制输入转换为输出表面;分配、裁剪和预算由本实现检查。
|
||||
pub fn render(view: &View, width: u32, height: u32) -> Result<Vec<u32>, &'static str> {
|
||||
render_aspect(view, width, height, width as f32 / height as f32)
|
||||
}
|
||||
/// 先校验并预留颜色/深度缓冲预算,再剔除、裁剪、投影和光栅化三角形;超预算返回错误。
|
||||
fn render_aspect(
|
||||
view: &View,
|
||||
width: u32,
|
||||
@@ -448,6 +481,7 @@ fn render_aspect(
|
||||
temporary = spatial::Index::build(&view.scene.meshes);
|
||||
&temporary
|
||||
};
|
||||
// 宽阶段只筛选候选:离屏网格不进入后续纹理分配和逐三角形处理。
|
||||
for instance in index.visible(camera, aspect) {
|
||||
let mesh = &view.scene.meshes[instance];
|
||||
let texture = mesh.panel.as_ref().map(panel::texture).transpose()?;
|
||||
@@ -464,6 +498,7 @@ fn render_aspect(
|
||||
}
|
||||
}
|
||||
let color = rgb(std::array::from_fn(|i| mesh.color[i] * light[i]));
|
||||
// 在透视除法之前裁剪,并插入具有正确 UV 的交点;不能先投影再裁剪近面。
|
||||
let polygon = clip(
|
||||
indices
|
||||
.map(|index| Vertex {
|
||||
@@ -485,6 +520,7 @@ fn render_aspect(
|
||||
if polygon.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
// 裁剪后可能多于三个顶点,用扇形重新三角化,保持同一平面的纹理参数。
|
||||
for index in 1..polygon.len() - 1 {
|
||||
let vertices = [polygon[0], polygon[index], polygon[index + 1]];
|
||||
let [a, b, c] = vertices.map(|vertex| project(vertex.position));
|
||||
@@ -515,16 +551,19 @@ fn render_aspect(
|
||||
if weights.iter().any(|weight| *weight < -1e-5) {
|
||||
continue;
|
||||
}
|
||||
// 透视投影下使用倒深度插值,正交投影下可直接线性插值。
|
||||
let depth = if perspective {
|
||||
1. / (weights[0] / a.z + weights[1] / b.z + weights[2] / c.z)
|
||||
} else {
|
||||
weights[0] * a.z + weights[1] * b.z + weights[2] * c.z
|
||||
};
|
||||
let index = y as usize * width as usize + x as usize;
|
||||
// 只让最近表面写像素;等深度保留先绘制实例,与拾取的排序约定一致。
|
||||
if depth < depths[index] {
|
||||
depths[index] = depth;
|
||||
pixels[index] =
|
||||
if let (Some(texture), Some(panel)) = (&texture, &mesh.panel) {
|
||||
// UV 也要除以深度再插值,否则倾斜面板会出现纹理拉伸失真。
|
||||
let interpolated = |axis| {
|
||||
if perspective {
|
||||
depth
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
//! 3D 数学基础:向量、仿射矩阵、摄像机基向量、投影射线和三角形相交。
|
||||
//! 矩阵组合顺序决定局部变换如何进入父坐标系;接近退化的逆矩阵、法线和射线使用 Option 拒绝无效计算。
|
||||
//!
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
|
||||
/// 三个有限浮点分量的数学向量,不是托管 Vec3 对象。
|
||||
pub struct Vec3 {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
}
|
||||
impl Vec3 {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub const fn new(x: f32, y: f32, z: f32) -> Self {
|
||||
Self { x, y, z }
|
||||
}
|
||||
/// 计算向量点积,用于方向投影、光照和相交判断。
|
||||
pub fn dot(self, b: Self) -> f32 {
|
||||
self.x * b.x + self.y * b.y + self.z * b.z
|
||||
}
|
||||
/// 计算右手叉积,用于摄像机基向量和三角形法线。
|
||||
pub fn cross(self, b: Self) -> Self {
|
||||
Self::new(
|
||||
self.y * b.z - self.z * b.y,
|
||||
@@ -21,13 +28,16 @@ impl Vec3 {
|
||||
self.x * b.y - self.y * b.x,
|
||||
)
|
||||
}
|
||||
/// 计算向量长度;归一化前还需检查是否接近零。
|
||||
pub fn length(self) -> f32 {
|
||||
self.dot(self).sqrt()
|
||||
}
|
||||
/// 仅在长度有效且不退化时返回单位向量,避免除零产生非有限结果。
|
||||
pub fn normalized(self) -> Option<Self> {
|
||||
let length = self.length();
|
||||
(length.is_finite() && length > 1e-6).then(|| self * (1.0 / length))
|
||||
}
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(self) -> bool {
|
||||
[self.x, self.y, self.z]
|
||||
.iter()
|
||||
@@ -36,26 +46,31 @@ impl Vec3 {
|
||||
}
|
||||
impl Add for Vec3 {
|
||||
type Output = Self;
|
||||
/// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。
|
||||
fn add(self, b: Self) -> Self {
|
||||
Self::new(self.x + b.x, self.y + b.y, self.z + b.z)
|
||||
}
|
||||
}
|
||||
impl Sub for Vec3 {
|
||||
type Output = Self;
|
||||
/// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。
|
||||
fn sub(self, b: Self) -> Self {
|
||||
Self::new(self.x - b.x, self.y - b.y, self.z - b.z)
|
||||
}
|
||||
}
|
||||
impl Mul<f32> for Vec3 {
|
||||
type Output = Self;
|
||||
/// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。
|
||||
fn mul(self, b: f32) -> Self {
|
||||
Self::new(self.x * b, self.y * b, self.z * b)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// 仿射 4×4 变换矩阵,支持局部到世界的组合与求逆。
|
||||
pub struct Matrix(pub [f32; 16]);
|
||||
impl Default for Matrix {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self([
|
||||
1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
|
||||
@@ -63,6 +78,7 @@ impl Default for Matrix {
|
||||
}
|
||||
}
|
||||
impl Matrix {
|
||||
/// 求仿射世界矩阵的逆;退化或不可逆时返回空值。
|
||||
pub fn inverse(self) -> Option<Self> {
|
||||
let m = self.0;
|
||||
let determinant = m[0] * (m[5] * m[10] - m[6] * m[9]) - m[1] * (m[4] * m[10] - m[6] * m[8])
|
||||
@@ -95,6 +111,7 @@ impl Matrix {
|
||||
out.0[11] = translation.z;
|
||||
Some(out)
|
||||
}
|
||||
/// 组合父子仿射变换,调用顺序决定局部坐标最终落在哪个世界位置。
|
||||
pub fn compose(self, local: Self) -> Self {
|
||||
let mut out = [0.; 16];
|
||||
for row in 0..4 {
|
||||
@@ -106,6 +123,7 @@ impl Matrix {
|
||||
}
|
||||
Self(out)
|
||||
}
|
||||
/// 对一个位置应用矩阵,包括平移项;不要用它代替纯方向变换。
|
||||
pub fn point(self, p: Vec3) -> Vec3 {
|
||||
Vec3::new(
|
||||
self.0[0] * p.x + self.0[1] * p.y + self.0[2] * p.z + self.0[3],
|
||||
@@ -113,6 +131,7 @@ impl Matrix {
|
||||
self.0[8] * p.x + self.0[9] * p.y + self.0[10] * p.z + self.0[11],
|
||||
)
|
||||
}
|
||||
/// 由平移、角度旋转和缩放构造局部仿射矩阵。
|
||||
pub fn trs(position: Vec3, rotation: Vec3, scale: Vec3) -> Self {
|
||||
let (sx, cx) = rotation.x.to_radians().sin_cos();
|
||||
let (sy, cy) = rotation.y.to_radians().sin_cos();
|
||||
@@ -140,12 +159,14 @@ impl Matrix {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// 投影策略:透视视场角或正交可见高度。
|
||||
pub enum Projection {
|
||||
Perspective { fov: f32 },
|
||||
Orthographic { height: f32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// 原生摄像机描述,包括观察基、近远面和投影参数;宽高比由视口提供。
|
||||
pub struct Camera {
|
||||
pub position: Vec3,
|
||||
pub target: Vec3,
|
||||
@@ -155,6 +176,7 @@ pub struct Camera {
|
||||
pub projection: Projection,
|
||||
}
|
||||
impl Default for Camera {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: Vec3::new(0., 0., 5.),
|
||||
@@ -167,18 +189,21 @@ impl Default for Camera {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 摄像机的右、上、前单位基向量。
|
||||
pub struct Basis {
|
||||
pub right: Vec3,
|
||||
pub up: Vec3,
|
||||
pub forward: Vec3,
|
||||
}
|
||||
impl Camera {
|
||||
/// 从观察位置、目标和上方向建立正交相机基;方向退化时拒绝。
|
||||
pub fn basis(self) -> Option<Basis> {
|
||||
let forward = (self.target - self.position).normalized()?;
|
||||
let right = forward.cross(self.up).normalized()?;
|
||||
let up = right.cross(forward).normalized()?;
|
||||
Some(Basis { right, up, forward })
|
||||
}
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(self) -> bool {
|
||||
self.position.valid()
|
||||
&& self.target.valid()
|
||||
@@ -196,10 +221,12 @@ impl Camera {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 把世界点投影到相机的右/上/前基向量坐标。
|
||||
pub fn view(self, basis: Basis, p: Vec3) -> Vec3 {
|
||||
let p = p - self.position;
|
||||
Vec3::new(p.dot(basis.right), p.dot(basis.up), p.dot(basis.forward))
|
||||
}
|
||||
/// 从视口归一化坐标生成透视或正交射线;宽高比由具体视口提供。
|
||||
pub fn ray(self, u: f32, v: f32, aspect: f32) -> Option<Ray> {
|
||||
let basis = self.basis()?;
|
||||
let (x, y) = (u * 2. - 1., 1. - v * 2.);
|
||||
@@ -224,11 +251,13 @@ impl Camera {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 世界坐标中的射线原点与方向,供网格和面板求交。
|
||||
pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
}
|
||||
impl Ray {
|
||||
/// 求射线与三角形相交距离和重心坐标,拒绝退化三角形及射线起点后方的交点。
|
||||
pub fn triangle(self, a: Vec3, b: Vec3, c: Vec3) -> Option<(f32, [f32; 3])> {
|
||||
let ab = b - a;
|
||||
let ac = c - a;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 连接 3D 平面与普通 UI 的坐标和绘制桥梁。
|
||||
//! Mapping 沿嵌套面板逐层把页面指针变成纹理坐标;可见命中还须检查最近射线交点,防止点穿遮挡物。
|
||||
//! compose 把各 Mount 的绘制命令按所属面板分组并递归装配;texture 再调用普通软件渲染器绘制面板内容。
|
||||
//!
|
||||
//! Perspective UI mapping. The same world plane supplies texture coordinates
|
||||
//! during rendering and inverse pointer coordinates during event dispatch.
|
||||
use super::{
|
||||
@@ -11,6 +15,7 @@ use crate::{
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 一次面板出现的正逆投影信息,可以链接父面板形成嵌套映射。
|
||||
pub struct Mapping {
|
||||
pub owner: u64,
|
||||
pub key: u64,
|
||||
@@ -21,6 +26,7 @@ pub struct Mapping {
|
||||
pub parent: Option<Arc<Mapping>>,
|
||||
}
|
||||
impl Mapping {
|
||||
/// 沿父面板逆投影到当前平面的 UI 像素;要求可见时使用最近射线命中拒绝被遮挡的目标。
|
||||
pub fn texture_point(&self, point: Point, clipped: bool) -> Option<Point> {
|
||||
let point = if let Some(parent) = &self.parent {
|
||||
parent.texture_point(point, clipped)?
|
||||
@@ -47,6 +53,7 @@ impl Mapping {
|
||||
})?;
|
||||
let panel = mesh.panel.as_ref()?;
|
||||
if clipped {
|
||||
// 指针落在平面投影内还不够:最近三角形必须属于本面板,不能点穿前方网格。
|
||||
let hit = self.view.pick(u, v, width / height)?;
|
||||
if self.view.scene.meshes[hit.instance]
|
||||
.panel
|
||||
@@ -56,6 +63,7 @@ impl Mapping {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// 捕获拖动可以不要求指针仍位于面板内,但仍使用原面板平面的连续逆映射。
|
||||
let ray = self.view.camera.ray(u, v, width / height)?;
|
||||
let center = mesh.world.point(Vec3::default());
|
||||
let normal = (mesh.world.point(Vec3::new(1., 0., 0.)) - center)
|
||||
@@ -81,6 +89,7 @@ impl Mapping {
|
||||
(point.x.is_finite() && point.y.is_finite()).then_some(point)
|
||||
}
|
||||
|
||||
/// 把当前面板的 UI 像素正向投影到外层页面空间,供坐标查询和交互测试使用。
|
||||
pub fn project_point(&self, point: Point) -> Option<Point> {
|
||||
let mesh = self.view.scene.meshes.iter().find(|mesh| {
|
||||
mesh.panel
|
||||
@@ -118,6 +127,7 @@ impl Mapping {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// 归属明确的一条内部 UI 绘制命令,装配时还保留裁剪、变换和 owner。
|
||||
struct Entry {
|
||||
command: crate::document::RenderCommand,
|
||||
clip: Option<[f32; 4]>,
|
||||
@@ -127,12 +137,14 @@ struct Entry {
|
||||
|
||||
/// Collect a panel's ordinary paint subtree into its texture. Nested 3D
|
||||
/// commands are assembled recursively before their containing texture.
|
||||
/// 按面板所属视口和稳定 key 分组绘制命令,递归将内部 UI 装配进 3D 面板表面。
|
||||
pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
use crate::document::RenderCommand;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
let mounts: HashMap<_, _> = frame.mounts.iter().map(|mount| (mount.id, mount)).collect();
|
||||
let mut groups = HashMap::<(u64, u64), Vec<Entry>>::new();
|
||||
let mut roots = Vec::new();
|
||||
// 每条命令都有出现归属;据此分流为窗口根命令或某个世界面板的内部命令。
|
||||
for (index, command) in frame.commands.iter().enumerate() {
|
||||
let owner = frame.command_mounts[index];
|
||||
let mount = mounts[&owner];
|
||||
@@ -172,6 +184,7 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
owner: mount.id,
|
||||
});
|
||||
}
|
||||
/// 递归装配一个绘制条目包含的世界 UI,检查嵌套环与深度,再生成独立不可变视图。
|
||||
fn assemble(
|
||||
entry: Entry,
|
||||
groups: &HashMap<(u64, u64), Vec<Entry>>,
|
||||
@@ -186,6 +199,7 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
depth < 8 && path.insert(entry.owner),
|
||||
"3D UI paint nesting cycle or depth budget exceeded"
|
||||
);
|
||||
// 只构造新的原生快照,不写回源 Scene3D;同一业务场景仍可被其他视口共享。
|
||||
let mut scene = (*view.scene).clone();
|
||||
for mesh in &mut scene.meshes {
|
||||
if let Some(panel) = &mut mesh.panel {
|
||||
@@ -226,6 +240,7 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 为一个面板分配有限像素表面,并调用普通 UI 软件渲染;当前实现不会按面板内容独立缓存纹理。
|
||||
pub fn texture(panel: &super::Panel) -> Result<Vec<u32>, &'static str> {
|
||||
crate::software_renderer::reserve_pixels(panel.width as usize * panel.height as usize)?;
|
||||
let mut pixels =
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 不可变世界空间 BVH。用网格变换后的包围盒快速筛选视锥与射线候选。
|
||||
//! 空间索引只做宽阶段筛选;最终拾取仍检查真实三角形。相机变化可复用索引,几何或变换变化需要新索引。
|
||||
//!
|
||||
//! Immutable world-space BVH. Camera changes reuse it; it contains no VM data.
|
||||
use super::{
|
||||
Mesh,
|
||||
@@ -5,11 +8,13 @@ use super::{
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 变换后网格的世界空间 AABB,做保守筛选而不是精确表面描述。
|
||||
pub struct Bounds {
|
||||
pub min: Vec3,
|
||||
pub max: Vec3,
|
||||
}
|
||||
impl Bounds {
|
||||
/// 变换网格顶点并求世界 AABB,作为空间索引的保守外包围。
|
||||
fn mesh(mesh: &Mesh) -> Option<Self> {
|
||||
let mut points = mesh
|
||||
.geometry
|
||||
@@ -36,6 +41,7 @@ impl Bounds {
|
||||
},
|
||||
))
|
||||
}
|
||||
/// 求两个包围盒的最小共同外包围,供 BVH 父节点使用。
|
||||
fn union(self, other: Self) -> Self {
|
||||
Self {
|
||||
min: Vec3::new(
|
||||
@@ -50,11 +56,13 @@ impl Bounds {
|
||||
),
|
||||
}
|
||||
}
|
||||
/// 取指定轴中心位置,供最长轴排序和分割使用。
|
||||
fn center(self, axis: usize) -> f32 {
|
||||
let min = [self.min.x, self.min.y, self.min.z];
|
||||
let max = [self.max.x, self.max.y, self.max.z];
|
||||
(min[axis] + max[axis]) / 2.
|
||||
}
|
||||
/// 用包围盒筛出射线候选;索引查询按进入距离排序,精确三角形求交在调用方完成。
|
||||
fn ray(self, ray: Ray) -> Option<f32> {
|
||||
let mut near = 0f32;
|
||||
let mut far = f32::INFINITY;
|
||||
@@ -79,6 +87,7 @@ impl Bounds {
|
||||
}
|
||||
Some(near)
|
||||
}
|
||||
/// 用相机六平面保守判断包围盒可见性,返回保持原场景顺序的候选。
|
||||
fn visible(self, camera: Camera, basis: Basis, planes: &[[f32; 4]; 6]) -> bool {
|
||||
let corners: [Vec3; 8] = std::array::from_fn(|index| {
|
||||
camera.view(
|
||||
@@ -110,6 +119,7 @@ impl Bounds {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// BVH 叶或分支;叶保存实例索引,分支保存两个子节点索引。
|
||||
enum Node {
|
||||
Leaf {
|
||||
bounds: Bounds,
|
||||
@@ -122,6 +132,7 @@ enum Node {
|
||||
},
|
||||
}
|
||||
impl Node {
|
||||
/// 读取叶/分支统一的包围盒,隐藏节点种类差异。
|
||||
fn bounds(&self) -> Bounds {
|
||||
match self {
|
||||
Self::Leaf { bounds, .. } | Self::Branch { bounds, .. } => *bounds,
|
||||
@@ -130,12 +141,14 @@ impl Node {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 由不可变场景建立的包围盒树,供相机视锥和射线查询复用。
|
||||
pub struct Index {
|
||||
bounds: Vec<Option<Bounds>>,
|
||||
nodes: Vec<Node>,
|
||||
root: Option<usize>,
|
||||
}
|
||||
impl Index {
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub fn build(meshes: &[Mesh]) -> Self {
|
||||
let bounds: Vec<_> = meshes.iter().map(Bounds::mesh).collect();
|
||||
let mut items: Vec<_> = bounds
|
||||
@@ -153,6 +166,7 @@ impl Index {
|
||||
}
|
||||
index
|
||||
}
|
||||
/// 按包围盒最长轴的中心排序并递归二分,叶节点保留少量网格候选。
|
||||
fn split(&mut self, items: &mut [usize]) -> usize {
|
||||
let bounds = items
|
||||
.iter()
|
||||
@@ -196,9 +210,11 @@ impl Index {
|
||||
}
|
||||
index
|
||||
}
|
||||
/// 检查索引覆盖的网格数量是否与场景相符;缺失缓存时允许重建。
|
||||
pub fn matches(&self, meshes: usize) -> bool {
|
||||
self.bounds.len() == meshes
|
||||
}
|
||||
/// 用包围盒筛出射线候选;索引查询按进入距离排序,精确三角形求交在调用方完成。
|
||||
pub fn ray(&self, ray: Ray) -> Vec<(f32, usize)> {
|
||||
let mut stack: Vec<_> = self.root.into_iter().collect();
|
||||
let mut output = Vec::new();
|
||||
@@ -223,6 +239,7 @@ impl Index {
|
||||
output.sort_unstable_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
|
||||
output
|
||||
}
|
||||
/// 用相机六平面保守判断包围盒可见性,返回保持原场景顺序的候选。
|
||||
pub fn visible(&self, camera: Camera, aspect: f32) -> Vec<usize> {
|
||||
let Some(basis) = camera.basis() else {
|
||||
return Vec::new();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 逐 Mount 的滚动状态、裁剪和输入目标选择。
|
||||
//! 共享组件的不同出现保留独立偏移。嵌套滚动先选择可接收位移的内层,偏移在实际内容范围内钳制。
|
||||
//!
|
||||
//! Per-occurrence scroll state and a single geometry/clip projection shared by
|
||||
//! native paint, source paint and input. No business object is copied.
|
||||
use crate::{
|
||||
@@ -12,6 +15,7 @@ use endor_core::{NativeContext, ObjectRef, Value, Vm, VmError, WeakObjectRef};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
/// 滚动容器允许响应的轴,不决定其业务内容组成方向。
|
||||
pub enum Axis {
|
||||
#[default]
|
||||
Both,
|
||||
@@ -19,6 +23,7 @@ pub enum Axis {
|
||||
Horizontal,
|
||||
}
|
||||
impl Axis {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Both => "both",
|
||||
@@ -26,6 +31,7 @@ impl Axis {
|
||||
Self::Horizontal => "horizontal",
|
||||
}
|
||||
}
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(name: &str) -> Result<Self, VmError> {
|
||||
match name {
|
||||
"both" => Ok(Self::Both),
|
||||
@@ -36,14 +42,17 @@ impl Axis {
|
||||
)),
|
||||
}
|
||||
}
|
||||
/// 判断滚动轴配置是否允许该方向移动,避免同时把禁用轴纳入滚动范围。
|
||||
pub fn horizontal(self) -> bool {
|
||||
self != Self::Vertical
|
||||
}
|
||||
/// 判断滚动轴配置是否允许该方向移动,避免同时把禁用轴纳入滚动范围。
|
||||
pub fn vertical(self) -> bool {
|
||||
self != Self::Horizontal
|
||||
}
|
||||
}
|
||||
|
||||
/// 求两个裁剪矩形的交集,空区域仍保持合法非负尺寸。
|
||||
fn intersection(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
let x = a[0].max(b[0]);
|
||||
let y = a[1].max(b[1]);
|
||||
@@ -54,6 +63,7 @@ fn intersection(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
(a[1] + a[3]).min(b[1] + b[3]).max(y) - y,
|
||||
]
|
||||
}
|
||||
/// 沿出现树累计滚动偏移和裁剪;遇到独立场景视口时按其有限边界重新建立投影范围。
|
||||
pub fn project(vm: &mut Vm, table: &MountTable, root: &UiNode, frame: &mut Frame) -> Result<()> {
|
||||
let mut nodes = HashMap::new();
|
||||
let mut work = vec![root];
|
||||
@@ -201,6 +211,7 @@ pub fn project(vm: &mut Vm, table: &MountTable, root: &UiNode, frame: &mut Frame
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 把源滚动值解码为有限坐标,阻止非数或越界值进入投影。
|
||||
pub fn coordinate(value: &Value) -> Result<f32, VmError> {
|
||||
let Value::Number(value) = value else {
|
||||
return Err(VmError::InvalidArgument(
|
||||
@@ -216,6 +227,7 @@ pub fn coordinate(value: &Value) -> Result<f32, VmError> {
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
/// 更新指定出现的滚动偏移,并钳制到本轴实际可滚动的内容范围。
|
||||
pub fn set(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -249,6 +261,7 @@ pub fn set(
|
||||
cx.replace_payload(owner, mount)?;
|
||||
cx.set_attr(desktop, "scrollRequested", Value::Bool(true))
|
||||
}
|
||||
/// 沿祖先滚动容器调整偏移,让目标出现进入可见范围。
|
||||
pub fn into_view(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -302,6 +315,7 @@ pub fn into_view(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 按逆出现顺序选择指针处可处理当前滚动方向的最内层容器。
|
||||
pub fn wheel_target(frame: &Frame, point: Point, delta: [f32; 2]) -> Option<u64> {
|
||||
let indices: HashMap<_, _> = frame.mounts.iter().map(|mount| (mount.id, mount)).collect();
|
||||
let mut current = frame
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
//! 把 DisplayList 写入 CPU 像素缓冲。裁剪栈限定矩形、文字和 3D 表面的实际写入区域。
|
||||
//! RenderScope 约束递归深度;累计像素预算限制嵌套 3D 和世界 UI 的中间表面分配。错误向上传递,不把半幅结果当成成功帧呈现。
|
||||
//!
|
||||
use crate::{
|
||||
display_list::{Color, DisplayList, DrawCommand},
|
||||
geometry::{Point, Rect},
|
||||
text::{self, TextStyle},
|
||||
};
|
||||
|
||||
/// 显示列表的 CPU 执行入口,错误在原生 present 前向上传递。
|
||||
pub struct SoftwareRenderer;
|
||||
|
||||
thread_local! {
|
||||
static RENDER_DEPTH:std::cell::Cell<usize>=const { std::cell::Cell::new(0) };
|
||||
static PIXEL_UNITS:std::cell::Cell<usize>=const { std::cell::Cell::new(0) };
|
||||
}
|
||||
/// 递归渲染的 RAII 深度保护,最外层还重置累计像素工作预算。
|
||||
pub(crate) struct RenderScope;
|
||||
impl RenderScope {
|
||||
/// 进入一次嵌套渲染并检查深度上限;最外层调用重新开始累计像素预算。
|
||||
pub fn enter() -> Result<Self, &'static str> {
|
||||
RENDER_DEPTH.with(|depth| {
|
||||
let current = depth.get();
|
||||
@@ -27,10 +33,12 @@ impl RenderScope {
|
||||
}
|
||||
}
|
||||
impl Drop for RenderScope {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
RENDER_DEPTH.with(|depth| depth.set(depth.get() - 1));
|
||||
}
|
||||
}
|
||||
/// 累加本帧中间表面工作量,超过预算立即返回错误。
|
||||
pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> {
|
||||
PIXEL_UNITS.with(|pixels| {
|
||||
let next = pixels.get().saturating_add(units);
|
||||
@@ -43,6 +51,7 @@ pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> {
|
||||
}
|
||||
|
||||
impl SoftwareRenderer {
|
||||
/// 顺序执行显示列表并维护裁剪栈;3D 和面板可递归进入同一渲染预算域。
|
||||
pub fn render(
|
||||
list: &DisplayList,
|
||||
target: &mut [u32],
|
||||
@@ -145,6 +154,7 @@ impl SoftwareRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 借用目标像素切片并维护当前裁剪,所有像素写入都受该上下文限制。
|
||||
struct Canvas<'a> {
|
||||
pixels: &'a mut [u32],
|
||||
width: u32,
|
||||
@@ -153,10 +163,12 @@ struct Canvas<'a> {
|
||||
}
|
||||
|
||||
impl Canvas<'_> {
|
||||
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
|
||||
fn clear(&mut self, color: Color) {
|
||||
self.pixels.fill(pack(color));
|
||||
}
|
||||
|
||||
/// 只遍历裁剪后的矩形像素,按圆角覆盖决定填充区域。
|
||||
fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
|
||||
let bounds = self.clamped_bounds(rect);
|
||||
let radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
|
||||
@@ -169,6 +181,7 @@ impl Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 用外轮廓与内轮廓的差绘制边框,避免把边框当成整个矩形填充。
|
||||
fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, width: f32, color: Color) {
|
||||
let bounds = self.clamped_bounds(rect);
|
||||
let width = width.max(1.0).min(rect.width / 2.0).min(rect.height / 2.0);
|
||||
@@ -194,6 +207,7 @@ impl Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把文字原点转换为局部裁剪区域,交给有界字形绘制,再混合到目标缓冲。
|
||||
fn draw_text(&mut self, origin: Point, value: &str, style: TextStyle, color: Color) {
|
||||
let x = origin.x.round() as i32;
|
||||
let y = origin.y.round() as i32;
|
||||
@@ -208,6 +222,7 @@ impl Canvas<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 把段落矩形与当前裁剪相交,按受限宽度绘制,再恢复外层裁剪。
|
||||
fn draw_wrapped_text(&mut self, rect: Rect, value: &str, style: TextStyle, color: Color) {
|
||||
if rect.width <= 0.0 || rect.height <= 0.0 {
|
||||
return;
|
||||
@@ -240,6 +255,7 @@ impl Canvas<'_> {
|
||||
self.clip = old;
|
||||
}
|
||||
|
||||
/// 把连续矩形转换成目标缓冲内的整数像素范围,避免在不可见区域进行光栅循环。
|
||||
fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) {
|
||||
(
|
||||
rect.x.max(self.clip.x).max(0.0).floor() as i32,
|
||||
@@ -255,6 +271,7 @@ impl Canvas<'_> {
|
||||
)
|
||||
}
|
||||
|
||||
/// 在画布与当前裁剪范围内进行透明度混合,防止文字/图形写出目标缓冲。
|
||||
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
|
||||
if x < 0
|
||||
|| y < 0
|
||||
@@ -288,6 +305,7 @@ impl Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试一个像素采样点是否位于圆角矩形内,角区域使用圆弧距离。
|
||||
fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
||||
if rect.width <= 0.0 || rect.height <= 0.0 {
|
||||
return false;
|
||||
@@ -306,6 +324,7 @@ fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
||||
dx * dx + dy * dy <= radius * radius
|
||||
}
|
||||
|
||||
/// 将颜色通道打包成目标缓冲的整数像素表示。
|
||||
fn pack(color: Color) -> u32 {
|
||||
(color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 通过真正的源/原生 hitTest 协议确定指针目标。
|
||||
//! 按逆绘制顺序筛选候选,并在同一只读 VM 调用与预算中比较按下、释放目标;透明或未命中的子项可以继续回退。
|
||||
//!
|
||||
//! Resolve pointer targets in paint order through source/native hitTest methods.
|
||||
//! Both press and release selectors share one read-only VM call and budget.
|
||||
use crate::{mount::Mount, ui_bridge::attach_desktop_type};
|
||||
@@ -7,12 +10,15 @@ use endor_core::{
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 验证 hitTest 返回的是 Bool,避免把任意真值隐式当成命中。
|
||||
struct Boolean;
|
||||
impl NativeCallable for Boolean {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Bool(value)] = args else {
|
||||
return Err(VmError::InvalidArgument("hitTest must return Bool"));
|
||||
@@ -21,12 +27,15 @@ impl NativeCallable for Boolean {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 比较按下与释放的选择结果,只有同一目标才能继续点击分发。
|
||||
struct SameTarget;
|
||||
impl NativeCallable for SameTarget {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(2))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let value = match args {
|
||||
[Value::Object(first), Value::Object(second)] if first == second => {
|
||||
@@ -37,6 +46,7 @@ impl NativeCallable for SameTarget {
|
||||
Ok(NativeResult::Return(value))
|
||||
}
|
||||
}
|
||||
/// 构造只读命中选择驱动所需的托管代码与函数对象。
|
||||
fn code(
|
||||
cx: &mut NativeContext<'_>,
|
||||
instructions: Vec<Instruction>,
|
||||
@@ -52,6 +62,7 @@ fn code(
|
||||
let globals = cx.new_dict()?;
|
||||
cx.new_function("pointer hitTest", code, globals)
|
||||
}
|
||||
/// 按逆绘制顺序构造候选 hitTest 调用,先进行几何与局部坐标筛选。
|
||||
fn selector(
|
||||
cx: &mut NativeContext<'_>,
|
||||
candidates: &[WeakObjectRef],
|
||||
@@ -137,6 +148,7 @@ fn selector(
|
||||
code(cx, instructions, constants, CodeLayout::default())
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 在一个只读调用中准备两次指针选择所需的候选与局部坐标。
|
||||
struct Prepare {
|
||||
candidates: Vec<WeakObjectRef>,
|
||||
press: [f32; 2],
|
||||
@@ -144,10 +156,12 @@ struct Prepare {
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Prepare {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let down = selector(cx, &self.candidates, self.press, self.desktop)?;
|
||||
let up = selector(cx, &self.candidates, self.release, self.desktop)?;
|
||||
@@ -192,6 +206,7 @@ impl NativeCallable for Prepare {
|
||||
}
|
||||
}
|
||||
/// Returned Mount owns one root. No-hit and mismatched targets return None.
|
||||
/// 解析输入所指向的实际目标,避免把包装身份与底层数据混淆。
|
||||
pub fn resolve(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 源 measure/arrange 协议的分片布局驱动器。自底向上的测量与自顶向下的安排共享一个只读 VM 调用域。
|
||||
//! Plan 只保存派生几何和求解过程;Measurement 缓存记录真实读取依赖。已有业务对象在只读阶段不能被修改。
|
||||
//! 较大树可在节点/指令量子之间挂起。Work 拥有尚未发布的执行状态,必须 resume 或 cancel;完成后才把 Draft 交给桥接层提交。
|
||||
//!
|
||||
//! Bottom-up measurement and top-down arrangement share one read-only VM call.
|
||||
//! The host scratch plan contains derived data only, never a second business heap.
|
||||
use crate::{
|
||||
@@ -22,9 +26,11 @@ use std::{
|
||||
// same per-occurrence VM driver so even intrinsic work can yield between nodes.
|
||||
const NATIVE_FAST_PATH_MAX_NODES: usize = 64;
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 读取当前 desktop 的类型字典,为布局协议结果提供名义类型检查。
|
||||
fn desktop_type_registry(cx: &NativeContext<'_>, desktop: ObjectRef) -> Result<ObjectRef, VmError> {
|
||||
match cx.store().get_attr(desktop, "types")? {
|
||||
Value::Object(object) => Ok(object),
|
||||
@@ -32,6 +38,7 @@ fn desktop_type_registry(cx: &NativeContext<'_>, desktop: ObjectRef) -> Result<O
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
/// 一次源测量的缓存输入、输出和真实读取依赖;不同约束或依赖版本不能复用。
|
||||
struct Measurement {
|
||||
owner: ObjectRef,
|
||||
method: ObjectRef,
|
||||
@@ -42,12 +49,14 @@ struct Measurement {
|
||||
}
|
||||
/// Borrowed identities and versions only: this cache adds no managed roots.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
/// 按 Mount 弱身份保存源测量结果,不为业务对象增加托管根。
|
||||
pub struct Cache {
|
||||
entries: HashMap<WeakObjectRef, Measurement>,
|
||||
pub hits: usize,
|
||||
pub misses: usize,
|
||||
}
|
||||
impl Cache {
|
||||
/// 从测量缓存依赖中移除宿主私有 desktop 记账对象,防止内部同步造成自我失效。
|
||||
pub(crate) fn exclude_host_desktops(&mut self, desktops: &HashSet<ObjectRef>) {
|
||||
for entry in self.entries.values_mut() {
|
||||
entry
|
||||
@@ -55,10 +64,12 @@ impl Cache {
|
||||
.retain(|read| !desktops.contains(&read.object()));
|
||||
}
|
||||
}
|
||||
/// 移除受影响 owner 的源测量缓存;其他出现继续按其读取版本检查。
|
||||
pub fn invalidate(&mut self, owners: &HashSet<ObjectRef>) {
|
||||
self.entries
|
||||
.retain(|_, entry| !owners.contains(&entry.owner));
|
||||
}
|
||||
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
self.hits = 0;
|
||||
@@ -66,6 +77,7 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 一个等待源测量返回的收集项,记录本次方法、约束和子项输入。
|
||||
struct Pending {
|
||||
token: usize,
|
||||
owner: ObjectRef,
|
||||
@@ -75,10 +87,13 @@ struct Pending {
|
||||
key: WeakObjectRef,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 未发布的布局求解工作区,包含出现树、期望尺寸、分配边界、收敛记录和缓存。
|
||||
struct Plan {
|
||||
// 使用出现索引保存树和几何,不为每个位置建立第二份业务对象。
|
||||
nodes: Vec<Arc<UiNode>>,
|
||||
children: Vec<Vec<usize>>,
|
||||
sizes: Vec<Size>,
|
||||
// preferred 是测量偏好,bounds 是父级实际分配;换行/flex 会使两者不同。
|
||||
preferred: Vec<Option<Size>>,
|
||||
bounds: Vec<Option<[f32; 4]>>,
|
||||
runtime: LayoutRuntime,
|
||||
@@ -92,16 +107,19 @@ struct Plan {
|
||||
native_arrange: Vec<bool>,
|
||||
custom_measure: Vec<bool>,
|
||||
measured_limits: Vec<[Option<f32>; 2]>,
|
||||
// 保存几何状态用于检测收敛和重复状态,防止相互依赖布局无限循环。
|
||||
previous: Option<Vec<u32>>,
|
||||
seen: BTreeSet<Vec<u32>>,
|
||||
}
|
||||
type Shared = Arc<Mutex<Plan>>;
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 源视觉调用当前处于测量还是安排阶段,决定调用方法与结果解码。
|
||||
enum Phase {
|
||||
Measure,
|
||||
Arrange,
|
||||
}
|
||||
impl Phase {
|
||||
/// 把当前布局阶段映射为 measure 或 arrange 方法名称。
|
||||
fn method(self) -> &'static str {
|
||||
match self {
|
||||
Self::Measure => "measure",
|
||||
@@ -110,6 +128,7 @@ impl Phase {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 为某次出现准备一个 measure/arrange 调用,选择原生快速结果或真正的源覆盖。
|
||||
struct Step {
|
||||
plan: Shared,
|
||||
index: usize,
|
||||
@@ -118,10 +137,12 @@ struct Step {
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Step {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mount = cx.store().payload::<Mount>(self.mount, "Mount")?.clone();
|
||||
let definition = cx
|
||||
@@ -293,6 +314,7 @@ impl NativeCallable for Step {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 接收并检查源协议返回结果,将其合入 Plan,而不是直接写成发布帧。
|
||||
struct Collect {
|
||||
plan: Shared,
|
||||
index: usize,
|
||||
@@ -300,10 +322,12 @@ struct Collect {
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Collect {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let pending = if matches!(self.phase, Phase::Measure) {
|
||||
self.plan
|
||||
@@ -496,14 +520,17 @@ impl NativeCallable for Collect {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 检查本轮布局是否已收敛,必要时安排下一次测量/分配迭代。
|
||||
struct Settle {
|
||||
plan: Shared,
|
||||
mounts: Arc<[WeakObjectRef]>,
|
||||
}
|
||||
impl NativeCallable for Settle {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let repeat = {
|
||||
let mut plan = self
|
||||
@@ -626,15 +653,18 @@ impl NativeCallable for Settle {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 准备当前求解步骤所需的约束和 Mount 临时上下文。
|
||||
struct Prepare {
|
||||
plan: Shared,
|
||||
mounts: Vec<WeakObjectRef>,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Prepare {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let advance = cx.allocate(ContextNativeFunction::new(
|
||||
"next layout occurrence",
|
||||
@@ -655,6 +685,7 @@ impl NativeCallable for Prepare {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 在同一驱动帧中安排下一项布局工作,使大树能够分片让出。
|
||||
struct Advance {
|
||||
plan: Shared,
|
||||
mounts: Arc<[WeakObjectRef]>,
|
||||
@@ -662,9 +693,11 @@ struct Advance {
|
||||
next: Mutex<usize>,
|
||||
}
|
||||
impl NativeCallable for Advance {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let count = self.mounts.len();
|
||||
let position = {
|
||||
@@ -741,6 +774,7 @@ impl NativeCallable for Advance {
|
||||
|
||||
/// An unpublished layout frame and derived caches. These are merged only after
|
||||
/// the driver completes; they are not another copy of application state.
|
||||
/// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。
|
||||
pub struct Draft {
|
||||
pub frame: Frame,
|
||||
pub runtime: LayoutRuntime,
|
||||
@@ -749,6 +783,7 @@ pub struct Draft {
|
||||
}
|
||||
|
||||
#[must_use = "pending layout work must be resumed or cancelled with its owning VM"]
|
||||
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
|
||||
pub enum Progress {
|
||||
Pending(Work),
|
||||
Complete(Draft),
|
||||
@@ -756,6 +791,7 @@ pub enum Progress {
|
||||
|
||||
/// Host-owned continuation. No borrowed frame/cache references cross a pause.
|
||||
/// The prepare handle owns one root until completion or explicit cancellation.
|
||||
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
|
||||
pub struct Work {
|
||||
state: Draft,
|
||||
plan: Shared,
|
||||
@@ -765,12 +801,14 @@ pub struct Work {
|
||||
}
|
||||
|
||||
impl Work {
|
||||
/// 恢复当前 Work 拥有的 VM 续体,继续同一轮求解而不是重放 measure/arrange。
|
||||
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
|
||||
self.slices += 1;
|
||||
let progress = vm.resume_read_only_call(slice);
|
||||
self.accept(vm, progress)
|
||||
}
|
||||
|
||||
/// 显式结束布局续体和只读域,释放本次尚未提交的临时根。
|
||||
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
|
||||
let cancelled = vm.cancel_read_only_call();
|
||||
let released = vm.release(self.prepare);
|
||||
@@ -779,6 +817,7 @@ impl Work {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 接收一次续体进度,挂起时保留工作,完成时将派生结果交给收尾阶段。
|
||||
fn accept(
|
||||
self,
|
||||
vm: &mut Vm,
|
||||
@@ -805,6 +844,7 @@ impl Work {
|
||||
}
|
||||
}
|
||||
|
||||
/// 建立只读布局 Plan 与托管驱动函数;小型原生树可走直接路径,大树保留分片推进能力。
|
||||
pub fn start(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
@@ -945,6 +985,7 @@ pub fn start(
|
||||
work.accept(vm, progress)
|
||||
}
|
||||
|
||||
/// 验证求解已收敛,收集最终几何、读取依赖和缓存,返回尚待桥接层提交的 Draft。
|
||||
fn finish(
|
||||
vm: &mut Vm,
|
||||
mut work: Work,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 执行源 paint 覆盖并解码受检查的 PaintCommand。
|
||||
//! 所有绘制回调位于只读域;临时返回对象可分配,但已有模型不可写。命令数量、类型、坐标和颜色在合入帧前检查。
|
||||
//! 源命令与原生命令按 Mount 绘制顺序合并,随后装配世界 UI 面板。挂起绘制同样需要显式恢复或取消。
|
||||
//!
|
||||
//! Source paint dispatch produces checked immutable commands. One read-only VM
|
||||
//! resumable call sequences all overrides, sharing one instruction/frame budget.
|
||||
use crate::{
|
||||
@@ -17,9 +21,11 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||||
fn object(value: Value) -> Result<ObjectRef, VmError> {
|
||||
if let Value::Object(object) = value {
|
||||
Ok(object)
|
||||
@@ -27,6 +33,7 @@ fn object(value: Value) -> Result<ObjectRef, VmError> {
|
||||
Err(invalid("paint must return managed objects"))
|
||||
}
|
||||
}
|
||||
/// 把当前源值或文本选择解码成此调用需要的文本表示。
|
||||
fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<String, VmError> {
|
||||
if let Value::Text(value) = store.get_attr(object, name)? {
|
||||
Ok(value)
|
||||
@@ -34,6 +41,7 @@ fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<String, Vm
|
||||
Err(invalid("paint field must be text"))
|
||||
}
|
||||
}
|
||||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||||
fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmError> {
|
||||
let Value::Number(value) = store.get_attr(object, name)? else {
|
||||
return Err(invalid("paint field must be a Number"));
|
||||
@@ -50,6 +58,7 @@ fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmE
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
/// 从当前 desktop 的类型注册表取得绘制结果必须符合的托管类型。
|
||||
fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<ObjectRef, VmError> {
|
||||
let registry = object(store.get_attr(desktop, "types")?)?;
|
||||
object(
|
||||
@@ -58,6 +67,7 @@ fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<Obj
|
||||
.ok_or(invalid("missing paint type"))?,
|
||||
)
|
||||
}
|
||||
/// 把源方法返回的托管绘制值解码成受限 Rust 命令,检查名义类型、字段和绘制预算。
|
||||
fn decode(
|
||||
store: &ObjectStore,
|
||||
command: ObjectRef,
|
||||
@@ -150,22 +160,26 @@ fn decode(
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 按出现 ID 收集已检查的源绘制覆盖,随后与原生默认命令合并。
|
||||
struct PaintOutput {
|
||||
commands: BTreeMap<u64, Vec<RenderCommand>>,
|
||||
count: usize,
|
||||
}
|
||||
type Output = Arc<Mutex<PaintOutput>>;
|
||||
#[derive(Debug)]
|
||||
/// 检查源 paint 返回列表并将命令保存到当前出现的绘制结果。
|
||||
struct Collect {
|
||||
desktop: ObjectRef,
|
||||
mount: u64,
|
||||
output: Output,
|
||||
}
|
||||
impl NativeCallable for Collect {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Object(result)] = args else {
|
||||
return Err(invalid("paint must return a DisplayList"));
|
||||
@@ -195,16 +209,19 @@ impl NativeCallable for Collect {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 为当前出现准备 paint 方法、几何和类型检查上下文。
|
||||
struct Prepare {
|
||||
desktop: ObjectRef,
|
||||
mounts: Vec<(u64, WeakObjectRef)>,
|
||||
output: Output,
|
||||
}
|
||||
impl NativeCallable for Prepare {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let advance = cx.allocate(ContextNativeFunction::new(
|
||||
"next paint occurrence",
|
||||
@@ -225,6 +242,7 @@ impl NativeCallable for Prepare {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 安排下一个出现的绘制调用,保持原始绘制顺序和共享 VM 预算。
|
||||
struct Advance {
|
||||
desktop: ObjectRef,
|
||||
mounts: Arc<[(u64, WeakObjectRef)]>,
|
||||
@@ -232,9 +250,11 @@ struct Advance {
|
||||
output: Output,
|
||||
}
|
||||
impl NativeCallable for Advance {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let occurrence = {
|
||||
let mut next = self
|
||||
@@ -304,6 +324,7 @@ impl NativeCallable for Advance {
|
||||
}
|
||||
}
|
||||
|
||||
/// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。
|
||||
pub struct Draft {
|
||||
pub frame: Frame,
|
||||
pub reads: Vec<endor_core::ReadDependency>,
|
||||
@@ -311,12 +332,14 @@ pub struct Draft {
|
||||
}
|
||||
|
||||
#[must_use = "pending paint must be resumed or cancelled on its owning VM"]
|
||||
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
|
||||
pub enum Progress {
|
||||
Pending(Box<Work>),
|
||||
Complete(Box<Draft>),
|
||||
}
|
||||
|
||||
/// Only unpublished native commands and an owning prepare root cross a pause.
|
||||
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
|
||||
pub struct Work {
|
||||
desktop: ObjectRef,
|
||||
prepare: ObjectRef,
|
||||
@@ -327,12 +350,14 @@ pub struct Work {
|
||||
}
|
||||
|
||||
impl Work {
|
||||
/// 继续当前绘制续体,保留其先前收集的命令和所属窗口上下文。
|
||||
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
|
||||
self.slices += 1;
|
||||
let progress = vm.resume_read_only_call(slice);
|
||||
self.accept(vm, progress)
|
||||
}
|
||||
|
||||
/// 关闭未完成绘制续体并释放其临时根,不发布半完成命令列表。
|
||||
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
|
||||
let cancelled = vm.cancel_read_only_call();
|
||||
let released = vm.release(self.prepare);
|
||||
@@ -341,6 +366,7 @@ impl Work {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 区分源绘制续体是挂起还是完成,保留工作或结束只读域并生成 Draft。
|
||||
fn accept(
|
||||
self,
|
||||
vm: &mut Vm,
|
||||
@@ -368,6 +394,7 @@ impl Work {
|
||||
}
|
||||
}
|
||||
|
||||
/// 建立本轮只读绘制驱动和结果收集器;源覆盖与原生默认绘制最终保持同一出现顺序。
|
||||
pub fn start(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
@@ -411,6 +438,7 @@ pub fn start(
|
||||
work.accept(vm, result)
|
||||
}
|
||||
|
||||
/// 按 Mount 合并源覆盖和原生命令,补齐投影/裁剪表,再递归装配世界 UI 面板。
|
||||
fn finish(vm: &mut Vm, mut work: Work, reads: Vec<endor_core::ReadDependency>) -> Result<Progress> {
|
||||
let output = work
|
||||
.output
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 统一字体发现、文字整形、测量与抗锯齿字形绘制。
|
||||
//! 测量与绘制使用同一字体系统和样式,避免布局宽度与屏幕文字不一致;中文与其他字符通过字体回退处理。
|
||||
//! 整形和字形缓存有数量/字节预算;极端放大采用有界字形 LOD,最终只遍历裁剪区域内的像素。
|
||||
//!
|
||||
//! Shared font discovery, shaping, measurement and antialiased rasterization.
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -9,12 +13,14 @@ use cosmic_text::{Align, Attrs, Buffer, Family, FontSystem, Metrics, Shaping, Sw
|
||||
use crate::display_list::Color;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
/// 界面比例字体与源码等宽字体的选择,不直接指定某个必然安装的系统字体。
|
||||
pub enum TextFamily {
|
||||
Interface,
|
||||
Code,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 字号、行高和字体类别,测量与绘制必须使用兼容样式。
|
||||
pub struct TextStyle {
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
@@ -22,6 +28,7 @@ pub struct TextStyle {
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
/// 生成界面文字的基准字号与行高,size 是协议使用的字号倍率。
|
||||
pub fn ui(size: u32) -> Self {
|
||||
Self {
|
||||
font_size: 14.0 * size as f32,
|
||||
@@ -30,6 +37,7 @@ impl TextStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成源码/Console 使用的等宽风格基础尺寸。
|
||||
pub fn code() -> Self {
|
||||
Self {
|
||||
font_size: 13.0,
|
||||
@@ -38,6 +46,7 @@ impl TextStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// 同比缩放字号与行高,保持设备比例变化后的排版比例一致。
|
||||
pub fn scaled(self, scale: f32) -> Self {
|
||||
Self {
|
||||
font_size: self.font_size * scale,
|
||||
@@ -48,12 +57,14 @@ impl TextStyle {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 整形后实际占用的逻辑文字尺寸。
|
||||
pub struct TextSize {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
/// 文字整形缓存键,包含文本、样式和可选换行宽度。
|
||||
struct TextKey {
|
||||
value: String,
|
||||
font_size: u32,
|
||||
@@ -62,11 +73,13 @@ struct TextKey {
|
||||
wrap_width: Option<u32>,
|
||||
}
|
||||
|
||||
/// 已整形 Buffer 与测量结果,后续测量/绘制复用同一布局。
|
||||
struct PreparedText {
|
||||
buffer: Buffer,
|
||||
size: TextSize,
|
||||
}
|
||||
|
||||
/// 共享字体系统、字形缓存和整形缓存;调用通过全局锁串行访问。
|
||||
struct TextEngine {
|
||||
fonts: FontSystem,
|
||||
code_family: Option<String>,
|
||||
@@ -76,6 +89,7 @@ struct TextEngine {
|
||||
}
|
||||
|
||||
impl TextEngine {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
fn new() -> Self {
|
||||
let mut fonts = FontSystem::new();
|
||||
// Select installed, readable families rather than assuming an OS font
|
||||
@@ -114,10 +128,12 @@ impl TextEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// 对不指定换行宽度的文本复用统一整形准备过程。
|
||||
fn prepare(&mut self, value: &str, style: TextStyle) -> TextKey {
|
||||
self.prepare_width(value, style, None)
|
||||
}
|
||||
|
||||
/// 为文本、字体样式和可选换行宽度建立整形缓存,超过缓存预算先清理旧结果。
|
||||
fn prepare_width(&mut self, value: &str, style: TextStyle, wrap_width: Option<f32>) -> TextKey {
|
||||
let key = TextKey {
|
||||
value: value.to_owned(),
|
||||
@@ -175,17 +191,20 @@ impl TextEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// 取得全局受锁保护的字体/整形引擎;共享缓存不等于可以并行修改字体状态。
|
||||
fn engine() -> &'static Mutex<TextEngine> {
|
||||
static ENGINE: OnceLock<Mutex<TextEngine>> = OnceLock::new();
|
||||
ENGINE.get_or_init(|| Mutex::new(TextEngine::new()))
|
||||
}
|
||||
|
||||
/// 返回不受换行宽度约束时的字体整形尺寸。
|
||||
pub fn measure(value: &str, style: TextStyle) -> TextSize {
|
||||
let mut engine = engine().lock().expect("text engine lock");
|
||||
let key = engine.prepare(value, style);
|
||||
engine.buffers[&key].size
|
||||
}
|
||||
|
||||
/// 按可用宽度整形并测量多行高度,供布局与实际段落绘制共用。
|
||||
pub fn measure_wrapped(value: &str, style: TextStyle, width: f32) -> TextSize {
|
||||
if width <= 0.0 {
|
||||
return TextSize {
|
||||
@@ -201,6 +220,7 @@ pub fn measure_wrapped(value: &str, style: TextStyle, width: f32) -> TextSize {
|
||||
/// Rasterize only visible glyph pixels. Large camera zooms use a bounded glyph
|
||||
/// bitmap and sample it at the requested size instead of allocating an enormous
|
||||
/// font image. Layout and wrapping still use proportionally identical metrics.
|
||||
/// 限制字形分辨率并只采样可见像素;保留文字布局比例,同时避免极端相机缩放产生巨型位图。
|
||||
pub fn draw_clipped(
|
||||
value: &str,
|
||||
style: TextStyle,
|
||||
@@ -300,6 +320,7 @@ pub fn draw_clipped(
|
||||
}
|
||||
|
||||
/// Fit a single displayed source line without slicing a UTF-8/grapheme cluster.
|
||||
/// 按真实字体测量截断到给定宽度,以字素簇边界拼接省略号,避免切开组合字符。
|
||||
pub fn fit(value: &str, width: f32, style: TextStyle) -> String {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 独立 2D 相机与可逆投影。视口尺寸属于布局;平移、缩放属于视图,不改变世界对象的逻辑位置。
|
||||
//! project 为每次出现附加组合变换、裁剪和嵌套面板映射;绘制与命中共用这些结果。fit 根据有限内容边界调整指定相机。
|
||||
//!
|
||||
//! Reversible projections keep camera movement independent of layout.
|
||||
pub mod spatial;
|
||||
use crate::geometry::{Point, Rect};
|
||||
@@ -6,21 +9,27 @@ use endor_core::{ObjectPayload, ObjectRef, ObjectStore, VmError};
|
||||
/// Marker payload gives cameras a native nominal identity while their fields
|
||||
/// remain ordinary traced, bindable VM attributes.
|
||||
#[derive(Debug)]
|
||||
/// 托管 2D 相机的名义标记,实际位置/缩放是被追踪的普通属性。
|
||||
pub struct Camera2D;
|
||||
impl ObjectPayload for Camera2D {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"camera-2d"
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, _: &mut dyn FnMut(ObjectRef)) {}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 从 VM 提取的独立相机数值描述,渲染与布局投影不直接读取托管堆。
|
||||
pub struct CameraView {
|
||||
pub identity: String,
|
||||
pub x: f32,
|
||||
@@ -28,6 +37,7 @@ pub struct CameraView {
|
||||
pub zoom: f32,
|
||||
}
|
||||
|
||||
/// 读取并校验托管 Camera2D 的位置和缩放,生成原生投影描述。
|
||||
pub fn camera(store: &ObjectStore, object: ObjectRef) -> Result<CameraView, VmError> {
|
||||
store.payload::<Camera2D>(object, "Camera2D")?;
|
||||
let x = crate::scroll::coordinate(&store.get_attr(object, "x")?)?;
|
||||
@@ -48,6 +58,7 @@ pub fn camera(store: &ObjectStore, object: ObjectRef) -> Result<CameraView, VmEr
|
||||
|
||||
/// Fit finite scene bounds using this occurrence's allocation. Sharing a camera
|
||||
/// is explicit, so fitting one view then intentionally affects all its users.
|
||||
/// 根据这个 CanvasViewport 出现的有限内容范围调整其相机;显式共享相机会同步影响其他视口。
|
||||
pub fn fit(
|
||||
cx: &mut endor_core::NativeContext<'_>,
|
||||
mount: &crate::mount::Mount,
|
||||
@@ -127,12 +138,14 @@ pub fn fit(
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 可组合的均匀缩放与平移;正向绘制和逆向命中必须使用同一结果。
|
||||
pub struct Transform {
|
||||
pub zoom: f32,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
impl Default for Transform {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
zoom: 1.0,
|
||||
@@ -142,15 +155,18 @@ impl Default for Transform {
|
||||
}
|
||||
}
|
||||
impl Transform {
|
||||
/// 将逻辑点按当前缩放和平移变换到外层视图。
|
||||
pub fn point(self, point: Point) -> Point {
|
||||
Point::new(point.x * self.zoom + self.x, point.y * self.zoom + self.y)
|
||||
}
|
||||
/// 将视图点反算回逻辑坐标,供命中和手势锚点计算使用。
|
||||
pub fn inverse(self, point: Point) -> Point {
|
||||
Point::new(
|
||||
(point.x - self.x) / self.zoom,
|
||||
(point.y - self.y) / self.zoom,
|
||||
)
|
||||
}
|
||||
/// 使用同一缩放和平移转换矩形的位置与尺寸。
|
||||
pub fn rect(self, rect: Rect) -> Rect {
|
||||
let point = self.point(Point::new(rect.x, rect.y));
|
||||
Rect::new(
|
||||
@@ -160,6 +176,7 @@ impl Transform {
|
||||
rect.height * self.zoom,
|
||||
)
|
||||
}
|
||||
/// 将当前变换再放入外层变换,保持绘制与逆向命中的组合顺序一致。
|
||||
pub fn then(self, outer: Self) -> Self {
|
||||
Self {
|
||||
zoom: self.zoom * outer.zoom,
|
||||
@@ -167,6 +184,7 @@ impl Transform {
|
||||
y: self.y * outer.zoom + outer.y,
|
||||
}
|
||||
}
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(self) -> bool {
|
||||
self.zoom.is_finite()
|
||||
&& self.zoom > 0.0
|
||||
@@ -178,6 +196,7 @@ impl Transform {
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算视口与祖先裁剪的共同可见矩形。
|
||||
fn intersect(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
let x = a[0].max(b[0]);
|
||||
let y = a[1].max(b[1]);
|
||||
@@ -189,6 +208,7 @@ fn intersect(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
]
|
||||
}
|
||||
|
||||
/// 遍历出现树,组合相机与父视图变换,建立裁剪和世界 UI 映射,最后重建屏幕命中索引。
|
||||
pub fn project(
|
||||
root: &crate::ui_bridge::UiNode,
|
||||
frame: &mut crate::document::Frame,
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
//! 有界屏幕网格,用来减少普通二维命中和相机目标查询的候选。
|
||||
//! 巨大对象和 3D 面板进入后备集合,再由精确命中判断;索引预算不足不能导致遗漏目标。反序列化或索引长度不匹配时可退回完整候选。
|
||||
//!
|
||||
//! Bounded screen-space broad phase. Projective world UI uses the precise
|
||||
//! ray-based narrow phase rather than an unsafe screen rectangle approximation.
|
||||
use crate::{document::MountGeometry, geometry::Point};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 有限屏幕格子和后备集合。候选筛选允许保守多取,但不能因索引预算漏掉有效命中。
|
||||
pub struct Index {
|
||||
count: usize,
|
||||
cells: HashMap<(i32, i32), Vec<usize>>,
|
||||
fallback: Vec<usize>,
|
||||
}
|
||||
impl Index {
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub fn build(mounts: &[MountGeometry]) -> Self {
|
||||
let mut result = Self {
|
||||
count: mounts.len(),
|
||||
@@ -66,6 +71,7 @@ impl Index {
|
||||
}
|
||||
result
|
||||
}
|
||||
/// 返回当前网格格子与后备集合中的候选,去重并恢复逆绘制顺序。
|
||||
pub fn candidates(&self, point: Point, count: usize) -> Vec<usize> {
|
||||
if self.count != count {
|
||||
return (0..count).rev().collect();
|
||||
@@ -79,6 +85,7 @@ impl Index {
|
||||
output
|
||||
}
|
||||
}
|
||||
/// 把屏幕坐标映射到有界整数网格坐标,处理负方向与极端数值。
|
||||
fn cell(value: f32) -> i32 {
|
||||
(value / 128.)
|
||||
.floor()
|
||||
|
||||
Reference in New Issue
Block a user