From b21b2524296c90f687428ed6173694ceedb4e844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 18 Aug 2026 18:28:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AF=BC=E5=85=A5=E5=99=A8?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E9=A2=84=E8=A7=88=E4=B8=8E=E6=A0=B9=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正文件管理器 Home 根路径并恢复本地来源入口 为字体候选增加受 manifest 约束的临时 FontFace 预览 同步字体预览读取约束 --- .../src-tauri/src/commands.rs | 13 ++++ .../src-tauri/src/main.rs | 1 + .../src/components/AssetImporter/index.tsx | 69 +++++++++++++++++-- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index a4aa7e7e0..fc36aa94f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -1996,6 +1996,19 @@ pub(crate) fn read_ui_editor_font_bytes( .map(|(_, bytes)| tauri::ipc::Response::new(bytes)) } +#[tauri::command] +pub(crate) fn read_ui_editor_font_preview( + project_path: String, + asset_id: String, + relative_path: String, +) -> Result { + let root = Path::new(project_path.trim()); + enforce_project_permission_policy(root, "file.read")?; + let manifest = read_existing_manifest_for_project(root)?; + let asset = find_registered_ui_editor_font(&manifest, asset_id.trim(), relative_path.trim())?; + read_registered_ui_editor_font(root, asset).map(|(_, bytes)| tauri::ipc::Response::new(bytes)) +} + #[tauri::command] pub(crate) fn check_ui_editor_font_glyph_coverage( project_path: String, diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 0290151e6..253e5edea 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2301,6 +2301,7 @@ fn main() { import_ui_editor_assets, prepare_ui_editor_project_fonts, read_ui_editor_font_bytes, + read_ui_editor_font_preview, check_ui_editor_font_glyph_coverage, suggest_ui_design_semantic, recognize_ui, diff --git a/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx b/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx index 8e560bc7b..d9301b81d 100644 --- a/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx +++ b/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx @@ -79,12 +79,16 @@ export function AssetImporter({ const [files, setFiles] = useState([]); const [selected, setSelected] = useState([]); const [preview, setPreview] = useState(null); + const [fontPreviewFamily, setFontPreviewFamily] = useState( + null, + ); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [currentPath, setCurrentPath] = useState(ROOT_PATH); const currentPathRef = useRef(ROOT_PATH); const remoteLoadedRef = useRef(false); const previewScopeIdRef = useRef(createProjectResourcePreviewScopeId()); + const previewFontFaceRef = useRef(null); useEffect(() => { const previousScopeId = previewScopeIdRef.current; @@ -221,15 +225,56 @@ export function AssetImporter({ setFiles(buildRootFiles(settings)); setSelected([]); setPreview(null); + if (previewFontFaceRef.current) { + document.fonts?.delete(previewFontFaceRef.current); + previewFontFaceRef.current = null; + } + setFontPreviewFamily(null); void refresh(ROOT_PATH); }, [open, refresh, settings]); useEffect(() => { const item = selected.at(-1); + if (previewFontFaceRef.current) { + document.fonts?.delete(previewFontFaceRef.current); + previewFontFaceRef.current = null; + } + setFontPreviewFamily(null); if (!item || item.isDirectory) { setPreview(null); return; } + if (settings.local.localPolicy.destination === 'assets/fonts') { + setPreview(null); + void (async () => { + try { + if ( + typeof FontFace === 'undefined' || + !document.fonts || + !item.asset + ) { + return; + } + const bytes = await invoke( + 'read_ui_editor_font_preview', + { + projectPath, + assetId: item.asset.id, + relativePath: item.asset.localPath, + }, + ); + const family = `asset-importer-preview-${item.asset.id}`; + const face = new FontFace(family, bytes); + await face.load(); + previewFontFaceRef.current = face; + document.fonts.add(face); + setFontPreviewFamily(family); + } catch { + setFontPreviewFamily(null); + } + })(); + return; + } if (item.previewUrl) { setPreview(item.previewUrl); return; @@ -255,7 +300,16 @@ export function AssetImporter({ }) .then((value) => setPreview(value.dataUrl)) .catch(() => setPreview(null)); - }, [projectPath, selected]); + }, [projectPath, selected, settings.local.localPolicy.destination]); + + useEffect( + () => () => { + if (previewFontFaceRef.current) { + document.fonts?.delete(previewFontFaceRef.current); + } + }, + [], + ); const pickLocal = async () => { setError(null); @@ -361,7 +415,7 @@ export function AssetImporter({ void pickLocal(); }; const handleFolderChange = (path: string) => { - const nextPath = path || ROOT_PATH; + const nextPath = path === '/' ? ROOT_PATH : path; currentPathRef.current = nextPath; setCurrentPath(nextPath); if ( @@ -412,7 +466,7 @@ export function AssetImporter({ ) : null}
) : ( - + {selected.at(-1)?.name ?? '选择一个文件'} )} diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 2129f8a8d..9fac09df3 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -18,7 +18,7 @@ UI Editor 的 `State.font_assets` 正式承载项目字体面资源;`FontAsset 字体资源发现与 Sprite 保持同一项目资产语义:字体 importer 打开后读取 manifest 与项目文件列表,只展示已登记且具有字体 MIME 或受支持字体扩展名的项目资产;不扫描或接纳未登记文件,不展示云端素材库。从电脑导入使用 Tauri 系统文件选择器,Rust 整批读取普通文件,拒绝符号链接、集合字体、超过 `8 MiB` 的单文件、超过 `64` 个字体面或 `32 MiB` 项目总量,完成真实签名、字体表、名称与 weight/style 解析后复制到 `assets/fonts/` 并登记 manifest。内容相同的字体复用已登记资源;Sprite 与 Font 的 State 批量加入都采用幂等合并:相同 ID 且完整资源相等时跳过,同 ID 数据冲突时整批失败。删除只移除会话 State 资源并清空相应 `Image.target_graphic` 或 `Text.font` 引用,不删除项目文件或 manifest 条目。 -候选格式为 TTF、OTF、WOFF 和 WOFF2,但 Rust 安全解析是导入硬门;当前解析依赖不能完整解析的压缩 Web Font 必须拒绝,不能把浏览器可能加载当作验证成功。已登记字体字节只能经字体专用 Tauri 命令读取;命令重新核对 manifest 的 asset ID / 相对路径、普通文件、大小、字体结构与摘要。前端以 `FontAssetId` 派生私有 CSS family,创建 Blob URL 和 `FontFace`,加载成功后加入当前 `document.fonts`,资源变更或卸载时删除 FontFace 并回收 Blob URL。同名 family 不共享浏览器注册名。WebView 加载失败或字体缺少当前文本字形时不阻塞后续阶段,Inspector 显示非阻断提示并回退系统字体;悬空字体 ID 继续由 prerequisite 阻止。`BestFit` 保持现有取最大字号的近似,不在本次字体闭环中扩展为测量算法。 +候选格式为 TTF、OTF、WOFF 和 WOFF2,但 Rust 安全解析是导入硬门;当前解析依赖不能完整解析的压缩 Web Font 必须拒绝,不能把浏览器可能加载当作验证成功。已登记字体字节只能经字体专用 Tauri 命令读取;命令重新核对 manifest 的 asset ID / 相对路径、普通文件、大小、字体结构与摘要。Importer 预览也只读取同一受 manifest 约束的字体字节,再临时加载 `FontFace` 显示选中文件名;切换选择或关闭时立即卸载。前端以 `FontAssetId` 派生私有 CSS family,创建 Blob URL 和 `FontFace`,加载成功后加入当前 `document.fonts`,资源变更或卸载时删除 FontFace 并回收 Blob URL。同名 family 不共享浏览器注册名。WebView 加载失败或字体缺少当前文本字形时不阻塞后续阶段,Inspector 显示非阻断提示并回退系统字体;悬空字体 ID 继续由 prerequisite 阻止。`BestFit` 保持现有取最大字号的近似,不在本次字体闭环中扩展为测量算法。 ## 2026-08-18 UI Editor 图片 / 字体通用 AssetImporter 契约