15 KiB
UI、场景、视口与摄像机
本次完整实现目标的持续记录。完成布局并不等于完成场景或原生交互验收。
边界
- Page 负责页面身份、生命周期和根内容。Row、Column、Overlay 负责有限盒子内的排版。
- Scene2D、Scene3D 管理世界对象。世界不是一个无限宽高的布局盒子。
- CanvasViewport、Viewport3D 是有限的 UI 盒子,可以加入 Row、Column、Overlay,也可以放进世界内的有限 UI 面板。
- Camera2D、透视和正交摄像机是显式对象。视口保存自己的尺寸、裁剪和投影比例;共享场景不隐式共享摄像机。
- 摄像机控制器独立于摄像机数据。移动相机只改变投影、可见性和命中坐标,不应重新排版无关内容。
- 视口覆盖 UI 不随世界移动。世界内 UI 面板拥有有限大小,使用普通布局,再由对应的世界变换呈现。
- 嵌套视口的输入逐级逆变换,尊重裁剪和捕获;同一手势不能同时驱动内外相机。
- 无限导航受数值、可见对象、递归、内存和绘制预算约束;引用环必须在发布部分结果前拒绝。
当前实现状态
| 阶段 | 状态 | 证据 / 剩余工作 |
|---|---|---|
| 基线审查 | 完成 | 保留共享 Block / 独立 Mount、源协议和已合并工作区示例 |
| 嵌套布局与盒模型 | 完成 | Row / Column / Overlay、双轴对齐、子项覆盖、外边距、非对称内边距和边框、尺寸策略、裁剪;见下面接口 |
| 独立 2D 场景与视口 | 完成 | 独立场景/摄像机、多视口及嵌套、覆盖 UI、投影/命中、原生输入路由、真实 HTTP 与像素回归;旧 Page.mode 仅保留兼容 |
| 3D 场景、视口和摄像机 | 完成 | 软件深度缓冲、网格/变换/灯光、透视/正交相机、可替换控制器、射线拾取、世界空间 UI 及 2D/3D 嵌套 |
| 可见性与资源防护 | 完成 | 2D 屏幕索引、3D BVH / 视锥剔除、场景缓存、连续原生事件版本更新、字形 LOD 和嵌套资源预算 |
| 原生综合验收 | 可取得检查完成 | release 构建与实际窗口截图,运行实例的按钮 / 旋转 / 缩放事件及过期拒绝均通过;系统不允许模拟鼠标,未冒充硬件验收 |
| 完成交付 | 已提交可评审 PR | PR #5 已移除 Draft,Gitea 返回 mergeable=true;等待用户手动合并 |
有限盒子的接口
现有 Row(padding, gap, children...)、Column(padding, gap, children...) 保留,新增 Overlay(padding, gap, children...)。Overlay 为构造协议兼容保留 gap 参数,但叠放不使用 gap;子项顺序决定绘制和命中顺序。
mainAlign:start、center、end、space-between。Overlay 对应横向对齐,不使用 space-between 分散规则。crossAlign:start、center、end、stretch。Row 对应纵向;Column 对应横向。Overlay 对应纵向。alignSelf:auto继承所在父容器的 crossAlign,或以上交叉轴选项。属性在对象上,每个 Mount 根据自己的父容器解释。marginTop/Right/Bottom/Left: 外边距参与父容器的测量、主轴分配和对齐。不折叠外边距,不支持负外边距。paddingTop/Right/Bottom/Left: 数值覆盖对应边;auto回退到构造参数或容器的 padding 属性。非容器回退为 0。borderTop/Right/Bottom/Left和borderColor: 边框占据 border box 内侧,压缩内容空间。原生绘制产生实心直角边框;自定义 paint 可调用基类绘制,也可自行提供完整外观。boxWidth/boxHeight:auto、非负数值或fill。保留 RectBlock / ScrollBlock 原有 width/height 和源子类字段语义,通用盒尺寸使用独立名称。数值表示 border box,不包含外边距。minWidth/minHeight/maxWidth/maxHeight: 盒尺寸边界,最低值不得超过最高值。父分配小于最低值时,外部有限约束仍然生效。- 主轴
fill使用零基准分享剩余空间;未显式设置 flex 时权重为 1。最大值饱和后,剩余空间继续分给其他项目。交叉轴 fill 使用父容器提供的有限内容尺寸。没有有限父分配的 fill 报错。 stretch仅拉伸交叉轴 auto 尺寸,遵守盒尺寸边界;数值固定尺寸保留。显式源 arrange 返回的布局始终优先。overflow:visible或clip;后者同时裁剪后代绘制和指针命中。需要滚动时使用 ScrollBlock,滚动轴与内层 Row/Column 排版独立。
尺寸、边距和边框必须有限、非负,受现有 1000000 上限限制。根约束、每个 Mount 的偏好尺寸、分配、收敛判定及源协议缓存均保持独立。示例见 box-layout.endor。
布局阶段验证:本地 cargo test --locked 的 162 项回归全部通过,cargo clippy --all-targets --locked -- -D warnings、格式与差异空白检查通过。新增用例执行实际 Endor 脚本,覆盖共享对象在不同父容器中的对齐、实时边距变更、flex 分配、叠放、尺寸边界、文本换行、有限 fill、无界 fill 拒绝、源 arrange 优先、裁剪命中和可运行示例。本阶段尚未重新打包或进行原生指针验收。
2D 场景与视口
Scene2D(children...) 持有世界内容。直接子项的 x/y 是世界位置;Row/Column/Overlay 仍然只解释自己的有限内容布局。给世界内卡片设置 boxWidth,即可在其内部换行和嵌套排版。
CanvasViewport(width, height, scene) 是可参加普通排版的有限盒子,世界范围不参与它的本征尺寸。视口可使用通用盒模型尺寸、边框、内边距和 flex。每个新建视口拥有自己的 Camera2D();显式赋值 second.camera = first.camera 才会共享视角。摄像机 x/y 是可视内容左上角的世界位置,zoom 范围 0.01–100;也可以 Camera2D(x, y, zoom) 创建。嵌套投影还受合成变换预算限制。
视口的有限内容框裁剪世界绘制和输入,Overlay 中的兄弟 UI 保持固定。场景内可以放另一个 CanvasViewport:布局坐标不变,投影逐层合成,指针逆变换至目标组件坐标。相机改变时复用不变的测量结果。view.mounts().at(index).fitView() 使用指定 Mount 的有限尺寸及场景直接子项的有限边界适配摄像机,不把无限世界当作排版边界。
滚轮优先交给命中的可滚动 ScrollView;其余平移交给最内层画布。空格加左键或中键拖动锁定开始时的视口,不因指针跨到另一个视口而切换相机;捏合及 Cmd/Ctrl+滚轮围绕指针缩放。viewport-pan / viewport-zoom 使用真实 Mount 身份及帧版本,通过 HTTP worker 串行修改所属 VM 的摄像机。非法缩放锚点在进入 VM 前拒绝,过期帧返回 409。
ScrollView 是现有 ScrollBlock 的名义子类型别名,滚动轴和其内容 Row/Column 排版独立。默认开始页已使用普通 Page + 独立场景/视口,纵横示例使用 ScrollView 对应的滚动容器组合;不再设置 Page.mode。原有 Page.mode/panX/panY/zoom 暂保留兼容旧脚本,不用于新架构示例。
示例见 scene-viewports.endor。回归包含两个视口共享同一业务对象、显式共享相机、嵌套变换、真实点击回调、相机修改时测量数为 0、真实 HTTP 链路及软件渲染像素中的缩放/裁剪。它们不能替代原生鼠标和触控板验收。
2D 阶段最终验证:169 项本地回归全部通过,all-target Clippy 拒绝 warnings 通过,编辑器未报告错误。默认页的真实按钮事件也已迁移验证。未将原生窗口验收记为完成。
3D 基础接口与当前边界
Scene3D(nodes...)管理世界对象;Group3D(nodes...)组合变换。children是 ObjectList,可显式替换;共享节点可以出现多次,路径循环会拒绝。Box3D(width,height,depth,color)创建网格;Geometry3D(ObjectList(Vec3(...),...), ObjectList(Triangle3D(a,b,c),...))创建不可变顶点/索引缓冲,再用Mesh3D(geometry,color)实例化。可替换 mesh.geometry;修改原输入 Vec3 不会改写已建立的不可变几何。- 网格/组有 x/y/z、rotationX/Y/Z(角度)、scaleX/Y/Z;局部缩放、X/Y/Z 旋转、平移依序应用,再继承父组变换。
translate(dx,dy,dz)、rotate(dx,dy,dz)可在 Endor 中直接调用。 DirectionalLight(dx,dy,dz,color,intensity)使用传播方向,Scene3D.ambient 为基础照明。当前材质是完全不透明的平面着色。Viewport3D(width,height,scene)是普通有限 UI 盒子,可以放入 Row、Column、Overlay 或 Scene2D 中。背景与 3D 内容在独立表面上渲染,再交给现有原生软件合成器;采用实际深度缓冲、六面视锥裁剪,没有用静态图替代三维场景。- 每个新 Viewport3D 有独立 PerspectiveCamera;可用
view.camera = OrthographicCamera()替换,或显式共享同一个 camera。摄像机字段为 x/y/z、targetX/Y/Z、upX/Y/Z、near/far、透视 fov(角度)、正交 height。宽高比由每次视口投影提供,不写回共享摄像机。camera.translate(...)同时平移位置与观察目标。 OrbitController()独立于摄像机数据,默认绑定到新视口。左键拖动旋转,中键/空格拖动平移,滚轮/捏合缩放。view.controller 可替换为提供update(camera, gesture)的 Endor 对象,或设为字符串none禁用。CameraGesture 提供 kind、dx/dy、factor、width/height、pointX/pointY。透视缩放改变观察距离,正交缩放改变 height。view.mounts().at(index).pick3D(x,y)接受投影前页面空间中的指针坐标,返回 RayHit3D 或空值;结果包含实际托管 mesh、世界 point/normal、distance、triangle 和 barycentric。原生指针点击对应视口时,标准 click Event.hit 提供相同结果;点击空白时为空。拾取与绘制共享摄像机和变换,遵守 near/far。
固定 Aster 版本不支持数值算术表达式,不能用 x = x + dx 编译控制器。因此示例使用可运行的原生 translate/rotate 操作;自定义控制器可以调用这些方法或直接设置摄像机字段。未修改 Aster 源码或 gitlink。
3D 表面最长边按需降采样至 2048,单表面不超过 4194304 像素;缓存最多 8 个不可变表面、总计 8388608 像素。三角形、顶点、场景节点、灯光、光栅工作和摄像机范围均有检查;超过光栅预算返回错误,不发布截断的半幅结果。降采样保留原始视口宽高比,使绘制与拾取使用相同投影。
示例见 scene3d.endor。测试覆盖真正的 Endor 构造/修改、最终渲染像素、深度顺序、透视/正交独立视图、近面裁剪、射线回调、可替换控制器的真实 HTTP 路由以及循环/非法几何拒绝。原生硬件交互尚未验收。
3D 基础阶段检查:176 项本地回归全部通过,all-target Clippy 拒绝 warnings 通过。后续世界空间 UI 与嵌套资源/可见性工作尚未完成,因此整体目标及 PR 仍处于进行中。
世界空间 UI 与组合
Panel3D(worldWidth, worldHeight, content) 把普通 Endor Block 放进 3D 场景。面板有独立的 uiWidth/uiHeight(默认 512×320,整数 1–2048)和不透明 background;本次 UI 表面不实现透明材质。世界尺寸控制平面几何,UI 尺寸提供有限排版约束,两者互不代替。面板与组/网格一样支持位置、旋转、缩放及 translate/rotate。
内容仍是同一个托管对象树,正常执行 measure/arrange/paint/hitTest;每次出现有自己的 Mount、有限尺寸、滚动状态和纹理。一个组件可以放进多个不同尺寸的面板,不复制其业务状态。3D 摄像机移动不改变 UI 的排版尺寸。UI 绘制指令按面板收集,透视裁剪时保留 UV,并按深度进行透视校正插值;普通网格会正确遮挡面板。
指针先沿视口摄像机发射射线,再沿可见面板的逆世界变换映射到 UI 像素,然后继续通过内部 CanvasViewport 的逆变换。被其他网格挡住的 UI 无法收到指针命中。拖动捕获使用平面的连续逆投影,因此指针移出面板后不会把同一手势交给另一个摄像机。Mount.toView(x,y) 将组件本地坐标转到页面视图坐标;toLocal(x,y) 执行反向转换,无法命中可见面板时返回空值。pick3D 另提供实际 node/mesh 和实例编号。
面板 UI 可以继续嵌入 CanvasViewport 或 Viewport3D,后者也可以再包含 Panel3D;普通 Overlay 的兄弟 UI 仍固定在其 UI 空间。引用环在派生布局前拒绝;世界 UI 最多嵌套 8 层,光栅/纹理像素采用统一总预算。滚动视口与普通画布的世界内容不会撑大外层有限视口或外层滚动范围。
示例见 world-ui.endor。世界 UI 阶段的 181 项完整本地回归和 all-target Clippy 均通过;新增回归覆盖真正的像素、托管回调、材质遮挡、多面板独立尺寸、再次嵌入 3D、真实 HTTP 射线/UV 摄像机输入、循环和递归预算。
可见性、连续输入与资源
3D 场景快照构建世界 AABB 的 BVH,摄像机变化复用相同的不可变场景和索引。视锥查询剔除不可见网格,射线查询按近到远缩小候选;序列化不携带缓存,反序列化后可重建。几何或变换变化会生成新场景和新索引。
普通 2D 交互和相机目标使用 128px 屏幕网格。每个对象最多进入 256 格,索引总计最多 65536 项;巨大矩形和项目化的 3D 面板 UI 进入精确命中的后备集合。后备集合仍遵循原有裁剪、投影和反向绘制顺序,不因近面投影或索引预算遗漏对象。源 hitTest 仍逐一执行其真实协议,未更改源覆盖行为。
原生输入使用有界 64 项队列。同一个运行时的连续 viewport / scene / scroll 事件在出队时更新到当前帧版本,保留原窗口、会话和捕获 Mount;目标消失、控制器禁用、运行时更换或失败时丢弃。所有窗口共用同一事件客户端和权威文档。离散点击及外部 HTTP 请求仍严格拒绝过时版本,不自动重放业务动作。队列满时仍可拒绝新输入,不保证任意输入速率下无损。
文字先裁剪再遍历可见字形像素;字体超过 512 设备像素时使用有界字形位图采样,避免高倍率缩放产生巨大单字形分配。字形缓存逐字检查 4096 项 / 32 MiB 阈值,整形缓存继续限制字符串数量与源字节数。极端放大时清晰度受字形 LOD 限制。面板派生槽位限制为非负 i64,避免哈希高位造成偶发排版错误;重复标识拒绝发布。
原生验证记录
2026-11-11:本地完整回归、格式、all-target Clippy 和 release 应用打包;实际启动 Tessera.app,通过本机 API 加载 world-ui 示例,并取得原生窗口截图,确认立方体、倾斜面板和内部 Canvas 的显示。系统 CGPreflightScreenCaptureAccess 为 true,CGPreflightPostEventAccess 为 false;集成的原生自动操作工具还因 OpenSSL 配置读取权限启动失败。因此 API / VM 事件与像素测试不宣称为硬件鼠标或触控板验收。最终检查数量及交付提交在 PR 中记录。
最终完整回归为 186 项通过;运行中的原生实例验证世界 UI 按钮将计数从 0 改为 1、scene-orbit 改变摄像机、scene-zoom 改变观察距离、重复旧帧请求返回 HTTP 409,空闲连续采样的版本保持一致。保留应用窗口供用户直接操作。