前端移除分析参考图步骤与界面图元数据编辑
- model.ts 收敛为“识别界面结构 / 自动切分素材”两步并删除界面角色标签表 - 删除 suggestion 异步操作、结果通知分支、uiDesignSuggestions 与其 ts-rs 生成类型 - InputSidebar、InspectorSidebar、PreviewWorkspace 的界面图显示名改用 path basename,Inspector 只保留尺寸与资源 ID - 删除因步骤退役而不可达的 ImportOverview,ToolNavigation 改为两格,删除“清空界面归属”提示 - requisites 删除角色/归属前置校验与参考图分析结果校验,importAdapter 不再写入界面图元数据 - 测试夹具去掉界面图元数据,删除界面角色与归属用例,历史用例改用 sprite 名称断言
This commit is contained in:
@@ -66,12 +66,6 @@ export async function prepareDesignImageBatch(
|
||||
scopeId,
|
||||
);
|
||||
const image: UIDesignImage = {
|
||||
metadata: {
|
||||
name: '',
|
||||
description: '',
|
||||
role: null,
|
||||
slave_to: null,
|
||||
},
|
||||
path: asset.localPath,
|
||||
pixel_size: pixelSize,
|
||||
pixels_per_unit: 1,
|
||||
|
||||
@@ -38,37 +38,6 @@ export function validateComponentRecognitionPrerequisites(
|
||||
if (ids.length > 4) {
|
||||
issues.push({ code: 'design-image-limit', message: '界面图最多 4 张' });
|
||||
}
|
||||
for (const id of ids) {
|
||||
const image = state.ui_design_images[id]!;
|
||||
const { role, slave_to: slaveTo } = image.metadata;
|
||||
if (role === 'Page' && slaveTo !== null) {
|
||||
issues.push({
|
||||
code: 'page-has-slave-to',
|
||||
message: '主页面不能设置归属页面',
|
||||
resourceId: id,
|
||||
});
|
||||
} else if (role !== null && role !== 'Page') {
|
||||
if (slaveTo === null) {
|
||||
issues.push({
|
||||
code: 'missing-slave-to',
|
||||
message: '该界面角色需要选择归属主页面',
|
||||
resourceId: id,
|
||||
});
|
||||
} else if (slaveTo === id) {
|
||||
issues.push({
|
||||
code: 'self-slave-to',
|
||||
message: '界面不能归属于自身',
|
||||
resourceId: id,
|
||||
});
|
||||
} else if (state.ui_design_images[slaveTo]?.metadata.role !== 'Page') {
|
||||
issues.push({
|
||||
code: 'invalid-slave-to',
|
||||
message: '归属页面必须是有效主页面',
|
||||
resourceId: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
@@ -131,24 +100,6 @@ function visitNodes(
|
||||
}
|
||||
|
||||
// 结果检查与进入步骤的前置条件保持分离;调用方只把结果作为非阻塞提示。
|
||||
export function validateReferenceAnalysisResult(
|
||||
state: State,
|
||||
): UiEditorPrerequisiteIssue[] {
|
||||
const images = Object.values(state.ui_design_images);
|
||||
if (images.length === 0) {
|
||||
return [{ code: 'missing-analysis-input', message: '尚未导入参考图' }];
|
||||
}
|
||||
if (images.every((image) => image.metadata.role === null)) {
|
||||
return [
|
||||
{
|
||||
code: 'missing-reference-analysis',
|
||||
message: '参考图尚未设置界面用途',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function validateStructureRecognitionResult(
|
||||
state: State,
|
||||
): UiEditorPrerequisiteIssue[] {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { UIDesignImageMetadata } from "./UIDesignImageMetadata";
|
||||
|
||||
export type UIDesignImage = { metadata: UIDesignImageMetadata, path: string, pixel_size: [number, number], pixels_per_unit: number, };
|
||||
export type UIDesignImage = { path: string, pixel_size: [number, number], pixels_per_unit: number, };
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { UIDesignImageId } from "./UIDesignImageId";
|
||||
import type { UIDesignImageRole } from "./UIDesignImageRole";
|
||||
|
||||
export type UIDesignImageMetadata = { name: string, description: string, role: UIDesignImageRole | null, slave_to: UIDesignImageId | null, };
|
||||
@@ -1,3 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type UIDesignImageRole = "Page" | "Section" | "Modal" | "Drawer" | "Popover" | "State" | "Scrolled" | "Detail";
|
||||
@@ -1,5 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { UIDesignImageId } from "./UIDesignImageId";
|
||||
import type { UIDesignImageRole } from "./UIDesignImageRole";
|
||||
|
||||
export type UIDesignSuggestionTreeNode = { id: UIDesignImageId, name: string, description: string, role: UIDesignImageRole, children: Array<UIDesignSuggestionTreeNode>, };
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { State } from './types/State';
|
||||
import type { UIDesignImageId } from './types/UIDesignImageId';
|
||||
import type { UIDesignSuggestionTreeNode } from './types/UIDesignSuggestionTreeNode';
|
||||
|
||||
/**
|
||||
* Applies semantic suggestions conservatively. The tree carries relationships;
|
||||
* existing metadata remains authoritative whenever it is already populated.
|
||||
* Persisted `slave_to` remains the owning Page ID, so nested descendants inherit
|
||||
* their root Page rather than pointing at an intermediate Section.
|
||||
*/
|
||||
export function applyUiDesignSuggestions(
|
||||
state: State,
|
||||
suggestions: readonly UIDesignSuggestionTreeNode[],
|
||||
): State {
|
||||
const next = structuredClone(state);
|
||||
|
||||
function applyNode(
|
||||
node: UIDesignSuggestionTreeNode,
|
||||
pageId: UIDesignImageId | null,
|
||||
): void {
|
||||
const image = next.ui_design_images[node.id];
|
||||
if (!image) return;
|
||||
|
||||
if (image.metadata.name.trim().length === 0 && node.name.trim()) {
|
||||
image.metadata.name = node.name.trim();
|
||||
}
|
||||
if (image.metadata.role === null) {
|
||||
image.metadata.role = node.role;
|
||||
}
|
||||
if (image.metadata.slave_to === null && pageId !== null) {
|
||||
image.metadata.slave_to = pageId;
|
||||
}
|
||||
if (
|
||||
image.metadata.description.trim().length === 0 &&
|
||||
node.description.trim()
|
||||
) {
|
||||
image.metadata.description = node.description.trim();
|
||||
}
|
||||
|
||||
const effectiveRole = image.metadata.role ?? node.role;
|
||||
const childPageId = effectiveRole === 'Page' ? node.id : pageId;
|
||||
for (const child of node.children) {
|
||||
applyNode(child, childPageId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of suggestions) {
|
||||
applyNode(root, null);
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import type { SpriteBorder } from './types/SpriteBorder';
|
||||
import type { State } from './types/State';
|
||||
import type { UIDesignImage } from './types/UIDesignImage';
|
||||
import type { UIDesignImageId } from './types/UIDesignImageId';
|
||||
import type { UIDesignImageRole } from './types/UIDesignImageRole';
|
||||
import type { UiNodeMoveRequest } from './types/UiNodeMoveRequest';
|
||||
|
||||
export const EMPTY_UI_EDITOR_STATE: State = {
|
||||
@@ -85,23 +84,6 @@ type NodeLocation = {
|
||||
index: number;
|
||||
};
|
||||
|
||||
function wouldCreateSlaveToCycle(
|
||||
images: State['ui_design_images'],
|
||||
id: UIDesignImageId,
|
||||
slaveTo: UIDesignImageId,
|
||||
): boolean {
|
||||
let current: UIDesignImageId | null = slaveTo;
|
||||
const visited = new Set<UIDesignImageId>();
|
||||
while (current !== null) {
|
||||
if (current === id || visited.has(current)) return true;
|
||||
visited.add(current);
|
||||
const image: UIDesignImage | undefined = images[current];
|
||||
if (!image) return true;
|
||||
current = image.metadata.slave_to;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function visitNodes(node: Node, visit: (node: Node) => void): void {
|
||||
visit(node);
|
||||
for (const child of node.children) visitNodes(child, visit);
|
||||
@@ -278,7 +260,6 @@ export type DesignImageInput = {
|
||||
export type RemovalImpact = {
|
||||
removedResourceCount: number;
|
||||
removedTreeCount: number;
|
||||
clearedSlaveToCount: number;
|
||||
clearedTargetGraphicCount: number;
|
||||
clearedFontCount: number;
|
||||
};
|
||||
@@ -495,9 +476,6 @@ export function designImageRemovalImpact(
|
||||
removedResourceCount: id in state.ui_design_images ? 1 : 0,
|
||||
removedTreeCount: state.ui_trees.filter((tree) => tree.src_ui_design === id)
|
||||
.length,
|
||||
clearedSlaveToCount: Object.values(state.ui_design_images).filter(
|
||||
(image) => image.metadata.slave_to === id,
|
||||
).length,
|
||||
clearedTargetGraphicCount: 0,
|
||||
clearedFontCount: 0,
|
||||
};
|
||||
@@ -518,7 +496,6 @@ export function spriteAssetRemovalImpact(
|
||||
return {
|
||||
removedResourceCount: id in state.sprite_assets ? 1 : 0,
|
||||
removedTreeCount: 0,
|
||||
clearedSlaveToCount: 0,
|
||||
clearedTargetGraphicCount,
|
||||
clearedFontCount: 0,
|
||||
};
|
||||
@@ -539,7 +516,6 @@ export function fontAssetRemovalImpact(
|
||||
return {
|
||||
removedResourceCount: id in state.font_assets ? 1 : 0,
|
||||
removedTreeCount: 0,
|
||||
clearedSlaveToCount: 0,
|
||||
clearedTargetGraphicCount: 0,
|
||||
clearedFontCount,
|
||||
};
|
||||
@@ -740,86 +716,6 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
[],
|
||||
);
|
||||
|
||||
const setImageName = useCallback(
|
||||
(id: UIDesignImageId, name: string): UiEditorOperationResult => {
|
||||
const blocked = guard();
|
||||
if (blocked) return blocked;
|
||||
const current = stateRef.current;
|
||||
if (!(id in current.ui_design_images)) {
|
||||
return { ok: false, reason: 'missing' };
|
||||
}
|
||||
const next = cloneState(current);
|
||||
next.ui_design_images[id]!.metadata.name = name;
|
||||
commit(next);
|
||||
return { ok: true, value: undefined };
|
||||
},
|
||||
[commit, guard],
|
||||
);
|
||||
|
||||
const setImageDescription = useCallback(
|
||||
(id: UIDesignImageId, description: string): UiEditorOperationResult => {
|
||||
const blocked = guard();
|
||||
if (blocked) return blocked;
|
||||
const current = stateRef.current;
|
||||
if (!(id in current.ui_design_images)) {
|
||||
return { ok: false, reason: 'missing' };
|
||||
}
|
||||
const next = cloneState(current);
|
||||
next.ui_design_images[id]!.metadata.description = description;
|
||||
commit(next);
|
||||
return { ok: true, value: undefined };
|
||||
},
|
||||
[commit, guard],
|
||||
);
|
||||
|
||||
const setImageRole = useCallback(
|
||||
(
|
||||
id: UIDesignImageId,
|
||||
role: UIDesignImageRole | null,
|
||||
): UiEditorOperationResult => {
|
||||
const blocked = guard();
|
||||
if (blocked) return blocked;
|
||||
const current = stateRef.current;
|
||||
if (!(id in current.ui_design_images)) {
|
||||
return { ok: false, reason: 'missing' };
|
||||
}
|
||||
const next = cloneState(current);
|
||||
next.ui_design_images[id]!.metadata.role = role;
|
||||
synchronizeDesignImageTrees(next);
|
||||
commit(next);
|
||||
return { ok: true, value: undefined };
|
||||
},
|
||||
[commit, guard],
|
||||
);
|
||||
|
||||
const setImageSlaveTo = useCallback(
|
||||
(
|
||||
id: UIDesignImageId,
|
||||
slaveTo: UIDesignImageId | null,
|
||||
): UiEditorOperationResult => {
|
||||
const blocked = guard();
|
||||
if (blocked) return blocked;
|
||||
const current = stateRef.current;
|
||||
if (!(id in current.ui_design_images)) {
|
||||
return { ok: false, reason: 'missing' };
|
||||
}
|
||||
if (slaveTo !== null && !(slaveTo in current.ui_design_images)) {
|
||||
return { ok: false, reason: 'missing' };
|
||||
}
|
||||
if (
|
||||
slaveTo !== null &&
|
||||
wouldCreateSlaveToCycle(current.ui_design_images, id, slaveTo)
|
||||
) {
|
||||
return { ok: false, reason: 'invalid:slave_to 不能形成循环' };
|
||||
}
|
||||
const next = cloneState(current);
|
||||
next.ui_design_images[id]!.metadata.slave_to = slaveTo;
|
||||
commit(next);
|
||||
return { ok: true, value: undefined };
|
||||
},
|
||||
[commit, guard],
|
||||
);
|
||||
|
||||
const addDesignImages = useCallback(
|
||||
(entries: readonly DesignImageInput[]): UiEditorOperationResult => {
|
||||
const blocked = guard();
|
||||
@@ -1384,9 +1280,6 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
const next = cloneState(current);
|
||||
delete next.ui_design_images[id];
|
||||
next.ui_trees = next.ui_trees.filter((tree) => tree.src_ui_design !== id);
|
||||
for (const image of Object.values(next.ui_design_images)) {
|
||||
if (image.metadata.slave_to === id) image.metadata.slave_to = null;
|
||||
}
|
||||
commit(next);
|
||||
return { ok: true, value: impact };
|
||||
},
|
||||
@@ -1486,10 +1379,6 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
|
||||
isLocked,
|
||||
runWithStateLocked,
|
||||
setImageName,
|
||||
setImageDescription,
|
||||
setImageRole,
|
||||
setImageSlaveTo,
|
||||
addDesignImages,
|
||||
addSpriteAssets,
|
||||
addFontAssets,
|
||||
|
||||
@@ -6352,7 +6352,7 @@ export default function ProjectDevelopmentView({
|
||||
...(result.asset.source.generationKind === 'ui-workflow.completed'
|
||||
? {
|
||||
initialStep: 'asset-separation',
|
||||
initialFurthestStepIndex: 2,
|
||||
initialFurthestStepIndex: 1,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -6403,7 +6403,7 @@ export default function ProjectDevelopmentView({
|
||||
)?.source.generationKind === 'ui-workflow.completed'
|
||||
? {
|
||||
initialStep: 'asset-separation' as const,
|
||||
initialFurthestStepIndex: 2,
|
||||
initialFurthestStepIndex: 1,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -6508,7 +6508,7 @@ export default function ProjectDevelopmentView({
|
||||
completed.localPath.split(/[\\/]/u).filter(Boolean).pop() ??
|
||||
'UI 设计资源',
|
||||
initialStep: 'asset-separation',
|
||||
initialFurthestStepIndex: 2,
|
||||
initialFurthestStepIndex: 1,
|
||||
});
|
||||
}, [advanceFocusGeneration, manifest.assets, uiEditorRoute]);
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ export function EditorDialogs({
|
||||
</p>
|
||||
<ul className="rounded-lg bg-black/4 p-3 pl-7 text-xs leading-6">
|
||||
<li>删除组件树:{impact?.removedTreeCount ?? 0}</li>
|
||||
<li>清空界面归属:{impact?.clearedSlaveToCount ?? 0}</li>
|
||||
<li>清空图片组件引用:{impact?.clearedTargetGraphicCount ?? 0}</li>
|
||||
<li>清空文本字体引用:{impact?.clearedFontCount ?? 0}</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { Node as UiNode } from '../../../features/ui-editor/types/Node';
|
||||
import type { SpriteAsset } from '../../../features/ui-editor/types/SpriteAsset';
|
||||
import type { UIDesignImage } from '../../../features/ui-editor/types/UIDesignImage';
|
||||
import type { UITree } from '../../../features/ui-editor/types/UITree';
|
||||
|
||||
function countUiComponents(nodes: UiNode[]): number {
|
||||
return nodes.reduce(
|
||||
(total, node) =>
|
||||
total + (node.component ? 1 : 0) + countUiComponents(node.children),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
export function ImportOverview({
|
||||
images,
|
||||
sprites,
|
||||
uiTrees,
|
||||
}: {
|
||||
images: Record<string, UIDesignImage>;
|
||||
sprites: Record<string, SpriteAsset>;
|
||||
uiTrees: UITree[];
|
||||
}) {
|
||||
const componentCount = uiTrees.reduce(
|
||||
(total, tree) => total + countUiComponents([tree.root]),
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="shrink-0 border-b border-l border-(--platform-subpanel-border) bg-white/35 px-4 pb-3"
|
||||
aria-label="导入概览"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-[10px] font-semibold tracking-wider text-(--platform-text-soft) uppercase">
|
||||
Overview
|
||||
</span>
|
||||
<h2 className="m-0 text-sm font-semibold">导入概览</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
<div className="rounded-lg border border-(--platform-subpanel-border) bg-white/45 p-2 text-center">
|
||||
<strong className="block text-base font-semibold text-(--platform-text-strong)">
|
||||
{Object.keys(images).length}
|
||||
</strong>
|
||||
<span className="block text-[10px] text-(--platform-text-soft)">
|
||||
界面图数量
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-(--platform-subpanel-border) bg-white/45 p-2 text-center">
|
||||
<strong className="block text-base font-semibold text-(--platform-text-strong)">
|
||||
{Object.keys(sprites).length}
|
||||
</strong>
|
||||
<span className="block text-[10px] text-(--platform-text-soft)">
|
||||
素材数量
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-(--platform-subpanel-border) bg-white/45 p-2 text-center">
|
||||
<strong className="block text-base font-semibold text-(--platform-text-strong)">
|
||||
{componentCount}
|
||||
</strong>
|
||||
<span className="block text-[10px] text-(--platform-text-soft)">
|
||||
已有组件
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,17 @@ import { visitUiNodes } from '../../../features/ui-editor/treeUtils';
|
||||
import type { Node as UiNode } from '../../../features/ui-editor/types/Node';
|
||||
import type { NodeId } from '../../../features/ui-editor/types/NodeId';
|
||||
import type { UIDesignImageId } from '../../../features/ui-editor/types/UIDesignImageId';
|
||||
import { resourceAssetDisplayName } from '../../project-development/resourceAssetDisplayName';
|
||||
import type { UiEditorInputProjection } from '../useUiEditorPage';
|
||||
import { CollapsibleSidebarPanel } from './CollapsibleSidebarPanel';
|
||||
import { UiTreePanel } from './UiTreePanel';
|
||||
|
||||
const SUPER_ROOT_ID = '__ui-editor-super-root__';
|
||||
|
||||
function designImageLabel(path: string, index: number) {
|
||||
return resourceAssetDisplayName(path) || `界面图 ${index + 1}`;
|
||||
}
|
||||
|
||||
export function InputSidebar({ input }: { input: UiEditorInputProjection }) {
|
||||
const {
|
||||
projectPath,
|
||||
@@ -130,7 +135,7 @@ export function InputSidebar({ input }: { input: UiEditorInputProjection }) {
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{imageOrder.length > 0 ? (
|
||||
imageOrder.map((id) => {
|
||||
imageOrder.map((id, index) => {
|
||||
const image = images[id];
|
||||
if (!image) return null;
|
||||
return (
|
||||
@@ -157,10 +162,9 @@ export function InputSidebar({ input }: { input: UiEditorInputProjection }) {
|
||||
</div>
|
||||
<span className="min-w-0 flex-1">
|
||||
<strong className="block truncate text-xs">
|
||||
{image.metadata.name}
|
||||
{designImageLabel(image.path, index)}
|
||||
</strong>
|
||||
<span className="block truncate text-[10px] text-(--platform-text-soft)">
|
||||
{image.metadata.role ?? '自动判断'} ·{' '}
|
||||
{image.pixel_size[0]} × {image.pixel_size[1]}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
+4
-76
@@ -12,10 +12,8 @@ import { FontSamplePreview } from '../../../../features/ui-editor/components/Fon
|
||||
import { SpriteImagePreview } from '../../../../features/ui-editor/components/SpriteImagePreview';
|
||||
import type { StageStatusField } from '../../../../features/ui-editor/stageStatusOverview';
|
||||
import type { NodeId } from '../../../../features/ui-editor/types/NodeId';
|
||||
import type { UIDesignImageId } from '../../../../features/ui-editor/types/UIDesignImageId';
|
||||
import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UIDesignImageRole';
|
||||
import { uiEditorPrivateFontFamily } from '../../../../features/ui-editor/useUiEditorFontFaces';
|
||||
import { UI_DESIGN_IMAGE_ROLES } from '../../model';
|
||||
import { resourceAssetDisplayName } from '../../../project-development/resourceAssetDisplayName';
|
||||
import type { UiEditorInspectorProjection } from '../../useUiEditorPage';
|
||||
import { ComponentPanel } from './Components/ComponentPanel';
|
||||
import {
|
||||
@@ -64,7 +62,6 @@ type InspectorView =
|
||||
kind: 'image';
|
||||
image: ActiveImage;
|
||||
imageId: ActiveImageId;
|
||||
pageOptions: UiEditorInspectorProjection['pageOptions'];
|
||||
}
|
||||
| { kind: 'empty' };
|
||||
|
||||
@@ -155,11 +152,6 @@ export function InspectorSidebar({
|
||||
<ImageInspector
|
||||
image={view.image}
|
||||
imageId={view.imageId}
|
||||
pageOptions={view.pageOptions}
|
||||
onNameChange={inspector.setImageName}
|
||||
onDescriptionChange={inspector.setImageDescription}
|
||||
onRoleChange={inspector.setImageRole}
|
||||
onSlaveToChange={inspector.setImageSlaveTo}
|
||||
onDelete={() => inspector.requestDesignImageRemoval(view.imageId)}
|
||||
deleteDisabled={inspector.isLocked}
|
||||
/>
|
||||
@@ -226,7 +218,6 @@ function getInspectorView(
|
||||
spriteReferenceCounts,
|
||||
fontReferenceCounts,
|
||||
fontFaces,
|
||||
pageOptions,
|
||||
} = inspector;
|
||||
|
||||
if (selectedNode) {
|
||||
@@ -263,7 +254,6 @@ function getInspectorView(
|
||||
kind: 'image',
|
||||
image: activeImage,
|
||||
imageId: activeImageId,
|
||||
pageOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1034,21 +1024,11 @@ function InspectorReadout({ label, value }: { label: string; value: string }) {
|
||||
function ImageInspector({
|
||||
image,
|
||||
imageId,
|
||||
pageOptions,
|
||||
onNameChange,
|
||||
onDescriptionChange,
|
||||
onRoleChange,
|
||||
onSlaveToChange,
|
||||
onDelete,
|
||||
deleteDisabled,
|
||||
}: {
|
||||
image: ActiveImage;
|
||||
imageId: ActiveImageId;
|
||||
pageOptions: UiEditorInspectorProjection['pageOptions'];
|
||||
onNameChange: UiEditorInspectorProjection['setImageName'];
|
||||
onDescriptionChange: UiEditorInspectorProjection['setImageDescription'];
|
||||
onRoleChange: UiEditorInspectorProjection['setImageRole'];
|
||||
onSlaveToChange: UiEditorInspectorProjection['setImageSlaveTo'];
|
||||
onDelete: () => void;
|
||||
deleteDisabled: boolean;
|
||||
}) {
|
||||
@@ -1056,65 +1036,13 @@ function ImageInspector({
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<InspectorInput
|
||||
label="名称"
|
||||
value={image.metadata.name}
|
||||
onChange={(event) => {
|
||||
if (!readOnly) onNameChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<InspectorTextarea
|
||||
label="描述"
|
||||
value={image.metadata.description}
|
||||
placeholder="补充这张界面图的语义描述"
|
||||
onChange={(event) => {
|
||||
if (!readOnly) onDescriptionChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-(--platform-text-soft)">
|
||||
{resourceAssetDisplayName(image.path)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<Metric label="宽度" value={`${image.pixel_size[0]} px`} />
|
||||
<Metric label="高度" value={`${image.pixel_size[1]} px`} />
|
||||
</div>
|
||||
<InspectorSelect
|
||||
label="界面角色"
|
||||
value={image.metadata.role ?? ''}
|
||||
onChange={(event) => {
|
||||
if (!readOnly) {
|
||||
onRoleChange(
|
||||
(event.target.value || null) as UIDesignImageRole | null,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">自动判断</option>
|
||||
{UI_DESIGN_IMAGE_ROLES.map((role) => (
|
||||
<option key={role.value} value={role.value}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</InspectorSelect>
|
||||
{image.metadata.role !== 'Page' || image.metadata.slave_to !== null ? (
|
||||
<InspectorSelect
|
||||
label="归属主页面"
|
||||
value={image.metadata.slave_to ?? ''}
|
||||
onChange={(event) => {
|
||||
if (!readOnly) {
|
||||
onSlaveToChange(
|
||||
(event.target.value || null) as UIDesignImageId | null,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">未设置</option>
|
||||
{pageOptions
|
||||
.filter(([id]) => id !== imageId)
|
||||
.map(([id, page]) => (
|
||||
<option key={id} value={id}>
|
||||
{page.metadata.name}
|
||||
</option>
|
||||
))}
|
||||
</InspectorSelect>
|
||||
) : null}
|
||||
<ResourceId value={imageId} />
|
||||
<DeleteResourceButton
|
||||
label="删除"
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ToolNavigation({
|
||||
}) {
|
||||
return (
|
||||
<nav
|
||||
className="grid shrink-0 grid-cols-3 gap-2 border-b border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-3"
|
||||
className="grid shrink-0 grid-cols-2 gap-2 border-b border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-3"
|
||||
aria-label="UI 编辑流程"
|
||||
>
|
||||
{UI_EDITOR_STEPS.map((step, index) => {
|
||||
|
||||
@@ -8,17 +8,14 @@ export function WorkflowActionCard({
|
||||
}) {
|
||||
const action = getStepAction(workflow);
|
||||
const running =
|
||||
(workflow.activeStep === 'reference-analysis' && workflow.isSuggesting) ||
|
||||
(workflow.activeStep === 'structure-recognition' &&
|
||||
workflow.isRecognizing) ||
|
||||
(workflow.activeStep === 'asset-separation' && workflow.isSeparating);
|
||||
const status = {
|
||||
'reference-analysis': workflow.suggestionStatus,
|
||||
'structure-recognition': workflow.recognitionStatus,
|
||||
'asset-separation': workflow.separationStatus,
|
||||
}[workflow.activeStep];
|
||||
const hasRun = {
|
||||
'reference-analysis': workflow.hasSuggested,
|
||||
'structure-recognition': workflow.hasRecognized,
|
||||
'asset-separation': workflow.hasSeparated,
|
||||
}[workflow.activeStep];
|
||||
@@ -100,13 +97,6 @@ export function WorkflowActionCard({
|
||||
}
|
||||
|
||||
function getStepAction(workflow: UiEditorWorkflowProjection) {
|
||||
if (workflow.activeStep === 'reference-analysis') {
|
||||
return {
|
||||
label: '分析参考图',
|
||||
runningLabel: '分析中…',
|
||||
action: workflow.suggestUiDesignSemantics,
|
||||
};
|
||||
}
|
||||
if (workflow.activeStep === 'structure-recognition') {
|
||||
return {
|
||||
label: '识别界面结构',
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
validateAssetSeparationPrerequisites,
|
||||
validateAssetSeparationResult,
|
||||
validateComponentRecognitionPrerequisites,
|
||||
validateReferenceAnalysisResult,
|
||||
validateStructureRecognitionResult,
|
||||
} from '../../../features/ui-editor/requisites';
|
||||
import type { State } from '../../../features/ui-editor/types/State';
|
||||
@@ -16,8 +15,6 @@ export function prerequisiteIssuesForStep(
|
||||
step: UiEditorStepId,
|
||||
): UiEditorPrerequisiteIssue[] {
|
||||
switch (step) {
|
||||
case 'reference-analysis':
|
||||
return [];
|
||||
case 'structure-recognition':
|
||||
return validateComponentRecognitionPrerequisites(state);
|
||||
case 'asset-separation':
|
||||
@@ -30,8 +27,6 @@ export function postCheckIssuesForStep(
|
||||
step: UiEditorStepId,
|
||||
): UiEditorPrerequisiteIssue[] {
|
||||
switch (step) {
|
||||
case 'reference-analysis':
|
||||
return validateReferenceAnalysisResult(state);
|
||||
case 'structure-recognition':
|
||||
return validateStructureRecognitionResult(state);
|
||||
case 'asset-separation':
|
||||
@@ -43,7 +38,6 @@ export function postCheckIssuesForSave(
|
||||
state: State,
|
||||
): UiEditorPrerequisiteIssue[] {
|
||||
return [
|
||||
...validateReferenceAnalysisResult(state),
|
||||
...validateStructureRecognitionResult(state),
|
||||
...validateAssetSeparationResult(state),
|
||||
];
|
||||
@@ -54,8 +48,6 @@ export function activeStepPrerequisiteIssues(
|
||||
step: UiEditorStepId,
|
||||
): UiEditorPrerequisiteIssue[] {
|
||||
switch (step) {
|
||||
case 'reference-analysis':
|
||||
return validateComponentRecognitionPrerequisites(state);
|
||||
case 'structure-recognition':
|
||||
return validateAssetSeparationPrerequisites(state);
|
||||
case 'asset-separation':
|
||||
|
||||
+4
-1
@@ -31,6 +31,7 @@ import {
|
||||
import { findNodePageContext } from '../../../../features/ui-editor/nodeTransformGeometry';
|
||||
import type { Node } from '../../../../features/ui-editor/types/Node';
|
||||
import type { NodeId } from '../../../../features/ui-editor/types/NodeId';
|
||||
import { resourceAssetDisplayName } from '../../../project-development/resourceAssetDisplayName';
|
||||
import type { UiEditorCanvasProjection } from '../../useUiEditorPage';
|
||||
import { UiNodeContextMenu } from '../UiNodeContextMenu';
|
||||
import { resolvePreviewGridStep } from './previewGrid';
|
||||
@@ -694,7 +695,9 @@ export function PreviewWorkspace({
|
||||
{showOriginImage && previewUrls[item.src_ui_design] ? (
|
||||
<img
|
||||
src={previewUrls[item.src_ui_design]}
|
||||
alt={image?.metadata.name ?? ''}
|
||||
alt={
|
||||
image ? resourceAssetDisplayName(image.path) : ''
|
||||
}
|
||||
className="pointer-events-none absolute inset-0 size-full object-fill shadow-[0_18px_50px_rgb(67_48_37_/_14%)]"
|
||||
draggable={false}
|
||||
/>
|
||||
|
||||
@@ -14,8 +14,6 @@ export function appendWorkflowCheckPrompt(message: string): string {
|
||||
|
||||
export function workflowStepLabel(step: UiEditorStepId): string {
|
||||
switch (step) {
|
||||
case 'reference-analysis':
|
||||
return '分析参考图';
|
||||
case 'structure-recognition':
|
||||
return '识别界面结构';
|
||||
case 'asset-separation':
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
uiDesignStateStore,
|
||||
} from '../../features/ui-editor/uiDesignStateStore';
|
||||
import { EditorDialogs } from './components/EditorDialogs';
|
||||
import { ImportOverview } from './components/ImportOverview';
|
||||
import { InputSidebar } from './components/InputSidebar';
|
||||
import { InspectorSidebar } from './components/Inspector/InspectorSidebar';
|
||||
import { PreviewWorkspace } from './components/preview/PreviewWorkspace';
|
||||
@@ -91,7 +90,7 @@ export default function UiEditorPage({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (session.input.activeStep === 'asset-separation') {
|
||||
} else {
|
||||
overview = (
|
||||
<SeparationOverview
|
||||
uiTrees={session.input.uiTrees}
|
||||
@@ -103,14 +102,6 @@ export default function UiEditorPage({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
overview = (
|
||||
<ImportOverview
|
||||
images={session.input.images}
|
||||
sprites={session.input.sprites}
|
||||
uiTrees={session.input.uiTrees}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const saveDisabled =
|
||||
session.save.isSaving ||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import type { NodeId } from '../../features/ui-editor/types/NodeId';
|
||||
import type { UIDesignImageId } from '../../features/ui-editor/types/UIDesignImageId';
|
||||
import type { UIDesignImageRole } from '../../features/ui-editor/types/UIDesignImageRole';
|
||||
import type { RemovalImpact } from '../../features/ui-editor/useUiEditorState';
|
||||
|
||||
export type UiEditorStepId =
|
||||
| 'reference-analysis'
|
||||
| 'structure-recognition'
|
||||
| 'asset-separation';
|
||||
export type UiEditorStepId = 'structure-recognition' | 'asset-separation';
|
||||
export type UiEditorImportKind = 'design-image' | 'font' | 'sprite';
|
||||
|
||||
export type UiEditorNodeFocusRequest = {
|
||||
@@ -25,29 +21,13 @@ export const UI_EDITOR_STEPS: Array<{
|
||||
id: UiEditorStepId;
|
||||
label: string;
|
||||
}> = [
|
||||
{ id: 'reference-analysis', label: '分析参考图' },
|
||||
{ id: 'structure-recognition', label: '识别界面结构' },
|
||||
{ id: 'asset-separation', label: '自动切分素材' },
|
||||
];
|
||||
|
||||
export const UI_DESIGN_IMAGE_ROLES: Array<{
|
||||
value: UIDesignImageRole;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: 'Page', label: '主页面' },
|
||||
{ value: 'Section', label: '子界面 / 页签' },
|
||||
{ value: 'Modal', label: '模态弹窗' },
|
||||
{ value: 'Drawer', label: '抽屉 / 侧栏' },
|
||||
{ value: 'Popover', label: '局部浮层' },
|
||||
{ value: 'State', label: '交互状态' },
|
||||
{ value: 'Scrolled', label: '滚动后内容' },
|
||||
{ value: 'Detail', label: '局部详情' },
|
||||
];
|
||||
|
||||
export function removalHasDownstreamReferences(impact: RemovalImpact) {
|
||||
return (
|
||||
impact.removedTreeCount +
|
||||
impact.clearedSlaveToCount +
|
||||
impact.clearedTargetGraphicCount +
|
||||
impact.clearedFontCount >
|
||||
0
|
||||
|
||||
@@ -35,18 +35,13 @@ import type { SeparationDTO } from '../../features/ui-editor/types/SeparationDTO
|
||||
import type { SeparationRecoveryDTO } from '../../features/ui-editor/types/SeparationRecoveryDTO';
|
||||
import type { SpriteAssetId } from '../../features/ui-editor/types/SpriteAssetId';
|
||||
import type { SpriteBorder } from '../../features/ui-editor/types/SpriteBorder';
|
||||
import type { State } from '../../features/ui-editor/types/State';
|
||||
import type { UIDesignImage } from '../../features/ui-editor/types/UIDesignImage';
|
||||
import type { UIDesignImageId } from '../../features/ui-editor/types/UIDesignImageId';
|
||||
import type { UIDesignImageRole } from '../../features/ui-editor/types/UIDesignImageRole';
|
||||
import type { UIDesignSuggestionTreeNode } from '../../features/ui-editor/types/UIDesignSuggestionTreeNode';
|
||||
import type { UiNodeMoveRequest } from '../../features/ui-editor/types/UiNodeMoveRequest';
|
||||
import {
|
||||
type IUiDesignStateStore,
|
||||
type UiDesignCodeGenerationResult,
|
||||
uiDesignStateStore,
|
||||
} from '../../features/ui-editor/uiDesignStateStore';
|
||||
import { applyUiDesignSuggestions } from '../../features/ui-editor/uiDesignSuggestions';
|
||||
import { useUiEditorFontFaces } from '../../features/ui-editor/useUiEditorFontFaces';
|
||||
import { addSpriteAssetsToState } from '../../features/ui-editor/useUiEditorState';
|
||||
import {
|
||||
@@ -77,6 +72,7 @@ import {
|
||||
import {
|
||||
type PendingResourceRemoval,
|
||||
removalHasDownstreamReferences,
|
||||
UI_EDITOR_STEPS,
|
||||
type UiEditorImportKind,
|
||||
uiEditorOperationError,
|
||||
type UiEditorStepId,
|
||||
@@ -151,25 +147,6 @@ function isUiNodeEffectivelyVisible(
|
||||
return null;
|
||||
}
|
||||
|
||||
function isSlaveToDescendant(
|
||||
images: State['ui_design_images'],
|
||||
candidateId: UIDesignImageId,
|
||||
ancestorId: UIDesignImageId | null,
|
||||
): boolean {
|
||||
if (ancestorId === null || candidateId === ancestorId) return true;
|
||||
let current: UIDesignImageId | null = candidateId;
|
||||
const visited = new Set<UIDesignImageId>();
|
||||
while (current !== null) {
|
||||
if (visited.has(current)) return true;
|
||||
visited.add(current);
|
||||
const image: UIDesignImage | undefined = images[current];
|
||||
if (!image) return true;
|
||||
current = image.metadata.slave_to;
|
||||
if (current === ancestorId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type PendingWorkflowStepChange = {
|
||||
from: UiEditorStepId;
|
||||
to: UiEditorStepId;
|
||||
@@ -187,7 +164,7 @@ export function useUiEditorSession(
|
||||
projectPath: string,
|
||||
resourceId?: string,
|
||||
stateStore: IUiDesignStateStore = uiDesignStateStore,
|
||||
initialStep: UiEditorStepId = 'reference-analysis',
|
||||
initialStep: UiEditorStepId = 'structure-recognition',
|
||||
initialFurthestStepIndex = 0,
|
||||
) {
|
||||
const editor = useUiEditorState(EMPTY_UI_EDITOR_STATE);
|
||||
@@ -206,15 +183,16 @@ export function useUiEditorSession(
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const normalizedInitialStepIndex =
|
||||
initialStep === 'reference-analysis'
|
||||
? 0
|
||||
: initialStep === 'structure-recognition'
|
||||
? 1
|
||||
: 2;
|
||||
const normalizedInitialStepIndex = Math.max(
|
||||
0,
|
||||
UI_EDITOR_STEPS.findIndex((step) => step.id === initialStep),
|
||||
);
|
||||
const normalizedInitialFurthestStepIndex = Math.max(
|
||||
normalizedInitialStepIndex,
|
||||
Math.min(2, Math.max(0, Math.trunc(initialFurthestStepIndex))),
|
||||
Math.min(
|
||||
UI_EDITOR_STEPS.length - 1,
|
||||
Math.max(0, Math.trunc(initialFurthestStepIndex)),
|
||||
),
|
||||
);
|
||||
const [activeStep, setActiveStep] = useState<UiEditorStepId>(initialStep);
|
||||
const [furthestStepIndex, setFurthestStepIndex] = useState(
|
||||
@@ -251,15 +229,9 @@ export function useUiEditorSession(
|
||||
const [clearOpen, setClearOpen] = useState(false);
|
||||
const [pendingRemoval, setPendingRemoval] =
|
||||
useState<PendingResourceRemoval | null>(null);
|
||||
const suggestionOperation = useUiEditorOperation();
|
||||
const recognitionOperation = useUiEditorOperation();
|
||||
const mergeOperation = useUiEditorOperation();
|
||||
const separationOperation = useUiEditorOperation();
|
||||
const isSuggesting = suggestionOperation.running;
|
||||
const suggestionStatus = suggestionOperation.status;
|
||||
const beginSuggestion = suggestionOperation.begin;
|
||||
const finishSuggestion = suggestionOperation.finish;
|
||||
const setSuggestionStatus = suggestionOperation.setStatus;
|
||||
const isRecognizing = recognitionOperation.running;
|
||||
const recognitionStatus = recognitionOperation.status;
|
||||
const beginRecognition = recognitionOperation.begin;
|
||||
@@ -277,7 +249,6 @@ export function useUiEditorSession(
|
||||
const setSeparationStatus = separationOperation.setStatus;
|
||||
const [separationRecovery, setSeparationRecovery] =
|
||||
useState<SeparationRecoveryDTO | null>(null);
|
||||
const [hasSuggested, setHasSuggested] = useState(false);
|
||||
const [hasRecognized, setHasRecognized] = useState(false);
|
||||
const [hasSeparated, setHasSeparated] = useState(false);
|
||||
const [completionNotice, setCompletionNotice] =
|
||||
@@ -411,13 +382,7 @@ export function useUiEditorSession(
|
||||
const activeImage = activeImageId ? images[activeImageId] : null;
|
||||
const selectedSprite = selectedSpriteId ? sprites[selectedSpriteId] : null;
|
||||
const selectedFont = selectedFontId ? fonts[selectedFontId] : null;
|
||||
const pageOptions = Object.entries(images).filter(
|
||||
([id, image]) =>
|
||||
image.metadata.role === 'Page' &&
|
||||
!isSlaveToDescendant(images, id as UIDesignImageId, activeImageId),
|
||||
);
|
||||
const isAiRunning =
|
||||
isSuggesting || isRecognizing || isMerging || isSeparating;
|
||||
const isAiRunning = isRecognizing || isMerging || isSeparating;
|
||||
const isWorkflowBusy =
|
||||
isAiRunning || isSaving || isGenerating || isLoading || editor.isLocked;
|
||||
const stateSignature = JSON.stringify(editor.state);
|
||||
@@ -426,7 +391,6 @@ export function useUiEditorSession(
|
||||
savedStateSignature !== null &&
|
||||
savedStateSignature !== stateSignature;
|
||||
const nextStepByStep: Partial<Record<UiEditorStepId, UiEditorStepId>> = {
|
||||
'reference-analysis': 'structure-recognition',
|
||||
'structure-recognition': 'asset-separation',
|
||||
};
|
||||
const nextStep = nextStepByStep[activeStep] ?? null;
|
||||
@@ -717,13 +681,10 @@ export function useUiEditorSession(
|
||||
|
||||
function enterStep(step: UiEditorStepId) {
|
||||
setActiveStep(step);
|
||||
const index =
|
||||
step === 'reference-analysis'
|
||||
? 0
|
||||
: step === 'structure-recognition'
|
||||
? 1
|
||||
: 2;
|
||||
setFurthestStepIndex((current) => Math.max(current, index));
|
||||
const index = UI_EDITOR_STEPS.findIndex(
|
||||
(candidate) => candidate.id === step,
|
||||
);
|
||||
setFurthestStepIndex((current) => Math.max(current, Math.max(0, index)));
|
||||
}
|
||||
|
||||
function requestStepChange(step: UiEditorStepId) {
|
||||
@@ -953,26 +914,6 @@ export function useUiEditorSession(
|
||||
setSelectedFontId(id);
|
||||
}
|
||||
|
||||
function setImageName(name: string) {
|
||||
if (!activeImageId) return;
|
||||
editor.setImageName(activeImageId, name);
|
||||
}
|
||||
|
||||
function setImageDescription(description: string) {
|
||||
if (!activeImageId) return;
|
||||
editor.setImageDescription(activeImageId, description);
|
||||
}
|
||||
|
||||
function setImageRole(role: UIDesignImageRole | null) {
|
||||
if (!activeImageId) return;
|
||||
editor.setImageRole(activeImageId, role);
|
||||
}
|
||||
|
||||
function setImageSlaveTo(slaveTo: UIDesignImageId | null) {
|
||||
if (!activeImageId) return;
|
||||
editor.setImageSlaveTo(activeImageId, slaveTo);
|
||||
}
|
||||
|
||||
function setSpriteName(name: string) {
|
||||
if (!selectedSpriteId) return;
|
||||
editor.setSpriteName(selectedSpriteId, name);
|
||||
@@ -988,38 +929,6 @@ export function useUiEditorSession(
|
||||
editor.setSpriteBorder(selectedSpriteId, border);
|
||||
}
|
||||
|
||||
async function suggestUiDesignSemantics() {
|
||||
if (isSuggesting || isWorkflowBusy) return;
|
||||
setCompletionNotice(null);
|
||||
setSuggestionStatus(null);
|
||||
beginSuggestion();
|
||||
try {
|
||||
await editor.runWithStateLocked(async (snapshot) => {
|
||||
const suggestions = await invoke<UIDesignSuggestionTreeNode[]>(
|
||||
'suggest_ui_design_semantic',
|
||||
{ projectPath, state: snapshot },
|
||||
);
|
||||
editor.replaceState(applyUiDesignSuggestions(snapshot, suggestions));
|
||||
setHasSuggested(true);
|
||||
reportWorkflowCompletion(
|
||||
'reference-analysis',
|
||||
'success',
|
||||
`参考图分析完成:已应用 ${suggestions.length} 条参考图语义建议`,
|
||||
setSuggestionStatus,
|
||||
);
|
||||
});
|
||||
} catch (cause) {
|
||||
reportWorkflowCompletion(
|
||||
'reference-analysis',
|
||||
'failure',
|
||||
cause instanceof Error ? cause.message : String(cause),
|
||||
setSuggestionStatus,
|
||||
);
|
||||
} finally {
|
||||
finishSuggestion();
|
||||
}
|
||||
}
|
||||
|
||||
async function recognizeUi() {
|
||||
if (isRecognizing || isWorkflowBusy) return;
|
||||
setCompletionNotice(null);
|
||||
@@ -1587,16 +1496,13 @@ export function useUiEditorSession(
|
||||
operations: {
|
||||
isMerging,
|
||||
isRecognizing,
|
||||
isSuggesting,
|
||||
mergeStatus,
|
||||
recognitionStatus,
|
||||
suggestionStatus,
|
||||
},
|
||||
checkPrerequisites,
|
||||
separateUi,
|
||||
mergeUi,
|
||||
recognizeUi,
|
||||
suggestUiDesignSemantics,
|
||||
openImporter: setImportKind,
|
||||
selectDesignImage,
|
||||
selectSprite,
|
||||
@@ -1666,7 +1572,6 @@ export function useUiEditorSession(
|
||||
sprites,
|
||||
fonts,
|
||||
fontFaces,
|
||||
pageOptions,
|
||||
spriteReferenceCounts,
|
||||
fontReferenceCounts,
|
||||
keepChildrenUnchanged,
|
||||
@@ -1685,10 +1590,6 @@ export function useUiEditorSession(
|
||||
setSpriteBorder,
|
||||
requestSpriteRemoval,
|
||||
requestFontRemoval,
|
||||
setImageName,
|
||||
setImageDescription,
|
||||
setImageRole,
|
||||
setImageSlaveTo,
|
||||
requestDesignImageRemoval,
|
||||
},
|
||||
workflow: {
|
||||
@@ -1698,10 +1599,6 @@ export function useUiEditorSession(
|
||||
isAiRunning,
|
||||
isBusy: isWorkflowBusy,
|
||||
pendingStepChange: pendingWorkflowStepChange,
|
||||
isSuggesting,
|
||||
hasSuggested,
|
||||
suggestionStatus,
|
||||
suggestUiDesignSemantics,
|
||||
isRecognizing,
|
||||
hasRecognized,
|
||||
recognitionStatus,
|
||||
|
||||
@@ -52,12 +52,6 @@ const root: UiNode = {
|
||||
};
|
||||
|
||||
const activeImage: UIDesignImage = {
|
||||
metadata: {
|
||||
name: '测试界面',
|
||||
description: '',
|
||||
role: null,
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'assets/page.png',
|
||||
pixel_size: [1200, 800],
|
||||
pixels_per_unit: 1,
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { State } from '../src/features/ui-editor/types/State';
|
||||
import { applyUiDesignSuggestions } from '../src/features/ui-editor/uiDesignSuggestions';
|
||||
|
||||
function state(): State {
|
||||
return {
|
||||
ui_trees: [],
|
||||
sprite_assets: {},
|
||||
font_assets: {},
|
||||
ui_design_images: {
|
||||
page: {
|
||||
metadata: { name: '', description: '', role: null, slave_to: null },
|
||||
path: 'page.png',
|
||||
pixel_size: [100, 100],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
section: {
|
||||
metadata: {
|
||||
name: '已有名称',
|
||||
description: '',
|
||||
role: 'Section',
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'section.png',
|
||||
pixel_size: [100, 100],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
detail: {
|
||||
metadata: {
|
||||
name: '',
|
||||
description: '已有描述',
|
||||
role: null,
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'detail.png',
|
||||
pixel_size: [100, 100],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('applyUiDesignSuggestions', () => {
|
||||
it('masks existing metadata and maps descendants to their owning Page', () => {
|
||||
const before = state();
|
||||
const after = applyUiDesignSuggestions(before, [
|
||||
{
|
||||
id: 'page',
|
||||
name: '主页面',
|
||||
description: '页面描述',
|
||||
role: 'Page',
|
||||
children: [
|
||||
{
|
||||
id: 'section',
|
||||
name: '模型名称不应覆盖',
|
||||
description: '页签描述',
|
||||
role: 'Section',
|
||||
children: [
|
||||
{
|
||||
id: 'detail',
|
||||
name: '详情',
|
||||
description: '模型描述不应覆盖',
|
||||
role: 'Detail',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(after.ui_design_images.page.metadata).toEqual({
|
||||
name: '主页面',
|
||||
description: '页面描述',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
});
|
||||
expect(after.ui_design_images.section.metadata).toEqual({
|
||||
name: '已有名称',
|
||||
description: '页签描述',
|
||||
role: 'Section',
|
||||
slave_to: 'page',
|
||||
});
|
||||
expect(after.ui_design_images.detail.metadata).toEqual({
|
||||
name: '详情',
|
||||
description: '已有描述',
|
||||
role: 'Detail',
|
||||
slave_to: 'page',
|
||||
});
|
||||
expect(before.ui_design_images.page.metadata.name).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -103,12 +103,6 @@ function stateWithPages(pageIds: string[]): State {
|
||||
pageIds.map((id) => [
|
||||
id,
|
||||
{
|
||||
metadata: {
|
||||
name: id,
|
||||
description: '',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
},
|
||||
path: `assets/${id}.png`,
|
||||
pixel_size: [320, 180],
|
||||
pixels_per_unit: 1,
|
||||
@@ -362,10 +356,6 @@ describe('UiEditorPage', () => {
|
||||
it('renders the overview that belongs to the active workflow stage', () => {
|
||||
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
|
||||
|
||||
expect(screen.getByRole('heading', { name: '导入概览' })).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /识别界面结构/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '仍然继续' }));
|
||||
expect(screen.getByRole('heading', { name: '识别概览' })).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /自动切分素材/ }));
|
||||
@@ -391,7 +381,7 @@ describe('UiEditorPage', () => {
|
||||
resourceId: 'ui-resource',
|
||||
stateStore,
|
||||
initialStep: 'asset-separation',
|
||||
initialFurthestStepIndex: 2,
|
||||
initialFurthestStepIndex: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -456,7 +446,7 @@ describe('UiEditorPage', () => {
|
||||
resourceId: 'ui-resource',
|
||||
stateStore,
|
||||
initialStep: 'structure-recognition',
|
||||
initialFurthestStepIndex: 1,
|
||||
initialFurthestStepIndex: 0,
|
||||
}),
|
||||
);
|
||||
const button = await screen.findByRole('button', {
|
||||
@@ -504,7 +494,7 @@ describe('UiEditorPage', () => {
|
||||
resourceId: 'ui-resource',
|
||||
stateStore,
|
||||
initialStep: 'structure-recognition',
|
||||
initialFurthestStepIndex: 1,
|
||||
initialFurthestStepIndex: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
|
||||
function image(name: string): UIDesignImage {
|
||||
return {
|
||||
metadata: { name, role: null, slave_to: null },
|
||||
path: `assets/${name}.png`,
|
||||
pixel_size: [100, 80],
|
||||
pixels_per_unit: 1,
|
||||
@@ -264,15 +263,11 @@ describe('useUiEditorState', () => {
|
||||
{ id: 'section-a', image: image('Section A') },
|
||||
]),
|
||||
).toEqual({ ok: true, value: undefined });
|
||||
result.current.setImageRole('page-a', 'Page');
|
||||
result.current.setImageRole('section-a', 'Section');
|
||||
result.current.setImageSlaveTo('section-a', 'page-a');
|
||||
result.current.setImageName('section-a', '任务页');
|
||||
});
|
||||
|
||||
expect(result.current.state.ui_design_images['section-a']).toMatchObject({
|
||||
metadata: { name: '任务页', role: 'Section', slave_to: 'page-a' },
|
||||
});
|
||||
expect(result.current.state.ui_design_images['section-a']).toEqual(
|
||||
image('Section A'),
|
||||
);
|
||||
expect(result.current.state.ui_trees).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -312,33 +307,6 @@ describe('useUiEditorState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects self-references and slave_to cycles', () => {
|
||||
const { result } = renderHook(() => useUiEditorState());
|
||||
|
||||
act(() => {
|
||||
result.current.addDesignImages([
|
||||
{ id: 'page', image: image('Page') },
|
||||
{ id: 'section', image: image('Section') },
|
||||
]);
|
||||
result.current.setImageRole('page', 'Page');
|
||||
result.current.setImageSlaveTo('section', 'page');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
expect(result.current.setImageSlaveTo('page', 'page')).toEqual({
|
||||
ok: false,
|
||||
reason: 'invalid:slave_to 不能形成循环',
|
||||
});
|
||||
expect(result.current.setImageSlaveTo('page', 'section')).toEqual({
|
||||
ok: false,
|
||||
reason: 'invalid:slave_to 不能形成循环',
|
||||
});
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.slave_to).toBe(
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a batch atomically and rejects duplicates and limits', () => {
|
||||
const { result } = renderHook(() => useUiEditorState());
|
||||
|
||||
@@ -395,7 +363,11 @@ describe('useUiEditorState', () => {
|
||||
|
||||
expect(result.current.isLocked).toBe(true);
|
||||
act(() => {
|
||||
expect(result.current.setImageName('a', 'Locked')).toEqual({
|
||||
expect(
|
||||
result.current.addDesignImages([
|
||||
{ id: 'locked', image: image('Locked') },
|
||||
]),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
reason: 'locked',
|
||||
});
|
||||
@@ -424,14 +396,8 @@ describe('useUiEditorState', () => {
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: {
|
||||
page: {
|
||||
...image('Page'),
|
||||
metadata: { name: 'Page', role: 'Page', slave_to: null },
|
||||
},
|
||||
child: {
|
||||
...image('Child'),
|
||||
metadata: { name: 'Child', role: 'Section', slave_to: 'page' },
|
||||
},
|
||||
page: image('Page'),
|
||||
child: image('Child'),
|
||||
},
|
||||
sprite_assets: { panel: sprite('panel') },
|
||||
ui_trees: [
|
||||
@@ -478,7 +444,6 @@ describe('useUiEditorState', () => {
|
||||
value: {
|
||||
removedResourceCount: 1,
|
||||
removedTreeCount: 0,
|
||||
clearedSlaveToCount: 0,
|
||||
clearedTargetGraphicCount: 1,
|
||||
clearedFontCount: 0,
|
||||
},
|
||||
@@ -493,9 +458,7 @@ describe('useUiEditorState', () => {
|
||||
expect(result.current.state.sprite_assets.panel).toBeUndefined();
|
||||
expect(result.current.state.ui_trees).toHaveLength(1);
|
||||
expect(result.current.state.ui_design_images.page).toBeUndefined();
|
||||
expect(
|
||||
result.current.state.ui_design_images.child?.metadata.slave_to,
|
||||
).toBeNull();
|
||||
expect(result.current.state.ui_design_images.child).toBeDefined();
|
||||
});
|
||||
|
||||
it('merges identical sprite and font resources idempotently and rejects identity conflicts', () => {
|
||||
@@ -584,7 +547,6 @@ describe('useUiEditorState', () => {
|
||||
value: {
|
||||
removedResourceCount: 1,
|
||||
removedTreeCount: 0,
|
||||
clearedSlaveToCount: 0,
|
||||
clearedTargetGraphicCount: 0,
|
||||
clearedFontCount: 1,
|
||||
},
|
||||
@@ -597,48 +559,15 @@ describe('useUiEditorState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps setters local and defers workflow errors to prerequisites', () => {
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: {
|
||||
page: {
|
||||
...image('Page'),
|
||||
metadata: { name: 'Page', role: 'Page', slave_to: null },
|
||||
},
|
||||
child: {
|
||||
...image('Child'),
|
||||
metadata: { name: 'Child', role: 'Section', slave_to: 'page' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderHook(() => useUiEditorState(initial));
|
||||
|
||||
act(() => {
|
||||
result.current.setImageRole('page', null);
|
||||
});
|
||||
|
||||
expect(result.current.state.ui_design_images.child?.metadata.slave_to).toBe(
|
||||
'page',
|
||||
);
|
||||
expect(
|
||||
validateComponentRecognitionPrerequisites(result.current.state),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
code: 'invalid-slave-to',
|
||||
resourceId: 'child',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('records, undoes, redoes, and clears redo after a new edit', () => {
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: { page: image('Page') },
|
||||
sprite_assets: { panel: sprite('panel') },
|
||||
};
|
||||
const { result } = renderHook(() => useUiEditorState(initial));
|
||||
|
||||
act(() => {
|
||||
result.current.setImageName('page', '第一次');
|
||||
result.current.setSpriteName('panel', '第一次');
|
||||
});
|
||||
expect(result.current.historyState).toEqual({
|
||||
canUndo: true,
|
||||
@@ -648,8 +577,8 @@ describe('useUiEditorState', () => {
|
||||
act(() => {
|
||||
expect(result.current.undo()).toBe(true);
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
'Page',
|
||||
expect(result.current.state.sprite_assets.panel?.metadata.name).toBe(
|
||||
'panel',
|
||||
);
|
||||
expect(result.current.historyState).toEqual({
|
||||
canUndo: false,
|
||||
@@ -659,15 +588,15 @@ describe('useUiEditorState', () => {
|
||||
act(() => {
|
||||
expect(result.current.redo()).toBe(true);
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
expect(result.current.state.sprite_assets.panel?.metadata.name).toBe(
|
||||
'第一次',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setImageName('page', '第二次');
|
||||
result.current.setSpriteName('panel', '第二次');
|
||||
expect(result.current.redo()).toBe(false);
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
expect(result.current.state.sprite_assets.panel?.metadata.name).toBe(
|
||||
'第二次',
|
||||
);
|
||||
});
|
||||
@@ -675,17 +604,17 @@ describe('useUiEditorState', () => {
|
||||
it('does not record no-op edits and resets history when replacing loaded state', () => {
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: { page: image('Page') },
|
||||
sprite_assets: { panel: sprite('panel') },
|
||||
};
|
||||
const { result } = renderHook(() => useUiEditorState(initial));
|
||||
|
||||
act(() => {
|
||||
result.current.setImageName('page', 'Page');
|
||||
result.current.setSpriteName('panel', 'panel');
|
||||
});
|
||||
expect(result.current.historyState.canUndo).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.setImageName('page', '编辑后');
|
||||
result.current.setSpriteName('panel', '编辑后');
|
||||
result.current.replaceState(initial, { history: 'reset' });
|
||||
});
|
||||
expect(result.current.historyState).toEqual({
|
||||
@@ -694,7 +623,7 @@ describe('useUiEditorState', () => {
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setImageName('page', '清空前');
|
||||
result.current.setSpriteName('panel', '清空前');
|
||||
result.current.clearState();
|
||||
});
|
||||
expect(result.current.state).toEqual(EMPTY_UI_EDITOR_STATE);
|
||||
@@ -707,27 +636,27 @@ describe('useUiEditorState', () => {
|
||||
it('records each replacement as an independent history entry', () => {
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: { page: image('Page') },
|
||||
sprite_assets: { panel: sprite('panel') },
|
||||
};
|
||||
const { result } = renderHook(() => useUiEditorState(initial));
|
||||
|
||||
act(() => {
|
||||
result.current.replaceState({
|
||||
...initial,
|
||||
ui_design_images: { page: image('中间') },
|
||||
sprite_assets: { panel: sprite('中间') },
|
||||
});
|
||||
result.current.replaceState({
|
||||
...initial,
|
||||
ui_design_images: { page: image('最终') },
|
||||
sprite_assets: { panel: sprite('最终') },
|
||||
});
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
expect(result.current.state.sprite_assets.panel?.metadata.name).toBe(
|
||||
'最终',
|
||||
);
|
||||
act(() => {
|
||||
expect(result.current.undo()).toBe(true);
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
expect(result.current.state.sprite_assets.panel?.metadata.name).toBe(
|
||||
'中间',
|
||||
);
|
||||
});
|
||||
@@ -735,7 +664,7 @@ describe('useUiEditorState', () => {
|
||||
it('records one history entry after skipped replacement batches', () => {
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: { page: image('Page') },
|
||||
sprite_assets: { panel: sprite('panel') },
|
||||
};
|
||||
const { result } = renderHook(() => useUiEditorState(initial));
|
||||
|
||||
@@ -743,20 +672,20 @@ describe('useUiEditorState', () => {
|
||||
result.current.replaceState(
|
||||
{
|
||||
...initial,
|
||||
ui_design_images: { page: image('第一批') },
|
||||
sprite_assets: { panel: sprite('第一批') },
|
||||
},
|
||||
{ history: 'skip' },
|
||||
);
|
||||
result.current.replaceState(
|
||||
{
|
||||
...initial,
|
||||
ui_design_images: { page: image('最终') },
|
||||
sprite_assets: { panel: sprite('最终') },
|
||||
},
|
||||
{ history: 'record' },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
expect(result.current.state.sprite_assets.panel?.metadata.name).toBe(
|
||||
'最终',
|
||||
);
|
||||
expect(result.current.historyState).toEqual({
|
||||
@@ -766,8 +695,8 @@ describe('useUiEditorState', () => {
|
||||
act(() => {
|
||||
expect(result.current.undo()).toBe(true);
|
||||
});
|
||||
expect(result.current.state.ui_design_images.page?.metadata.name).toBe(
|
||||
'Page',
|
||||
expect(result.current.state.sprite_assets.panel?.metadata.name).toBe(
|
||||
'panel',
|
||||
);
|
||||
expect(result.current.historyState).toEqual({
|
||||
canUndo: false,
|
||||
|
||||
Reference in New Issue
Block a user