diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs index 676fea4ef..15627288e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs @@ -271,6 +271,7 @@ pub(crate) async fn bind_components_impl( #[cfg(test)] mod tests { use super::*; + use crate::ui_editor::component::text::{FontSource, TextComponent}; use ts_rs::{Config, TS}; fn id(value: &str) -> NodeId { @@ -326,5 +327,7 @@ mod tests { let config = Config::from_env(); BindingDTO::export_all(&config).expect("BindingDTO TypeScript export succeeds"); Node::export_all(&config).expect("Node TypeScript export succeeds"); + TextComponent::export_all(&config).expect("TextComponent TypeScript export succeeds"); + FontSource::export_all(&config).expect("FontSource TypeScript export succeeds"); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/component/text.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/component/text.rs index 05b4335c3..092e8fc0c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/component/text.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/component/text.rs @@ -81,12 +81,20 @@ impl Default for FontSizing { } } +/// 文本的字体来源。系统字体是一个明确的有效选择,而不是缺失的字体绑定。 +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, TS)] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))] +pub enum FontSource { + SystemFont, + Bound(FontAssetId), +} + /// Text / Label 渲染组件;布局由同一 Entity 上独立的 `Control` 提供。 #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, TS)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))] pub struct TextComponent { pub content: String, - pub font: Option, + pub font: FontSource, pub font_style: FontStyle, pub font_sizing: FontSizing, #[ts(as = "[u8; 4]")] @@ -103,7 +111,7 @@ impl TextComponent { pub fn new(content: impl Into) -> Self { Self { content: content.into(), - font: None, + font: FontSource::SystemFont, font_style: FontStyle::Normal, font_sizing: FontSizing::default(), color: WHITE, @@ -120,3 +128,29 @@ impl Default for TextComponent { Self::new(String::new()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn system_font_is_an_explicit_serialized_choice() { + let component = TextComponent::default(); + assert_eq!( + serde_json::to_value(component).expect("serialize default text component")["font"], + serde_json::json!("SystemFont") + ); + } + + #[test] + fn bound_font_preserves_its_asset_id() { + let component = TextComponent { + font: FontSource::Bound(FontAssetId::new("font-main").expect("valid font id")), + ..TextComponent::default() + }; + assert_eq!( + serde_json::to_value(component).expect("serialize bound font")["font"], + serde_json::json!({ "Bound": "font-main" }) + ); + } +} diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts b/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts index 53cee5d9f..665e54548 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts @@ -45,7 +45,8 @@ export function getImageBindingCounts( export function getTextBindingCounts( component: Extract['Text'], ): ComponentBindingCounts { - return countRequiredAssetSlot(component.font !== null); + void component; + return countRequiredAssetSlot(true); } export function getComponentBindingCounts( diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts b/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts index 2cbae566d..2cfe307c6 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts @@ -12,9 +12,7 @@ function imageResourceIssues(state: State): UiEditorPrerequisiteIssue[] { for (const [id, image] of Object.entries(state.ui_design_images)) { if ( !image.path.trim() || - image.pixel_size.some( - (value) => !Number.isFinite(value) || value <= 0, - ) || + image.pixel_size.some((value) => !Number.isFinite(value) || value <= 0) || !Number.isFinite(image.pixels_per_unit) || image.pixels_per_unit <= 0 ) { @@ -61,9 +59,7 @@ export function validateComponentRecognitionPrerequisites( message: '界面不能归属于自身', resourceId: id, }); - } else if ( - state.ui_design_images[slaveTo]?.metadata.role !== 'Page' - ) { + } else if (state.ui_design_images[slaveTo]?.metadata.role !== 'Page') { issues.push({ code: 'invalid-slave-to', message: '归属页面必须是有效主页面', @@ -123,13 +119,13 @@ export function validateLayoutGenerationPrerequisites( } if ( 'Text' in component && - component.Text.font !== null && - !(component.Text.font in state.font_assets) + typeof component.Text.font !== 'string' && + !(component.Text.font.Bound in state.font_assets) ) { issues.push({ code: 'missing-font', message: '文本组件引用的字体不存在', - resourceId: component.Text.font, + resourceId: component.Text.font.Bound, }); } } diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/types/FontSource.ts b/apps/ai-game-creator-shell/src/features/ui-editor/types/FontSource.ts new file mode 100644 index 000000000..7893a3777 --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/ui-editor/types/FontSource.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FontAssetId } from './FontAssetId'; + +/** + * 文本的字体来源。系统字体是一个明确的有效选择,而不是缺失的字体绑定。 + */ +export type FontSource = 'SystemFont' | { Bound: FontAssetId }; diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/types/TextComponent.ts b/apps/ai-game-creator-shell/src/features/ui-editor/types/TextComponent.ts index e3f8d35fd..8745af8c9 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/types/TextComponent.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/types/TextComponent.ts @@ -1,12 +1,22 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FontAssetId } from "./FontAssetId"; -import type { FontSizing } from "./FontSizing"; -import type { FontStyle } from "./FontStyle"; -import type { HorizontalTextOverflow } from "./HorizontalTextOverflow"; -import type { TextAlignment } from "./TextAlignment"; -import type { VerticalTextOverflow } from "./VerticalTextOverflow"; +import type { FontSizing } from './FontSizing'; +import type { FontSource } from './FontSource'; +import type { FontStyle } from './FontStyle'; +import type { HorizontalTextOverflow } from './HorizontalTextOverflow'; +import type { TextAlignment } from './TextAlignment'; +import type { VerticalTextOverflow } from './VerticalTextOverflow'; /** * Text / Label 渲染组件;布局由同一 Entity 上独立的 `Control` 提供。 */ -export type TextComponent = { content: string, font: FontAssetId | null, font_style: FontStyle, font_sizing: FontSizing, color: [number, number, number, number], alignment: TextAlignment, horizontal_overflow: HorizontalTextOverflow, vertical_overflow: VerticalTextOverflow, line_spacing: number, }; +export type TextComponent = { + content: string; + font: FontSource; + font_style: FontStyle; + font_sizing: FontSizing; + color: [number, number, number, number]; + alignment: TextAlignment; + horizontal_overflow: HorizontalTextOverflow; + vertical_overflow: VerticalTextOverflow; + line_spacing: number; +}; diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts b/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts index 757e5062e..e42874cb3 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts @@ -5,7 +5,6 @@ import type { Component } from './types/Component'; import type { Node } from './types/Node'; import type { NodeId } from './types/NodeId'; import type { NodeMetadata } from './types/NodeMetadata'; -import type { StageStatus } from './types/StageStatus'; import type { SpriteAsset } from './types/SpriteAsset'; import type { SpriteAssetId } from './types/SpriteAssetId'; import type { SpriteBorder } from './types/SpriteBorder'; @@ -344,6 +343,12 @@ function isValidImageType(imageType: unknown): boolean { return false; } +function isValidFontSource(font: unknown): boolean { + if (font === 'SystemFont') return true; + const record = asRecord(font); + return record !== null && typeof record.Bound === 'string'; +} + function isValidComponent(component: Component): boolean { if ('Image' in component) { return ( @@ -370,7 +375,7 @@ function isValidComponent(component: Component): boolean { text.font_sizing.BestFit.min <= text.font_sizing.BestFit.max); return ( typeof text.content === 'string' && - (text.font === null || typeof text.font === 'string') && + isValidFontSource(text.font) && isEnumValue(text.font_style, [ 'Normal', 'Bold', diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Components/TextPanel.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Components/TextPanel.tsx index c87dd27cf..9f0d5df7e 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Components/TextPanel.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Components/TextPanel.tsx @@ -48,9 +48,9 @@ export function TextPanel({ {/* TODO: expose editable imported font assets after FontAsset gets a safe browser resource. */}
- {component.font - ? (fonts[component.font]?.asset_id ?? component.font) - : '未绑定(使用系统字体)'} + {typeof component.font !== 'string' + ? (fonts[component.font.Bound]?.asset_id ?? component.font.Bound) + : '系统字体'}
{ expect(getBindingOverview(trees, sprites)).toEqual({ componentsNeedingAssets: 4, assetSlots: 4, - boundSlots: 2, - pendingSlots: 2, + boundSlots: 3, + pendingSlots: 1, needsAttention: 2, blocked: 1, independentAssets: 2, @@ -113,7 +118,7 @@ describe('getBindingOverview', () => { getNextMatchingUiTreeNodeTarget(trees, 'review', (target) => nodeHasPendingBinding(target), )?.node.id, - ).toBe('blocked'); + ).toBe('review'); expect( getNextMatchingUiTreeNodeTarget(trees, null, nodeNeedsComponentReview) ?.node.id, diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 1e1476abf..c4435444a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -7161,6 +7161,11 @@ - HTTP 传输边界:根 H5 包不得依赖 Tauri guest 插件;AGC 独立包保留 `@tauri-apps/plugin-http`。Rust 插件显式关闭默认特性,只启用 `charset`、`cookies`、`http2` 和 `rustls-tls`,避免 `reqwest/system-proxy` 通过 Cargo feature union 把画布、Provider、Runtime 与本地回环夹具统一接入 OS 自动系统代理;如未来产品要求正式客户端继承系统代理,必须按各客户端明确设计并单独完成跨平台验证。 - 运行决策:Godot 项目提交给 Project Supervisor 时使用 `standard` Run Profile,避免触发 Web 专用 `game/index.html`、HTTP preview 与自主 Web 完成门。Godot 编辑器启动和内嵌运行预览不在本切片范围。 +## 2026-08-17 UI Editor 文本字体来源显式化 + +- 决策:UI Editor 的 Rust 领域类型以 `FontSource::SystemFont | FontSource::Bound(FontAssetId)` 表达 Text 的字体来源,`SystemFont` 是有效的明确选择,不再以 nullable `font` 承担业务语义;TypeScript 类型必须由 `ts-rs` 从该 Rust 类型生成。 +- 统计与校验:两种字体来源都计入一个已完成字体槽;只有 `Bound(id)` 校验对应字体资源存在。缺失图片资源仍是绑定总览中唯一的待处理槽,系统字体文本不进入待处理导航。 + ## 2026-08-12 AGC Agent 设置分层界面 - `RuntimeConfigDialog` 保持现有配置字段、持久化格式、Runner 读取和 MCP 测试语义,只重构界面信息架构。