diff --git a/examples/hello.endor b/examples/hello.endor index 11fcf45..d785d51 100644 --- a/examples/hello.endor +++ b/examples/hello.endor @@ -1,10 +1,10 @@ -function heading(value: Text): View { // 辅助函数把字符串包装成大字号 TextBlock;返回值仍是托管视觉引用。 +function heading(value: Text): View { // 辅助函数把字符串包装成大字号 TextBlock;返回值仍是托管视觉引用。调用方为下面的 main,参数 value 是该次调用的 Text,不是可写全局状态。 return ui.text(value, 2, "#242422") // 这里只创建文字对象,具体宽高由实际字体和出现位置约束测量。 } -function main(): View { // 兼容模块入口 main 返回视觉根,宿主负责把它显示到页面,不需要源代码显式 render。 +function main(): View { // 兼容模块入口 main 返回视觉根,宿主负责把它显示到页面,不需要源代码显式 render。endor_host 的模块执行路径在定义加载完成后调用这个零参数入口。 let title = heading("Hello Endor") // 调用上述辅助函数,局部变量持有同一托管文字对象。 let rectangle = ui.rect(240, 80, 12, "#DCE8FF") // 矩形使用逻辑尺寸,圆角与颜色由原生载荷保存。 let detail = ui.row(0, 12, ui.text("Aster VM"), ui.spacer(8), ui.text("Tessera UI")) // Row 依次摆放文字、固定间隔和文字;共享几何仍由每次出现位置承担。 - return ui.column(24, 12, title, rectangle, detail) // Column 的 24px 内边距和 12px 间隔决定最终命令位置;返回后发布快照拥有所需数据。 + return ui.column(24, 12, title, rectangle, detail) // Column 的 24px 内边距和 12px 间隔决定最终命令位置;返回后宿主提取原生发布快照;Column 在 VM 中继续按托管引用拥有这些子对象。 } diff --git a/examples/main.endor b/examples/main.endor index 3859ea5..5728e55 100644 --- a/examples/main.endor +++ b/examples/main.endor @@ -1,8 +1,10 @@ // 与 page.endor 等价的 main 返回根入口:用于对比顶层显式 render 和兼容模块入口的绘图结果。 // 返回 Column 视觉对象,宿主选择/挂载页面;这里的 title 和 rectangle 只在 VM 内建立业务引用。 +// endor_script 将本文件作为完整模块解析,endor_host 调用零参数 main 后把返回的视觉根交给兼容展示路径。 function main(): View { // 文字内容与矩形参数和 page 示例一致,最终绘图命令应等价。 let heading = ui.text("Hello Endor", 2, "#242422") + // 两次构造返回独立托管业务对象;此处变量只是对象引用,出现位置在挂载到页面后由布局生成。 let rectangle = ui.rect(240, 80, 12, "#DCE8FF") // 布局边距与间隔保持一致;不同 VM 的对象/Mount 身份独立,命令相同不表示共享对象。 return ui.column(24, 12, heading, rectangle) diff --git a/examples/page.endor b/examples/page.endor index 82fb1c5..421fa05 100644 --- a/examples/page.endor +++ b/examples/page.endor @@ -1,4 +1,4 @@ -let heading = ui.text("Hello Endor", 2, "#242422") // 顶层脚本不需要 main:先创建文字对象;字号 2 和十六进制颜色传给原生文字构造器。 +let heading = ui.text("Hello Endor", 2, "#242422") // 顶层脚本不需要 main:先创建文字对象;字号 2 和十六进制颜色传给原生文字构造器。endor_host 的脚本执行路径按顶层语句顺序运行本文件。 let rectangle = ui.rect(240, 80, 12, "#DCE8FF") // 创建固定逻辑尺寸矩形;12 为圆角半径,业务对象尚未出现在任何页面中。 let myui = ui.page() // 创建托管 Page 资源;它负责持有展示根和页面生命周期。 -myui.render(ui.column(24, 12, heading, rectangle)) // 把文字和矩形放入 Column,padding=24、gap=12;render 选择此页并生成出现位置及发布帧。 +myui.render(ui.column(24, 12, heading, rectangle)) // 把文字和矩形放入 Column,padding=24、gap=12;render 请求展示此页,宿主刷新时再完成出现位置、测量、安排与绘制;源方法调用结束不等于 OS 已呈现像素。 diff --git a/scripts/build-macos-app.sh b/scripts/build-macos-app.sh index 2273d27..92bc383 100755 --- a/scripts/build-macos-app.sh +++ b/scripts/build-macos-app.sh @@ -2,6 +2,7 @@ # macOS 应用打包入口:构建 release 可执行文件并组装最小 Tessera.app;脚本本身不运行测试或部署。 # 执行会产生构建产物,应在已确认的 macOS 构建环境中调用。 # 任一命令失败或使用未定义变量即退出,避免在失败编译后继续打包旧文件。 +# 构建调用方显式执行此脚本才产生文件;阅读或 sh -n 仅检查语法,不会运行下方 Cargo。 set -eu # 此入口只能在 macOS 使用;构建授权由调用方确认,脚本不自动执行测试或部署。 if [ "$(uname -s)" != Darwin ]; then @@ -11,17 +12,21 @@ fi # 以脚本所在目录的父目录作为仓库根;清空这一次 cd 的 CDPATH,防止用户 shell 配置改变目录解析。 cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" # 锁定依赖并生成 release 可执行文件;附加参数原样交给 Cargo。 +# 这里确实触发编译,不是只验证清单;依赖锁不匹配时 --locked 要求失败而非自动更新 Cargo.lock。 cargo build --release --locked "$@" # 从 Cargo 包元数据读取首个 version 值,空值由 test 拒绝,不生成缺版本的 bundle。 +# version 从清单文本读取,后面用于两个 plist 版本键;脚本不调用构建后的 Tessera 来查询版本。 version=$(awk -F '"' '$1 ~ /^version[[:space:]]*=/ { print $2; exit }' Cargo.toml) test -n "$version" # macOS bundle 约定:Contents/MacOS 保存入口二进制,Contents/Info.plist 保存系统识别信息。 bundle="$(pwd)/target/release/Tessera.app" +# 创建/复用固定 release 产物目录,不清空旧 bundle;后面逐文件替换主二进制和元数据。 mkdir -p "$bundle/Contents/MacOS" # 先写临时文件再重命名,避免复制途中将半个二进制留作 bundle 的主程序。 cp target/release/tessera "$bundle/Contents/MacOS/tessera.new" mv -f "$bundle/Contents/MacOS/tessera.new" "$bundle/Contents/MacOS/tessera" # 生成最小 macOS bundle 元数据;应用名、入口与高分辨率标志供系统启动/显示使用。 +# heredoc 展开本次 version,XML 文本与入口文件名相互对应;这里没有签名、公证或安装动作。 cat > "$bundle/Contents/Info.plist.new" < diff --git a/scripts/render-uml.mjs b/scripts/render-uml.mjs index 1a43b4a..2bbcf06 100644 --- a/scripts/render-uml.mjs +++ b/scripts/render-uml.mjs @@ -6,6 +6,7 @@ import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +// 文档维护者从命令行执行本脚本;运行时入口不导入它,依赖应事先安装,本脚本不执行包管理器。 const tools = process.env.UML_RENDER_TOOLS; // 工具依赖放在独立目录,避免给应用运行依赖增加文档渲染包。 if (!tools) throw new Error('Set UML_RENDER_TOOLS to the documentation tool directory; see docs/architecture/uml/README.md'); @@ -67,6 +68,7 @@ document.createElementNS = (namespace, tag) => { document.body = element('body'); document.documentElement.appendChild(document.body); globalThis.document = document; +// 全局 DOM/Canvas 适配已设置完毕才加载引擎,避免模块初始化读取到缺失的浏览器接口。 const { renderToString } = await import(pathToFileURL(require.resolve('@plantuml/core'))); // 仓库位置由脚本 URL 确定,与调用者工作目录无关;输出目录不存在时创建。 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); @@ -80,6 +82,7 @@ for (const filename of (await readdir(source)).filter(name => name.endsWith('.pu // 将引擎 resolve/reject 回调适配为 Promise,逐图等待;渲染失败直接终止,不覆盖该图产物。 const rendered = await new Promise((resolve, reject) => renderToString(text.split('\n'), resolve, reject)); const xml = new DOMParser().parseFromString(rendered, 'image/svg+xml'); + // 引擎有时以正常 SVG 输出错误页,所以不能只判断 Promise resolve;还要检查文字层的错误标签。 const labels = Array.from(xml.getElementsByTagName('text'), node => node.textContent).join('\n'); if (xml.documentElement.tagName !== 'svg' || /syntax error|error line|assumed diagram type|an error has occurr/i.test(labels)) { throw new Error(`${filename}: renderer returned an error diagram\n${labels}`); @@ -88,9 +91,11 @@ for (const filename of (await readdir(source)).filter(name => name.endsWith('.pu const paper = xml.createElementNS('http://www.w3.org/2000/svg', 'rect'); for (const [key, value] of Object.entries({ x: '0', y: '0', width: '100%', height: '100%', fill: '#FFFFFF' })) paper.setAttribute(key, value); xml.documentElement.insertBefore(paper, xml.documentElement.firstChild); + // 序列化修改后的 DOM,而不是直接写引擎原字符串,确保白底矩形进入两个导出格式。 const svg = new XMLSerializer().serializeToString(xml); // 同时保存可缩放 SVG 和通用 PNG;写完读取 PNG 元数据确认生成尺寸。 const stem = filename.slice(0, -5); + // 两种产物顺序写入且不组成事务;SVG 成功后若 PNG 失败,前面已写出的文件不会自动回滚。 await writeFile(path.join(output, `${stem}.svg`), svg); // 以 192 DPI 光栅化为 PNG 供通用预览;SVG 同时保留矢量文字和缩放能力。 const png = sharp(Buffer.from(svg), { density: 192 }); diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs index 610922e..518145c 100644 --- a/src/bridge/mod.rs +++ b/src/bridge/mod.rs @@ -1,7 +1,10 @@ //! 语言桥接模块入口。集中导出 Endor 类型和宿主实现之间的适配层。 +//! endor_host 建立会话时安装 ui_bridge 能力,源程序随后通过名义方法调用宿主;渲染线程不直接运行这些回调。 //! // 目录确定模块归属;main 中的 crate 根别名保持内部调用路径稳定。 /// 注册托管类型/宿主方法,验证参数并连接业务对象、Mount、视觉协议与窗口/任务资源。 pub(crate) mod ui_bridge; +/// 注册图片、字体、文字流与避让形状能力,并把托管绘制参数解码为可跨线程使用的不可变资源。 +/// ui_bridge 安装时调用注册入口,source_paint 解码 image 命令时复用图像检查。 pub(crate) mod graphics; diff --git a/src/main.rs b/src/main.rs index f698d0e..3b6523c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,12 +3,14 @@ //! //! Application composition root. Directory modules own implementation; these //! crate-private aliases keep existing internal paths stable during reorganization. +// 以下目录模块只声明编译归属,真正启动服务和创建窗口发生在 main 的顺序调用中。 mod bridge; mod model; mod render; mod runtime; mod shell; +// crate 内旧路径通过私有重导出访问目录实现,不为 GUI 与 runtime 额外建立另一套对象或注册表。 pub(crate) use bridge::ui_bridge; // 模型层负责从 VM 场景对象提取快照,渲染层 scene3d 负责原生计算;别名区分两者。 pub(crate) use model::scene3d as scene3d_model; @@ -36,17 +38,21 @@ use document::NamespaceState; use winit::event_loop::{ControlFlow, EventLoop}; /// 按顺序建立事件循环、共享文档、API/VM worker 和应用窗口;事件循环结束后返回原生退出清理结果。 +// 操作系统启动可执行文件时进入;各阶段的 ? 将启动/循环错误返回进程入口,不把失败当作成功运行。 fn main() -> Result<()> { // 原生窗口由事件循环所属线程驱动;Wait 让空闲时休眠,而不是持续重绘。 let event_loop = EventLoop::::with_user_event().build()?; + // 后台发布通过 EventLoopProxy 唤醒等待,原生窗口请求 RedrawRequested 驱动实际绘制。 event_loop.set_control_flow(ControlFlow::Wait); // 共享最近完整发布的文档。后台 worker 拥有 VM,GUI 不借用其托管堆。 let namespace = Arc::new(RwLock::new(NamespaceState::default())); // worker 发布完整 NamespaceState,activity 供 Network 工具观察,control 用于原生窗口发运行时控制请求。 + // start 先绑定本机 API 并启动后台设施;端口或线程启动错误会阻止后续应用状态构造。 let (activity, control) = api::start(namespace.clone(), event_loop.create_proxy())?; // 两个 proxy 指向同一事件循环;后台线程通过纯数据通知唤醒 GUI,不直接操作原生窗口。 let mut app = TesseraApp::new(namespace, activity, event_loop.create_proxy(), control)?; // run_app 接管线程直至退出;退出后仍需检查清理过程中记录的错误。 event_loop.run_app(&mut app)?; + // 仅 run_app 正常返回后汇总窗口/VM 清理错误;循环本身返回 Err 时上面的 ? 已直接传播。 app.exit_result() } diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index d6a83bc..1f80469 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -1,4 +1,6 @@ //! 运行时模块入口。VM 生命周期、API、源程序组织、事件与异步任务在这里组合。 +//! main 调用 api::start 建立服务;worker 通过 endor_host 操作所属 VM,events 客户端将原生输入排队交付。 +//! 任务后台只完成纯数据 I/O,结果是否仍可交付以及如何回到源回调,由串行运行时检查和推进。 //! // 目录确定模块归属;main 中的 crate 根别名保持内部调用路径稳定。 /// 提供 HTTP 路由、有界请求队列与串行 worker,协调候选 VM 和已发布 namespace。