修复 UI 编辑器字体绑定

允许文本组件绑定项目字体资源

修正字体引用计数与删除回退

补充字体绑定状态回归测试
This commit is contained in:
2026-08-18 21:22:40 +08:00
parent 43e8a6f904
commit 52f7ee5448
2 changed files with 39 additions and 6 deletions
@@ -292,6 +292,28 @@ function asRecord(value: unknown): Record<string, unknown> | null {
: null;
}
function isBoundFontSource(value: unknown): value is { Bound: FontAssetId } {
const record = asRecord(value);
return (
record !== null &&
Object.keys(record).length === 1 &&
typeof record.Bound === 'string' &&
record.Bound.length > 0 &&
record.Bound.trim() === record.Bound
);
}
function isValidFontSource(value: unknown): boolean {
return value === 'SystemFont' || isBoundFontSource(value);
}
function isBoundToFont(
value: unknown,
id: FontAssetId,
): value is { Bound: FontAssetId } {
return isBoundFontSource(value) && value.Bound === id;
}
function isValidFillMethod(method: unknown): boolean {
const record = asRecord(method);
if (!record) return false;
@@ -377,7 +399,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) &&
sizingValid &&
colorValid &&
isEnumValue(text.alignment, [
@@ -443,7 +465,7 @@ export function fontAssetRemovalImpact(
let clearedFontCount = 0;
for (const tree of state.ui_trees) {
visitComponents([tree.root], (component) => {
if ('Text' in component && component.Text.font === id) {
if ('Text' in component && isBoundToFont(component.Text.font, id)) {
clearedFontCount += 1;
}
});
@@ -1359,7 +1381,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
delete next.font_assets[id];
for (const tree of next.ui_trees) {
visitComponents([tree.root], (component) => {
if ('Text' in component && component.Text.font === id) {
if ('Text' in component && isBoundToFont(component.Text.font, id)) {
component.Text.font = 'SystemFont';
}
});
@@ -456,7 +456,7 @@ describe('useUiEditorState', () => {
);
});
it('dry-runs and clears Text font references without deleting project files', () => {
it('accepts and clears bound Text font references without deleting project files', () => {
const initial: State = {
...structuredClone(EMPTY_UI_EDITOR_STATE),
font_assets: { body: font('body') },
@@ -471,7 +471,8 @@ describe('useUiEditorState', () => {
{
Text: {
content: '你好',
font: 'body',
font: { Bound: 'body' },
font_style: 'Normal',
font_sizing: { Fixed: 14 },
color: [255, 255, 255, 255],
alignment: 'UpperLeft',
@@ -488,6 +489,16 @@ describe('useUiEditorState', () => {
const { result } = renderHook(() => useUiEditorState(initial));
act(() => {
expect(
result.current.setNodeComponents('page', 'text-root', [
{
Text: {
...initial.ui_trees[0]!.root.components[0]!.Text!,
font: { Bound: 'body' },
},
},
]),
).toEqual({ ok: true, value: undefined });
expect(result.current.removeFontAsset('body', { dryRun: true })).toEqual({
ok: true,
value: {
@@ -502,7 +513,7 @@ describe('useUiEditorState', () => {
});
expect(result.current.state.font_assets.body).toBeUndefined();
expect(result.current.state.ui_trees[0]?.root.components[0]).toMatchObject({
Text: { font: null },
Text: { font: 'SystemFont' },
});
});