diff --git a/apps/ai-game-creator-shell/src/components/AssetImporter/FontImporterPreview.tsx b/apps/ai-game-creator-shell/src/components/AssetImporter/FontImporterPreview.tsx index 865269731..292166312 100644 --- a/apps/ai-game-creator-shell/src/components/AssetImporter/FontImporterPreview.tsx +++ b/apps/ai-game-creator-shell/src/components/AssetImporter/FontImporterPreview.tsx @@ -1,6 +1,7 @@ import { invoke } from '@tauri-apps/api/core'; import { useEffect, useRef, useState } from 'react'; +import { FontSamplePreview } from '../../features/ui-editor/components/FontSamplePreview'; import type { FontAsset } from '../../features/ui-editor/types/FontAsset'; import { type LoadedUiEditorFontFace, @@ -13,6 +14,7 @@ export function FontImporterPreview({ selected, }: AssetImporterPreviewProps) { const [fontFamily, setFontFamily] = useState(null); + const [font, setFont] = useState(null); const [error, setError] = useState(null); const loadedRef = useRef<{ face: FontFace; url: string } | null>(null); @@ -25,6 +27,7 @@ export function FontImporterPreview({ loadedRef.current = null; } setFontFamily(null); + setFont(null); setError(null); if (!selected?.asset || selected.isDirectory) return; void (async () => { @@ -41,6 +44,7 @@ export function FontImporterPreview({ } document.fonts.add(loaded.face); loadedRef.current = loaded; + setFont(font); setFontFamily(loaded.cssFamily); } catch (cause) { console.error('[ui-editor-font-preview] failed', { @@ -69,17 +73,10 @@ export function FontImporterPreview({ ); } return ( -
-

- 中文字体预览 · English Font Preview · 0123456789 -

-

- 春夏秋冬,山川湖海;Aa Bb Cc,!? @#&*() -

-

陶泥儿 Genarrative Studio

-

你好,Hello World! 字体设计

-

字体样张 Aa 字母数字 123

-

中文 English

-
+ ); } diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/components/FontSamplePreview.tsx b/apps/ai-game-creator-shell/src/features/ui-editor/components/FontSamplePreview.tsx new file mode 100644 index 000000000..e2ee7c94f --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/ui-editor/components/FontSamplePreview.tsx @@ -0,0 +1,72 @@ +export function FontSamplePreview({ + fontFamily, + fontWeight, + italic, +}: { + fontFamily?: string; + fontWeight?: number; + italic?: boolean; +}) { + // 将字体样张的排版属性放到每一行上,而不是依赖 Inspector 容器的继承。 + // Inspector 同时承载节点文本样式编辑,样张必须始终只反映当前字体资源。 + const sampleStyle = fontFamily + ? { + color: '#000', + fontFamily: JSON.stringify(fontFamily), + fontWeight, + fontStyle: italic ? 'italic' : 'normal', + fontSynthesis: 'none', + } + : { color: '#000' }; + + const sampleLines = [ + { + text: '中文字体预览 · English Font Preview · 0123456789', + fontSize: 12, + lineHeight: '20px', + }, + { + text: '春夏秋冬,山川湖海;Aa Bb Cc,!? @#&*()', + fontSize: 14, + lineHeight: '24px', + }, + { + text: '陶泥儿 Genarrative Studio', + fontSize: 18, + lineHeight: '28px', + }, + { + text: '你好,Hello World! 字体设计', + fontSize: 24, + lineHeight: '32px', + }, + { + text: '字体样张 Aa 字母数字 123', + fontSize: 30, + lineHeight: '40px', + }, + { + text: '中文 English', + fontSize: 36, + lineHeight: '40px', + }, + ]; + + return ( +
+ {sampleLines.map((line, index) => ( +

+ {line.text} +

+ ))} +
+ ); +} diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx index 02b8c7838..232c9ddbc 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx @@ -6,6 +6,7 @@ import type { TextareaHTMLAttributes, } from 'react'; +import { FontSamplePreview } from '../../../../features/ui-editor/components/FontSamplePreview'; import { SpriteImagePreview } from '../../../../features/ui-editor/components/SpriteImagePreview'; import type { UIDesignImageId } from '../../../../features/ui-editor/types/UIDesignImageId'; import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UIDesignImageRole'; @@ -593,19 +594,16 @@ function FontInspector({ return (
-
- + - Aa 中文 123 - + fontWeight={font.metadata.weight} + italic={font.metadata.italic} + />
diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorNodeFocus.ts b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorNodeFocus.ts index fa7b1e702..7f4da4920 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorNodeFocus.ts +++ b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorNodeFocus.ts @@ -7,10 +7,12 @@ import type { UiEditorNodeFocusRequest } from './model'; export function useUiEditorNodeFocus({ activateImage, clearSpriteSelection, + clearFontSelection, selectNode, }: { activateImage: (id: UIDesignImageId) => void; clearSpriteSelection: () => void; + clearFontSelection: () => void; selectNode: (id: NodeId) => void; }) { const nextRequestIdRef = useRef(0); @@ -21,6 +23,7 @@ export function useUiEditorNodeFocus({ (treeId: UIDesignImageId, nodeId: NodeId) => { activateImage(treeId); clearSpriteSelection(); + clearFontSelection(); selectNode(nodeId); nextRequestIdRef.current += 1; setFocusRequest({ @@ -29,7 +32,7 @@ export function useUiEditorNodeFocus({ requestId: nextRequestIdRef.current, }); }, - [activateImage, clearSpriteSelection, selectNode], + [activateImage, clearFontSelection, clearSpriteSelection, selectNode], ); const clearFocusRequest = useCallback(() => setFocusRequest(null), []); diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts index 0dc84b1ab..3064113fd 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts +++ b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts @@ -137,6 +137,7 @@ export function useUiEditorPage( const { focusRequest, focusNode } = useUiEditorNodeFocus({ activateImage: setActiveImageId, clearSpriteSelection: () => setSelectedSpriteId(null), + clearFontSelection: () => setSelectedFontId(null), selectNode: setSelectedNodeId, }); const [keepChildrenUnchanged, setKeepChildrenUnchanged] = useState(false); @@ -607,6 +608,7 @@ export function useUiEditorPage( setImageOrder([]); setActiveImageId(null); setSelectedSpriteId(null); + setSelectedFontId(null); setSelectedNodeId(null); setHiddenNodeIds(new Set()); setPreviewUrls({}); @@ -616,11 +618,13 @@ export function useUiEditorPage( function selectDesignImage(id: UIDesignImageId) { setActiveImageId(id); setSelectedSpriteId(null); + setSelectedFontId(null); setSelectedNodeId(null); } function selectNode(id: NodeId) { setSelectedSpriteId(null); + setSelectedFontId(null); setSelectedNodeId(id); } @@ -758,6 +762,7 @@ export function useUiEditorPage( function selectSprite(id: SpriteAssetId) { setSelectedNodeId(null); setSelectedSpriteId(id); + setSelectedFontId(null); } function selectFont(id: FontAssetId) { diff --git a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts index 852d75b1b..2b2f45281 100644 --- a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts +++ b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts @@ -145,6 +145,21 @@ describe('UiEditorPage', () => { border: { left: 0, right: 0, top: 0, bottom: 0 }, }, ]); + result.current.editor.addFontAssets([ + { + asset_id: 'body', + metadata: { + family_name: '测试字体', + face_name: 'Regular', + weight: 400, + italic: false, + format: 'TrueType', + source_file_name: 'body.ttf', + }, + path: 'assets/fonts/body.ttf', + content_sha256: 'a'.repeat(64), + }, + ]); }); const rootId = result.current.editor.state.ui_trees[0]!.root.id; act(() => { @@ -156,6 +171,20 @@ describe('UiEditorPage', () => { act(() => result.current.selectSprite('panel')); expect(result.current.selectedNodeId).toBeNull(); expect(result.current.selectedSpriteId).toBe('panel'); + + act(() => result.current.selectFont('body')); + expect(result.current.selectedFontId).toBe('body'); + + act(() => result.current.selectDesignImage('page')); + expect(result.current.selectedFontId).toBeNull(); + + act(() => result.current.selectFont('body')); + act(() => result.current.selectSprite('panel')); + expect(result.current.selectedFontId).toBeNull(); + + act(() => result.current.selectFont('body')); + act(() => result.current.selectNode(rootId)); + expect(result.current.selectedFontId).toBeNull(); }); it('keeps node preview visibility transient and attached to global node IDs', () => {