深化 UI 编辑会话模块
将载入、revision、选择、预览、步骤门禁、异步操作和保存意图收敛到 UI 编辑会话 按输入、画布、检查器、工作流、弹窗和保存用途拆分视图 projection 补充 UI 编辑会话模块边界文档
This commit is contained in:
@@ -5,36 +5,36 @@ import {
|
||||
SPRITE_IMPORTER_SETTINGS,
|
||||
} from '../../../components/AssetImporter/settings';
|
||||
import { ThemedModal } from '../../../components/modal/ThemedModal';
|
||||
import type { UiEditorPageController } from '../useUiEditorPage';
|
||||
import type { UiEditorDialogsProjection } from '../useUiEditorPage';
|
||||
|
||||
export function EditorDialogs({
|
||||
controller,
|
||||
dialogs,
|
||||
}: {
|
||||
controller: UiEditorPageController;
|
||||
dialogs: UiEditorDialogsProjection;
|
||||
}) {
|
||||
const impact = controller.pendingRemoval?.impact;
|
||||
const impact = dialogs.pendingRemoval?.impact;
|
||||
const importerSettings =
|
||||
controller.importKind === 'font'
|
||||
dialogs.importKind === 'font'
|
||||
? FONT_IMPORTER_SETTINGS
|
||||
: controller.importKind === 'design-image'
|
||||
: dialogs.importKind === 'design-image'
|
||||
? DESIGN_IMAGE_IMPORTER_SETTINGS
|
||||
: SPRITE_IMPORTER_SETTINGS;
|
||||
return (
|
||||
<>
|
||||
<AssetImporter
|
||||
projectPath={controller.projectPath}
|
||||
open={controller.importKind !== null}
|
||||
onClose={controller.closeImporter}
|
||||
projectPath={dialogs.projectPath}
|
||||
open={dialogs.importKind !== null}
|
||||
onClose={dialogs.closeImporter}
|
||||
onImport={(assets) => {
|
||||
void controller.importAssets(assets);
|
||||
controller.closeImporter();
|
||||
void dialogs.importAssets(assets);
|
||||
dialogs.closeImporter();
|
||||
}}
|
||||
settings={importerSettings}
|
||||
/>
|
||||
|
||||
<ThemedModal
|
||||
open={controller.clearOpen}
|
||||
onClose={controller.closeClearDialog}
|
||||
open={dialogs.clearOpen}
|
||||
onClose={dialogs.closeClearDialog}
|
||||
ariaLabel="确认清空 UI Editor State"
|
||||
panelClassName="w-[420px] rounded-2xl p-5"
|
||||
>
|
||||
@@ -47,14 +47,14 @@ export function EditorDialogs({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-xs"
|
||||
onClick={controller.closeClearDialog}
|
||||
onClick={dialogs.closeClearDialog}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-red-600 px-3 py-2 text-xs font-semibold text-white"
|
||||
onClick={controller.clearState}
|
||||
onClick={dialogs.clearState}
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
@@ -62,8 +62,8 @@ export function EditorDialogs({
|
||||
</ThemedModal>
|
||||
|
||||
<ThemedModal
|
||||
open={controller.pendingRemoval !== null}
|
||||
onClose={controller.cancelRemoval}
|
||||
open={dialogs.pendingRemoval !== null}
|
||||
onClose={dialogs.cancelRemoval}
|
||||
ariaLabel="确认删除并清理引用"
|
||||
panelClassName="w-[440px] rounded-2xl p-5"
|
||||
>
|
||||
@@ -82,14 +82,14 @@ export function EditorDialogs({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-xs"
|
||||
onClick={controller.cancelRemoval}
|
||||
onClick={dialogs.cancelRemoval}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-red-600 px-3 py-2 text-xs font-semibold text-white"
|
||||
onClick={controller.confirmRemoval}
|
||||
onClick={dialogs.confirmRemoval}
|
||||
>
|
||||
删除并清理
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 type { UiEditorPageController } from '../useUiEditorPage';
|
||||
import type { UiEditorInputProjection } from '../useUiEditorPage';
|
||||
import { BindingOverview } from './BindingOverview';
|
||||
import { ImportOverview } from './ImportOverview';
|
||||
import { RecognitionOverview } from './RecognitionOverview';
|
||||
@@ -13,14 +13,9 @@ import { UiTreePanel } from './UiTreePanel';
|
||||
|
||||
const SUPER_ROOT_ID = '__ui-editor-super-root__';
|
||||
|
||||
export function InputSidebar({
|
||||
controller,
|
||||
}: {
|
||||
controller: UiEditorPageController;
|
||||
}) {
|
||||
export function InputSidebar({ input }: { input: UiEditorInputProjection }) {
|
||||
const {
|
||||
projectPath,
|
||||
editor,
|
||||
activeStep,
|
||||
imageOrder,
|
||||
activeImageId,
|
||||
@@ -34,20 +29,22 @@ export function InputSidebar({
|
||||
spriteReferenceCounts,
|
||||
selectedNodeId,
|
||||
focusRequest,
|
||||
} = controller;
|
||||
uiTrees,
|
||||
isLocked,
|
||||
} = input;
|
||||
|
||||
const treeIdByNodeId = useMemo(() => {
|
||||
const result = new Map<NodeId, UIDesignImageId>();
|
||||
for (const tree of editor.state.ui_trees) {
|
||||
for (const tree of uiTrees) {
|
||||
visitUiNodes(tree.root, (node) =>
|
||||
result.set(node.id, tree.src_ui_design),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [editor.state.ui_trees]);
|
||||
}, [uiTrees]);
|
||||
|
||||
const superRoot = useMemo<UiNode | null>(() => {
|
||||
if (editor.state.ui_trees.length === 0) return null;
|
||||
if (uiTrees.length === 0) return null;
|
||||
return {
|
||||
id: SUPER_ROOT_ID,
|
||||
layout: {
|
||||
@@ -74,9 +71,9 @@ export function InputSidebar({
|
||||
},
|
||||
components: [],
|
||||
children_display_mode: 'Stack',
|
||||
children: editor.state.ui_trees.map((tree) => tree.root),
|
||||
children: uiTrees.map((tree) => tree.root),
|
||||
};
|
||||
}, [editor.state.ui_trees]);
|
||||
}, [uiTrees]);
|
||||
|
||||
return (
|
||||
<aside className="h-full min-h-0 min-w-0 overflow-x-hidden overflow-y-auto border-r border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-3">
|
||||
@@ -92,8 +89,8 @@ export function InputSidebar({
|
||||
type="button"
|
||||
className="grid size-8 place-items-center rounded-lg border border-orange-200 bg-orange-50 text-orange-800 disabled:opacity-40"
|
||||
aria-label="导入界面图"
|
||||
disabled={!projectPath || imageOrder.length >= 4 || editor.isLocked}
|
||||
onClick={() => controller.openImporter('design-image')}
|
||||
disabled={!projectPath || imageOrder.length >= 4 || isLocked}
|
||||
onClick={() => input.openImporter('design-image')}
|
||||
>
|
||||
<Plus size={15} />
|
||||
</button>
|
||||
@@ -112,7 +109,7 @@ export function InputSidebar({
|
||||
? 'border-orange-300 bg-orange-50'
|
||||
: 'border-(--platform-subpanel-border) bg-white/45'
|
||||
}`}
|
||||
onClick={() => controller.selectDesignImage(id)}
|
||||
onClick={() => input.selectDesignImage(id)}
|
||||
>
|
||||
<div className="grid size-10 shrink-0 place-items-center overflow-hidden rounded-md bg-black/5">
|
||||
{previewUrls[id] ? (
|
||||
@@ -157,8 +154,8 @@ export function InputSidebar({
|
||||
type="button"
|
||||
className="grid size-8 place-items-center rounded-lg border border-orange-200 bg-orange-50 text-orange-800 disabled:opacity-40"
|
||||
aria-label="导入独立素材"
|
||||
disabled={!projectPath || editor.isLocked}
|
||||
onClick={() => controller.openImporter('sprite')}
|
||||
disabled={!projectPath || isLocked}
|
||||
onClick={() => input.openImporter('sprite')}
|
||||
>
|
||||
<Plus size={15} />
|
||||
</button>
|
||||
@@ -170,7 +167,7 @@ export function InputSidebar({
|
||||
type="button"
|
||||
className="relative aspect-square overflow-hidden rounded-lg border border-(--platform-subpanel-border) bg-[linear-gradient(45deg,#eee_25%,transparent_25%),linear-gradient(-45deg,#eee_25%,transparent_25%),linear-gradient(45deg,transparent_75%,#eee_75%),linear-gradient(-45deg,transparent_75%,#eee_75%)] bg-[length:14px_14px]"
|
||||
title={`${sprite.metadata.name} · 引用 ${spriteReferenceCounts[sprite.asset_id] ?? 0}`}
|
||||
onClick={() => controller.selectSprite(sprite.asset_id)}
|
||||
onClick={() => input.selectSprite(sprite.asset_id)}
|
||||
>
|
||||
{previewUrls[sprite.asset_id] ? (
|
||||
<img
|
||||
@@ -201,8 +198,8 @@ export function InputSidebar({
|
||||
type="button"
|
||||
className="grid size-8 place-items-center rounded-lg border border-orange-200 bg-orange-50 text-orange-800 disabled:opacity-40"
|
||||
aria-label="导入字体"
|
||||
disabled={!projectPath || editor.isLocked}
|
||||
onClick={() => controller.openImporter('font')}
|
||||
disabled={!projectPath || isLocked}
|
||||
onClick={() => input.openImporter('font')}
|
||||
>
|
||||
<Plus size={15} />
|
||||
</button>
|
||||
@@ -220,7 +217,7 @@ export function InputSidebar({
|
||||
: 'border-(--platform-subpanel-border) bg-white/45'
|
||||
}`}
|
||||
title={`${font.metadata.source_file_name} · 引用 ${fontReferenceCounts[font.asset_id] ?? 0}`}
|
||||
onClick={() => controller.selectFont(font.asset_id)}
|
||||
onClick={() => input.selectFont(font.asset_id)}
|
||||
>
|
||||
<span className="grid size-9 shrink-0 place-items-center rounded-md bg-black/5">
|
||||
<Type size={16} />
|
||||
@@ -247,27 +244,23 @@ export function InputSidebar({
|
||||
|
||||
{activeStep === 'structure-recognition' ? (
|
||||
<RecognitionOverview
|
||||
uiTrees={editor.state.ui_trees}
|
||||
uiTrees={uiTrees}
|
||||
onFocusStatusNode={(treeId, nodeId) => {
|
||||
controller.focusNode(treeId, nodeId);
|
||||
controller.highlightStatusField('layout_status');
|
||||
input.focusNode(treeId, nodeId);
|
||||
input.highlightStatusField('layout_status');
|
||||
}}
|
||||
/>
|
||||
) : activeStep === 'visual-binding' ? (
|
||||
<BindingOverview
|
||||
uiTrees={editor.state.ui_trees}
|
||||
uiTrees={uiTrees}
|
||||
sprites={sprites}
|
||||
onFocusStatusNode={(treeId, nodeId) => {
|
||||
controller.focusNode(treeId, nodeId);
|
||||
controller.highlightStatusField('components_status');
|
||||
input.focusNode(treeId, nodeId);
|
||||
input.highlightStatusField('components_status');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ImportOverview
|
||||
images={images}
|
||||
sprites={sprites}
|
||||
uiTrees={editor.state.ui_trees}
|
||||
/>
|
||||
<ImportOverview images={images} sprites={sprites} uiTrees={uiTrees} />
|
||||
)}
|
||||
|
||||
<UiTreePanel
|
||||
@@ -275,25 +268,25 @@ export function InputSidebar({
|
||||
selectedNodeId={selectedNodeId}
|
||||
focusRequest={focusRequest}
|
||||
treeIdForNode={(nodeId) => treeIdByNodeId.get(nodeId) ?? null}
|
||||
isNodePreviewVisible={controller.isNodePreviewVisible}
|
||||
isNodePreviewVisible={input.isNodePreviewVisible}
|
||||
onSelectNode={(treeId, nodeId) => {
|
||||
controller.selectDesignImage(treeId);
|
||||
controller.selectNode(nodeId);
|
||||
input.selectDesignImage(treeId);
|
||||
input.selectNode(nodeId);
|
||||
}}
|
||||
onToggleNodeVisibility={(nodeId) =>
|
||||
controller.toggleNodePreviewVisibility(nodeId)
|
||||
input.toggleNodePreviewVisibility(nodeId)
|
||||
}
|
||||
onInsertNode={(treeId, parentId) => {
|
||||
controller.insertNode(parentId, treeId);
|
||||
input.insertNode(parentId, treeId);
|
||||
}}
|
||||
onInsertNodeAfter={(treeId, nodeId) => {
|
||||
controller.insertNodeAfter(nodeId, treeId);
|
||||
input.insertNodeAfter(nodeId, treeId);
|
||||
}}
|
||||
onDeleteNode={(treeId, nodeId) => {
|
||||
controller.deleteNode(nodeId, treeId);
|
||||
input.deleteNode(nodeId, treeId);
|
||||
}}
|
||||
onMoveNode={(request) => controller.moveNode(request)}
|
||||
isLocked={controller.editor.isLocked}
|
||||
onMoveNode={input.moveNode}
|
||||
isLocked={isLocked}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
|
||||
+82
-92
@@ -13,7 +13,7 @@ import type { UIDesignImageId } from '../../../../features/ui-editor/types/UIDes
|
||||
import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UIDesignImageRole';
|
||||
import { uiEditorPrivateFontFamily } from '../../../../features/ui-editor/useUiEditorFontFaces';
|
||||
import { UI_DESIGN_IMAGE_ROLES } from '../../model';
|
||||
import type { UiEditorPageController } from '../../useUiEditorPage';
|
||||
import type { UiEditorInspectorProjection } from '../../useUiEditorPage';
|
||||
import { ComponentPanel } from './Components/ComponentPanel';
|
||||
import {
|
||||
InspectorReadOnlyContext,
|
||||
@@ -22,19 +22,25 @@ import {
|
||||
import { SpriteBorderEditor } from './SpriteBorder/SpriteBorderEditor';
|
||||
import TransformEditor from './Transform/TransformEditor';
|
||||
|
||||
type SelectedNode = NonNullable<UiEditorPageController['selectedNode']>;
|
||||
type SelectedSprite = NonNullable<UiEditorPageController['selectedSprite']>;
|
||||
type SelectedFont = NonNullable<UiEditorPageController['selectedFont']>;
|
||||
type ActiveImage = NonNullable<UiEditorPageController['activeImage']>;
|
||||
type SelectedSpriteId = NonNullable<UiEditorPageController['selectedSpriteId']>;
|
||||
type SelectedFontId = NonNullable<UiEditorPageController['selectedFontId']>;
|
||||
type ActiveImageId = NonNullable<UiEditorPageController['activeImageId']>;
|
||||
type SelectedNode = NonNullable<UiEditorInspectorProjection['selectedNode']>;
|
||||
type SelectedSprite = NonNullable<
|
||||
UiEditorInspectorProjection['selectedSprite']
|
||||
>;
|
||||
type SelectedFont = NonNullable<UiEditorInspectorProjection['selectedFont']>;
|
||||
type ActiveImage = NonNullable<UiEditorInspectorProjection['activeImage']>;
|
||||
type SelectedSpriteId = NonNullable<
|
||||
UiEditorInspectorProjection['selectedSpriteId']
|
||||
>;
|
||||
type SelectedFontId = NonNullable<
|
||||
UiEditorInspectorProjection['selectedFontId']
|
||||
>;
|
||||
type ActiveImageId = NonNullable<UiEditorInspectorProjection['activeImageId']>;
|
||||
|
||||
type InspectorView =
|
||||
| {
|
||||
kind: 'node';
|
||||
node: SelectedNode;
|
||||
parentSize: UiEditorPageController['selectedNodeParentSize'];
|
||||
parentSize: UiEditorInspectorProjection['selectedNodeParentSize'];
|
||||
parentIsContainer: boolean;
|
||||
}
|
||||
| {
|
||||
@@ -55,7 +61,7 @@ type InspectorView =
|
||||
kind: 'image';
|
||||
image: ActiveImage;
|
||||
imageId: ActiveImageId;
|
||||
pageOptions: UiEditorPageController['pageOptions'];
|
||||
pageOptions: UiEditorInspectorProjection['pageOptions'];
|
||||
}
|
||||
| { kind: 'empty' };
|
||||
|
||||
@@ -73,11 +79,11 @@ const INSPECTOR_INPUT_CLASS_NAME = `${INSPECTOR_CONTROL_BASE_CLASS_NAME} h-9 px-
|
||||
const INSPECTOR_TEXTAREA_CLASS_NAME = `${INSPECTOR_CONTROL_BASE_CLASS_NAME} min-h-20 resize-y px-2 py-2 leading-5`;
|
||||
|
||||
export function InspectorSidebar({
|
||||
controller,
|
||||
inspector,
|
||||
}: {
|
||||
controller: UiEditorPageController;
|
||||
inspector: UiEditorInspectorProjection;
|
||||
}) {
|
||||
const view = getInspectorView(controller);
|
||||
const view = getInspectorView(inspector);
|
||||
let inspectorContent: ReactNode;
|
||||
switch (view.kind) {
|
||||
case 'node':
|
||||
@@ -86,34 +92,32 @@ export function InspectorSidebar({
|
||||
node={view.node}
|
||||
parentSize={view.parentSize}
|
||||
parentIsContainer={view.parentIsContainer}
|
||||
readOnly={controller.editor.isLocked}
|
||||
isNodePreviewVisible={controller.isNodePreviewVisible(view.node.id)}
|
||||
readOnly={inspector.isLocked}
|
||||
isNodePreviewVisible={inspector.isNodePreviewVisible(view.node.id)}
|
||||
onSetNodePreviewVisible={(visible) =>
|
||||
controller.setNodePreviewVisible(view.node.id, visible)
|
||||
inspector.setNodePreviewVisible(view.node.id, visible)
|
||||
}
|
||||
onChildrenDisplayModeChange={controller.setNodeChildrenDisplayMode}
|
||||
onChildrenDisplayModeChange={inspector.setNodeChildrenDisplayMode}
|
||||
transformReadOnly={
|
||||
controller.editor.isLocked ||
|
||||
view.node.id === controller.treeForActiveImage?.root.id
|
||||
inspector.isLocked || view.node.id === inspector.tree?.root.id
|
||||
}
|
||||
keepChildrenUnchanged={controller.keepChildrenUnchanged}
|
||||
onKeepChildrenUnchangedChange={controller.setKeepChildrenUnchanged}
|
||||
onMetadataChange={controller.setNodeMetadata}
|
||||
highlightedStatusField={controller.highlightedStatusField}
|
||||
onTransformChange={controller.setNodeTransform}
|
||||
onLayoutChange={controller.setNodeLayout}
|
||||
sprites={controller.sprites}
|
||||
fonts={controller.editor.state.font_assets}
|
||||
fontFaces={controller.fontFaces}
|
||||
projectPath={controller.projectPath}
|
||||
onSetComponents={controller.setNodeComponents}
|
||||
onInsertComponent={controller.insertNodeComponent}
|
||||
onDeleteComponent={controller.deleteNodeComponent}
|
||||
onMoveComponent={controller.moveNodeComponent}
|
||||
onDeleteNode={() => controller.deleteNode(view.node.id)}
|
||||
keepChildrenUnchanged={inspector.keepChildrenUnchanged}
|
||||
onKeepChildrenUnchangedChange={inspector.setKeepChildrenUnchanged}
|
||||
onMetadataChange={inspector.setNodeMetadata}
|
||||
highlightedStatusField={inspector.highlightedStatusField}
|
||||
onTransformChange={inspector.setNodeTransform}
|
||||
onLayoutChange={inspector.setNodeLayout}
|
||||
sprites={inspector.sprites}
|
||||
fonts={inspector.fonts}
|
||||
fontFaces={inspector.fontFaces}
|
||||
projectPath={inspector.projectPath}
|
||||
onSetComponents={inspector.setNodeComponents}
|
||||
onInsertComponent={inspector.insertNodeComponent}
|
||||
onDeleteComponent={inspector.deleteNodeComponent}
|
||||
onMoveComponent={inspector.moveNodeComponent}
|
||||
onDeleteNode={() => inspector.deleteNode(view.node.id)}
|
||||
deleteDisabled={
|
||||
controller.editor.isLocked ||
|
||||
view.node.id === controller.treeForActiveImage?.root.id
|
||||
inspector.isLocked || view.node.id === inspector.tree?.root.id
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -125,11 +129,11 @@ export function InspectorSidebar({
|
||||
spriteId={view.spriteId}
|
||||
previewUrl={view.previewUrl}
|
||||
referenceCount={view.referenceCount}
|
||||
onNameChange={controller.setSpriteName}
|
||||
onAssetTypeChange={controller.setSpriteAssetType}
|
||||
onBorderChange={controller.setSpriteBorder}
|
||||
onDelete={() => controller.requestSpriteRemoval(view.spriteId)}
|
||||
deleteDisabled={controller.editor.isLocked}
|
||||
onNameChange={inspector.setSpriteName}
|
||||
onAssetTypeChange={inspector.setSpriteAssetType}
|
||||
onBorderChange={inspector.setSpriteBorder}
|
||||
onDelete={() => inspector.requestSpriteRemoval(view.spriteId)}
|
||||
deleteDisabled={inspector.isLocked}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -140,8 +144,8 @@ export function InspectorSidebar({
|
||||
fontId={view.fontId}
|
||||
referenceCount={view.referenceCount}
|
||||
loadStatus={view.loadStatus}
|
||||
onDelete={() => controller.requestFontRemoval(view.fontId)}
|
||||
deleteDisabled={controller.editor.isLocked}
|
||||
onDelete={() => inspector.requestFontRemoval(view.fontId)}
|
||||
deleteDisabled={inspector.isLocked}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -151,12 +155,12 @@ export function InspectorSidebar({
|
||||
image={view.image}
|
||||
imageId={view.imageId}
|
||||
pageOptions={view.pageOptions}
|
||||
onNameChange={controller.setImageName}
|
||||
onDescriptionChange={controller.setImageDescription}
|
||||
onRoleChange={controller.setImageRole}
|
||||
onSlaveToChange={controller.setImageSlaveTo}
|
||||
onDelete={() => controller.requestDesignImageRemoval(view.imageId)}
|
||||
deleteDisabled={controller.editor.isLocked}
|
||||
onNameChange={inspector.setImageName}
|
||||
onDescriptionChange={inspector.setImageDescription}
|
||||
onRoleChange={inspector.setImageRole}
|
||||
onSlaveToChange={inspector.setImageSlaveTo}
|
||||
onDelete={() => inspector.requestDesignImageRemoval(view.imageId)}
|
||||
deleteDisabled={inspector.isLocked}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -170,11 +174,11 @@ export function InspectorSidebar({
|
||||
}
|
||||
|
||||
return (
|
||||
<InspectorReadOnlyContext.Provider value={controller.editor.isLocked}>
|
||||
<InspectorReadOnlyContext.Provider value={inspector.isLocked}>
|
||||
<aside
|
||||
aria-readonly={controller.editor.isLocked}
|
||||
aria-readonly={inspector.isLocked}
|
||||
className={`flex h-full min-h-0 min-w-0 flex-col overflow-x-hidden overflow-y-auto border-l p-4 ${
|
||||
controller.editor.isLocked
|
||||
inspector.isLocked
|
||||
? 'border-slate-300 bg-slate-100/90'
|
||||
: 'border-(--platform-subpanel-border) bg-(--platform-subpanel-fill)'
|
||||
}`}
|
||||
@@ -186,7 +190,7 @@ export function InspectorSidebar({
|
||||
{INSPECTOR_VIEW_TITLES[view.kind]}
|
||||
</h2>
|
||||
</div>
|
||||
{controller.editor.isLocked ? (
|
||||
{inspector.isLocked ? (
|
||||
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-slate-300 bg-slate-200 px-2 py-1 text-[10px] font-semibold text-slate-600">
|
||||
<LockKeyhole size={12} aria-hidden="true" />
|
||||
只读
|
||||
@@ -204,7 +208,9 @@ export function InspectorSidebar({
|
||||
);
|
||||
}
|
||||
|
||||
function getInspectorView(controller: UiEditorPageController): InspectorView {
|
||||
function getInspectorView(
|
||||
inspector: UiEditorInspectorProjection,
|
||||
): InspectorView {
|
||||
const {
|
||||
activeImage,
|
||||
activeImageId,
|
||||
@@ -220,7 +226,7 @@ function getInspectorView(controller: UiEditorPageController): InspectorView {
|
||||
fontReferenceCounts,
|
||||
fontFaces,
|
||||
pageOptions,
|
||||
} = controller;
|
||||
} = inspector;
|
||||
|
||||
if (selectedNode) {
|
||||
return {
|
||||
@@ -290,30 +296,30 @@ function NodeInspector({
|
||||
deleteDisabled,
|
||||
}: {
|
||||
node: SelectedNode;
|
||||
parentSize: UiEditorPageController['selectedNodeParentSize'];
|
||||
parentSize: UiEditorInspectorProjection['selectedNodeParentSize'];
|
||||
parentIsContainer: boolean;
|
||||
readOnly: boolean;
|
||||
isNodePreviewVisible: boolean;
|
||||
onSetNodePreviewVisible: (visible: boolean) => void;
|
||||
onChildrenDisplayModeChange: UiEditorPageController['setNodeChildrenDisplayMode'];
|
||||
onChildrenDisplayModeChange: UiEditorInspectorProjection['setNodeChildrenDisplayMode'];
|
||||
transformReadOnly: boolean;
|
||||
keepChildrenUnchanged: boolean;
|
||||
onKeepChildrenUnchangedChange: (value: boolean) => void;
|
||||
onMetadataChange: UiEditorPageController['setNodeMetadata'];
|
||||
onMetadataChange: UiEditorInspectorProjection['setNodeMetadata'];
|
||||
highlightedStatusField: {
|
||||
field: StageStatusField;
|
||||
requestId: number;
|
||||
} | null;
|
||||
onTransformChange: UiEditorPageController['setNodeTransform'];
|
||||
onLayoutChange: UiEditorPageController['setNodeLayout'];
|
||||
sprites: UiEditorPageController['sprites'];
|
||||
fonts: UiEditorPageController['editor']['state']['font_assets'];
|
||||
fontFaces: UiEditorPageController['fontFaces'];
|
||||
onTransformChange: UiEditorInspectorProjection['setNodeTransform'];
|
||||
onLayoutChange: UiEditorInspectorProjection['setNodeLayout'];
|
||||
sprites: UiEditorInspectorProjection['sprites'];
|
||||
fonts: UiEditorInspectorProjection['fonts'];
|
||||
fontFaces: UiEditorInspectorProjection['fontFaces'];
|
||||
projectPath: string;
|
||||
onSetComponents: UiEditorPageController['setNodeComponents'];
|
||||
onInsertComponent: UiEditorPageController['insertNodeComponent'];
|
||||
onDeleteComponent: UiEditorPageController['deleteNodeComponent'];
|
||||
onMoveComponent: UiEditorPageController['moveNodeComponent'];
|
||||
onSetComponents: UiEditorInspectorProjection['setNodeComponents'];
|
||||
onInsertComponent: UiEditorInspectorProjection['insertNodeComponent'];
|
||||
onDeleteComponent: UiEditorInspectorProjection['deleteNodeComponent'];
|
||||
onMoveComponent: UiEditorInspectorProjection['moveNodeComponent'];
|
||||
onDeleteNode: () => void;
|
||||
deleteDisabled: boolean;
|
||||
}) {
|
||||
@@ -511,7 +517,7 @@ function LayoutEditor({
|
||||
}: {
|
||||
node: SelectedNode;
|
||||
readOnly: boolean;
|
||||
onChange: UiEditorPageController['setNodeLayout'];
|
||||
onChange: UiEditorInspectorProjection['setNodeLayout'];
|
||||
}) {
|
||||
const layout = node.layout;
|
||||
const containerKind =
|
||||
@@ -852,22 +858,6 @@ function NodeStageSelect({
|
||||
);
|
||||
}
|
||||
|
||||
function stageStatusFromSelect(
|
||||
value: string,
|
||||
): SelectedNode['metadata']['layout_status'] {
|
||||
switch (value) {
|
||||
case 'NeedReview':
|
||||
return { NeedReview: '待审' };
|
||||
case 'Blocked':
|
||||
return 'Blocked';
|
||||
case 'Pending':
|
||||
return 'Pending';
|
||||
case 'Passed':
|
||||
default:
|
||||
return 'Passed';
|
||||
}
|
||||
}
|
||||
|
||||
function SpriteInspector({
|
||||
sprite,
|
||||
spriteId,
|
||||
@@ -883,9 +873,9 @@ function SpriteInspector({
|
||||
spriteId: SelectedSpriteId;
|
||||
previewUrl: string | undefined;
|
||||
referenceCount: number;
|
||||
onNameChange: UiEditorPageController['setSpriteName'];
|
||||
onAssetTypeChange: UiEditorPageController['setSpriteAssetType'];
|
||||
onBorderChange: UiEditorPageController['setSpriteBorder'];
|
||||
onNameChange: UiEditorInspectorProjection['setSpriteName'];
|
||||
onAssetTypeChange: UiEditorInspectorProjection['setSpriteAssetType'];
|
||||
onBorderChange: UiEditorInspectorProjection['setSpriteBorder'];
|
||||
onDelete: () => void;
|
||||
deleteDisabled: boolean;
|
||||
}) {
|
||||
@@ -1023,11 +1013,11 @@ function ImageInspector({
|
||||
}: {
|
||||
image: ActiveImage;
|
||||
imageId: ActiveImageId;
|
||||
pageOptions: UiEditorPageController['pageOptions'];
|
||||
onNameChange: UiEditorPageController['setImageName'];
|
||||
onDescriptionChange: UiEditorPageController['setImageDescription'];
|
||||
onRoleChange: UiEditorPageController['setImageRole'];
|
||||
onSlaveToChange: UiEditorPageController['setImageSlaveTo'];
|
||||
pageOptions: UiEditorInspectorProjection['pageOptions'];
|
||||
onNameChange: UiEditorInspectorProjection['setImageName'];
|
||||
onDescriptionChange: UiEditorInspectorProjection['setImageDescription'];
|
||||
onRoleChange: UiEditorInspectorProjection['setImageRole'];
|
||||
onSlaveToChange: UiEditorInspectorProjection['setImageSlaveTo'];
|
||||
onDelete: () => void;
|
||||
deleteDisabled: boolean;
|
||||
}) {
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import type { UiEditorPageController } from '../useUiEditorPage';
|
||||
import type { UiEditorWorkflowProjection } from '../useUiEditorPage';
|
||||
|
||||
export function WorkflowActionCard({
|
||||
controller,
|
||||
workflow,
|
||||
}: {
|
||||
controller: UiEditorPageController;
|
||||
workflow: UiEditorWorkflowProjection;
|
||||
}) {
|
||||
const action = getStepAction(controller);
|
||||
const action = getStepAction(workflow);
|
||||
const running =
|
||||
(controller.activeStep === 'reference-analysis' &&
|
||||
controller.isSuggesting) ||
|
||||
(controller.activeStep === 'structure-recognition' &&
|
||||
controller.isRecognizing) ||
|
||||
(controller.activeStep === 'visual-binding' && controller.isBinding);
|
||||
(workflow.activeStep === 'reference-analysis' && workflow.isSuggesting) ||
|
||||
(workflow.activeStep === 'structure-recognition' &&
|
||||
workflow.isRecognizing) ||
|
||||
(workflow.activeStep === 'visual-binding' && workflow.isBinding);
|
||||
const status =
|
||||
controller.activeStep === 'reference-analysis'
|
||||
? controller.suggestionStatus
|
||||
: controller.activeStep === 'structure-recognition'
|
||||
? controller.recognitionStatus
|
||||
: controller.bindingStatus;
|
||||
workflow.activeStep === 'reference-analysis'
|
||||
? workflow.suggestionStatus
|
||||
: workflow.activeStep === 'structure-recognition'
|
||||
? workflow.recognitionStatus
|
||||
: workflow.bindingStatus;
|
||||
const hasRun =
|
||||
controller.activeStep === 'reference-analysis'
|
||||
? controller.hasSuggested
|
||||
: controller.activeStep === 'structure-recognition'
|
||||
? controller.hasRecognized
|
||||
: controller.hasBound;
|
||||
workflow.activeStep === 'reference-analysis'
|
||||
? workflow.hasSuggested
|
||||
: workflow.activeStep === 'structure-recognition'
|
||||
? workflow.hasRecognized
|
||||
: workflow.hasBound;
|
||||
|
||||
return (
|
||||
<section className="shrink-0 border-b border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) px-4 py-3">
|
||||
@@ -40,17 +39,17 @@ export function WorkflowActionCard({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-orange-600 px-3 py-1.5 text-xs font-semibold text-white disabled:cursor-wait disabled:opacity-60"
|
||||
disabled={controller.isAiRunning}
|
||||
disabled={workflow.isAiRunning}
|
||||
onClick={() => void action.action()}
|
||||
>
|
||||
{running ? action.runningLabel : hasRun ? '重新运行' : action.label}
|
||||
</button>
|
||||
{controller.nextStep ? (
|
||||
{workflow.nextStep ? (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-1.5 text-xs disabled:cursor-wait disabled:opacity-60"
|
||||
disabled={controller.isAiRunning}
|
||||
onClick={controller.continueToNextStep}
|
||||
disabled={workflow.isAiRunning}
|
||||
onClick={workflow.continueToNextStep}
|
||||
>
|
||||
继续
|
||||
</button>
|
||||
@@ -61,24 +60,24 @@ export function WorkflowActionCard({
|
||||
);
|
||||
}
|
||||
|
||||
function getStepAction(controller: UiEditorPageController) {
|
||||
if (controller.activeStep === 'reference-analysis') {
|
||||
function getStepAction(workflow: UiEditorWorkflowProjection) {
|
||||
if (workflow.activeStep === 'reference-analysis') {
|
||||
return {
|
||||
label: '分析参考图',
|
||||
runningLabel: '分析中…',
|
||||
action: controller.suggestUiDesignSemantics,
|
||||
action: workflow.suggestUiDesignSemantics,
|
||||
};
|
||||
}
|
||||
if (controller.activeStep === 'structure-recognition') {
|
||||
if (workflow.activeStep === 'structure-recognition') {
|
||||
return {
|
||||
label: '识别界面结构',
|
||||
runningLabel: '识别中…',
|
||||
action: controller.recognizeUi,
|
||||
action: workflow.recognizeUi,
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: '绑定视觉素材',
|
||||
runningLabel: '绑定中…',
|
||||
action: controller.bindComponents,
|
||||
action: workflow.bindComponents,
|
||||
};
|
||||
}
|
||||
|
||||
+29
-31
@@ -25,18 +25,18 @@ import {
|
||||
|
||||
import { findNodePageContext } from '../../../../features/ui-editor/nodeTransformGeometry';
|
||||
import type { NodeId } from '../../../../features/ui-editor/types/NodeId';
|
||||
import type { UiEditorPageController } from '../../useUiEditorPage';
|
||||
import type { UiEditorCanvasProjection } from '../../useUiEditorPage';
|
||||
import { UiNodeContextMenu } from '../UiNodeContextMenu';
|
||||
import { ExclusiveChildrenTabs } from './ExclusiveChildrenTabs';
|
||||
import { type UiEditorRenderMode, UiTreeRenderer } from './UiTreeRenderer';
|
||||
import { useNodeTransformInteraction } from './useNodeTransformInteraction';
|
||||
|
||||
export function PreviewWorkspace({
|
||||
controller,
|
||||
canvas,
|
||||
}: {
|
||||
controller: UiEditorPageController;
|
||||
canvas: UiEditorCanvasProjection;
|
||||
}) {
|
||||
const { editor, activeImage, activeImageId, previewUrls } = controller;
|
||||
const { activeImage, activeImageId, previewUrls } = canvas;
|
||||
const viewportElementRef = useRef<HTMLDivElement>(null);
|
||||
const viewportRef = useRef<CanvasViewport>({ x: 0, y: 0, scale: 0.5 });
|
||||
const handledFocusRequestIdRef = useRef<number | null>(null);
|
||||
@@ -55,7 +55,7 @@ export function PreviewWorkspace({
|
||||
y: number;
|
||||
isPageRoot: boolean;
|
||||
} | null>(null);
|
||||
const tree = controller.treeForActiveImage ?? null;
|
||||
const tree = canvas.tree ?? null;
|
||||
const activeImagePixelWidth = activeImage?.pixel_size[0];
|
||||
const activeImagePixelHeight = activeImage?.pixel_size[1];
|
||||
const activeImagePixelsPerUnit = activeImage?.pixels_per_unit;
|
||||
@@ -84,7 +84,7 @@ export function PreviewWorkspace({
|
||||
|
||||
const nodeInteractions = useNodeTransformInteraction({
|
||||
activeImageId,
|
||||
controller,
|
||||
canvas,
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
@@ -146,7 +146,7 @@ export function PreviewWorkspace({
|
||||
}, [activeImageId, fitToCanvas]);
|
||||
|
||||
useEffect(() => {
|
||||
const request = controller.focusRequest;
|
||||
const request = canvas.focusRequest;
|
||||
if (
|
||||
!request ||
|
||||
request.requestId === handledFocusRequestIdRef.current ||
|
||||
@@ -182,7 +182,7 @@ export function PreviewWorkspace({
|
||||
activeImageId,
|
||||
canvasSize.height,
|
||||
canvasSize.width,
|
||||
controller.focusRequest,
|
||||
canvas.focusRequest,
|
||||
logicalSize,
|
||||
setViewport,
|
||||
tree,
|
||||
@@ -250,7 +250,7 @@ export function PreviewWorkspace({
|
||||
return;
|
||||
}
|
||||
if (event.button === 0 && event.target === event.currentTarget) {
|
||||
controller.clearNodeSelection();
|
||||
canvas.clearNodeSelection();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -341,8 +341,8 @@ export function PreviewWorkspace({
|
||||
</label>
|
||||
) : null}
|
||||
<span className="text-[10px] text-(--platform-text-soft)">
|
||||
{Object.keys(controller.images).length} 张界面图 ·{' '}
|
||||
{Object.keys(controller.sprites).length} 项素材
|
||||
{Object.keys(canvas.images).length} 张界面图 ·{' '}
|
||||
{Object.keys(canvas.sprites).length} 项素材
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -350,14 +350,12 @@ export function PreviewWorkspace({
|
||||
logicalSize &&
|
||||
(renderMode === 'final-preview' || previewUrls[activeImageId ?? '']) ? (
|
||||
<div className="relative min-h-0 flex-1">
|
||||
{renderMode === 'final-preview' && controller.selectedNode ? (
|
||||
{renderMode === 'final-preview' && canvas.selectedNode ? (
|
||||
<ExclusiveChildrenTabs
|
||||
parent={controller.selectedNode}
|
||||
isChildVisible={(nodeId) =>
|
||||
controller.isNodePreviewVisible(nodeId)
|
||||
}
|
||||
parent={canvas.selectedNode}
|
||||
isChildVisible={(nodeId) => canvas.isNodePreviewVisible(nodeId)}
|
||||
onToggleChild={(nodeId) =>
|
||||
controller.toggleNodePreviewVisibility(nodeId)
|
||||
canvas.toggleNodePreviewVisibility(nodeId)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
@@ -371,7 +369,7 @@ export function PreviewWorkspace({
|
||||
onWheel={handleWheel}
|
||||
onClick={(event) => {
|
||||
if (!(event.target as Element).closest('[data-node-id]')) {
|
||||
controller.clearNodeSelection();
|
||||
canvas.clearNodeSelection();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -400,14 +398,14 @@ export function PreviewWorkspace({
|
||||
tree={tree}
|
||||
renderMode={renderMode}
|
||||
showFrame={showFrame}
|
||||
hiddenNodeIds={controller.hiddenNodeIds}
|
||||
selectedNodeId={controller.selectedNodeId}
|
||||
hiddenNodeIds={canvas.hiddenNodeIds}
|
||||
selectedNodeId={canvas.selectedNodeId}
|
||||
resources={{
|
||||
previewUrls,
|
||||
sprites: controller.sprites,
|
||||
fontFaces: controller.fontFaces,
|
||||
sprites: canvas.sprites,
|
||||
fontFaces: canvas.fontFaces,
|
||||
}}
|
||||
onSelectNode={controller.selectNode}
|
||||
onSelectNode={canvas.selectNode}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
onNodePointerDown={onNodePointerDown}
|
||||
onNodePointerMove={onNodePointerMove}
|
||||
@@ -425,11 +423,11 @@ export function PreviewWorkspace({
|
||||
nodeId={contextMenu.nodeId}
|
||||
position={contextMenu}
|
||||
isPageRoot={contextMenu.isPageRoot}
|
||||
disabled={editor.isLocked}
|
||||
disabled={canvas.isLocked}
|
||||
onClose={() => setContextMenu(null)}
|
||||
onInsertChild={(nodeId) => controller.insertNode(nodeId)}
|
||||
onInsertSibling={(nodeId) => controller.insertNodeAfter(nodeId)}
|
||||
onDelete={(nodeId) => controller.deleteNode(nodeId)}
|
||||
onInsertChild={(nodeId) => canvas.insertNode(nodeId)}
|
||||
onInsertSibling={(nodeId) => canvas.insertNodeAfter(nodeId)}
|
||||
onDelete={(nodeId) => canvas.deleteNode(nodeId)}
|
||||
/>
|
||||
) : null}
|
||||
<ZoomControls
|
||||
@@ -493,9 +491,9 @@ export function PreviewWorkspace({
|
||||
</div>
|
||||
)}
|
||||
<footer className="flex min-h-14 shrink-0 items-center gap-2 border-t border-(--platform-subpanel-border) px-4 py-2">
|
||||
{controller.status ? (
|
||||
{canvas.status ? (
|
||||
<span className="mr-auto text-xs text-(--platform-text-soft)">
|
||||
{controller.status}
|
||||
{canvas.status}
|
||||
</span>
|
||||
) : (
|
||||
<span className="mr-auto" />
|
||||
@@ -503,8 +501,8 @@ export function PreviewWorkspace({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-xs disabled:opacity-40"
|
||||
disabled={editor.isLocked}
|
||||
onClick={controller.openClearDialog}
|
||||
disabled={canvas.isLocked}
|
||||
onClick={canvas.openClearDialog}
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
|
||||
+13
-21
@@ -17,9 +17,9 @@ import {
|
||||
} from '../../../../features/ui-editor/nodeTransformGeometry';
|
||||
import type { Node as UiNode } from '../../../../features/ui-editor/types/Node';
|
||||
import type { UITree } from '../../../../features/ui-editor/types/UITree';
|
||||
import type { UiEditorPageController } from '../../useUiEditorPage';
|
||||
type ViewportScale = { scale: number };
|
||||
import type { UiEditorCanvasProjection } from '../../useUiEditorPage';
|
||||
|
||||
type ViewportScale = { scale: number };
|
||||
type GestureBase = {
|
||||
treeId: string;
|
||||
nodeId: string;
|
||||
@@ -80,14 +80,14 @@ function releasePointer(target: HTMLDivElement, pointerId: number) {
|
||||
|
||||
export function useNodeTransformInteraction({
|
||||
activeImageId,
|
||||
controller,
|
||||
canvas,
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
viewportRef,
|
||||
}: {
|
||||
activeImageId: UiEditorPageController['activeImageId'];
|
||||
controller: UiEditorPageController;
|
||||
activeImageId: UiEditorCanvasProjection['activeImageId'];
|
||||
canvas: Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'>;
|
||||
logicalSize: { width: number; height: number } | null;
|
||||
spaceHeld: boolean;
|
||||
tree: UITree | null;
|
||||
@@ -140,7 +140,7 @@ export function useNodeTransformInteraction({
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
controller.selectNode(node.id);
|
||||
canvas.selectNode(node.id);
|
||||
activeGestureRef.current = {
|
||||
kind: 'drag',
|
||||
treeId: activeImageId,
|
||||
@@ -153,7 +153,7 @@ export function useNodeTransformInteraction({
|
||||
hasMoved: false,
|
||||
};
|
||||
},
|
||||
[activeImageId, controller, spaceHeld, tree?.root.id],
|
||||
[activeImageId, canvas, spaceHeld, tree?.root.id],
|
||||
);
|
||||
|
||||
const onNodePointerMove = useCallback(
|
||||
@@ -185,13 +185,9 @@ export function useNodeTransformInteraction({
|
||||
cancelGesture();
|
||||
return;
|
||||
}
|
||||
controller.updateNodeTransform(
|
||||
gesture.treeId,
|
||||
gesture.nodeId,
|
||||
nextTransform,
|
||||
);
|
||||
canvas.updateNodeTransform(gesture.treeId, gesture.nodeId, nextTransform);
|
||||
},
|
||||
[acceptsGestureEvent, cancelGesture, controller, viewportRef],
|
||||
[acceptsGestureEvent, cancelGesture, canvas, viewportRef],
|
||||
);
|
||||
|
||||
const onNodePointerUp = useCallback(
|
||||
@@ -240,7 +236,7 @@ export function useNodeTransformInteraction({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
controller.selectNode(node.id);
|
||||
canvas.selectNode(node.id);
|
||||
activeGestureRef.current = {
|
||||
kind: 'resize',
|
||||
treeId: activeImageId,
|
||||
@@ -257,7 +253,7 @@ export function useNodeTransformInteraction({
|
||||
hasMoved: false,
|
||||
};
|
||||
},
|
||||
[activeImageId, controller, logicalSize, spaceHeld, tree],
|
||||
[activeImageId, canvas, logicalSize, spaceHeld, tree],
|
||||
);
|
||||
|
||||
const onNodeResizePointerMove = useCallback(
|
||||
@@ -314,13 +310,9 @@ export function useNodeTransformInteraction({
|
||||
return;
|
||||
}
|
||||
gesture.hasMoved = true;
|
||||
controller.updateNodeTransform(
|
||||
gesture.treeId,
|
||||
gesture.nodeId,
|
||||
nextTransform,
|
||||
);
|
||||
canvas.updateNodeTransform(gesture.treeId, gesture.nodeId, nextTransform);
|
||||
},
|
||||
[acceptsGestureEvent, cancelGesture, controller, viewportRef],
|
||||
[acceptsGestureEvent, cancelGesture, canvas, viewportRef],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { ThemedModal } from '../../components/modal/ThemedModal';
|
||||
import {
|
||||
@@ -15,8 +15,8 @@ import { ToolNavigation } from './components/ToolNavigation';
|
||||
import { WorkflowActionCard } from './components/WorkflowActionCard';
|
||||
import { UI_EDITOR_STEPS } from './model';
|
||||
import {
|
||||
type UiEditorPageController,
|
||||
useUiEditorPage,
|
||||
type UiEditorWorkflowProjection,
|
||||
useUiEditorSession,
|
||||
} from './useUiEditorPage';
|
||||
|
||||
export default function UiEditorPage({
|
||||
@@ -42,80 +42,26 @@ export default function UiEditorPage({
|
||||
: mockUiDesignStateStore),
|
||||
[projectId, projectPath, resourceId, stateStore],
|
||||
);
|
||||
const controller = useUiEditorPage(projectPath, resourceId, resolvedStore);
|
||||
const [savedStateSignature, setSavedStateSignature] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const session = useUiEditorSession(projectPath, resourceId, resolvedStore);
|
||||
const [saveWarningOpen, setSaveWarningOpen] = useState(false);
|
||||
const [saveAfterReturn, setSaveAfterReturn] = useState(false);
|
||||
const [returnConfirmOpen, setReturnConfirmOpen] = useState(false);
|
||||
const loadedResourceIdRef = useRef<string | undefined>(undefined);
|
||||
const stateSignature = JSON.stringify(controller.editor.state);
|
||||
|
||||
useEffect(() => {
|
||||
if (!resourceId || controller.isLoading || controller.loadError) return;
|
||||
if (loadedResourceIdRef.current !== resourceId) {
|
||||
loadedResourceIdRef.current = resourceId;
|
||||
setSavedStateSignature(stateSignature);
|
||||
setSaveError(null);
|
||||
}
|
||||
}, [controller.isLoading, controller.loadError, resourceId, stateSignature]);
|
||||
|
||||
const isDirty =
|
||||
resourceId !== undefined &&
|
||||
savedStateSignature !== null &&
|
||||
savedStateSignature !== stateSignature;
|
||||
|
||||
function saveFailureMessage() {
|
||||
return '保存失败,请稍后重试。';
|
||||
}
|
||||
|
||||
async function save(afterReturn = false) {
|
||||
if (
|
||||
!resourceId ||
|
||||
isSaving ||
|
||||
controller.isLoading ||
|
||||
controller.loadError ||
|
||||
controller.persistedRevision === null ||
|
||||
controller.editor.isLocked
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSaveError(null);
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const snapshot = structuredClone(controller.editor.state);
|
||||
const snapshotSignature = JSON.stringify(snapshot);
|
||||
const result = await resolvedStore.save(
|
||||
resourceId,
|
||||
controller.persistedRevision,
|
||||
snapshot,
|
||||
);
|
||||
if (result.status === 'conflict') {
|
||||
setSaveError('资源已在别处更新;请重新加载后再保存。');
|
||||
return;
|
||||
}
|
||||
controller.setPersistedRevision(result.revision);
|
||||
if (JSON.stringify(controller.editor.state) === snapshotSignature) {
|
||||
setSavedStateSignature(snapshotSignature);
|
||||
}
|
||||
if (await session.save.save()) {
|
||||
if (afterReturn) {
|
||||
setReturnConfirmOpen(false);
|
||||
onBack?.();
|
||||
}
|
||||
} catch {
|
||||
setSaveError(saveFailureMessage());
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function requestSave(afterReturn = false) {
|
||||
if (!resourceId || isSaving || controller.isAiRunning) return;
|
||||
if (!resourceId || session.save.isSaving || session.workflow.isAiRunning) {
|
||||
return;
|
||||
}
|
||||
setSaveAfterReturn(afterReturn);
|
||||
if (controller.postCheckIssuesForSave().length > 0) {
|
||||
if (session.save.hasWarnings()) {
|
||||
setSaveWarningOpen(true);
|
||||
return;
|
||||
}
|
||||
@@ -123,7 +69,7 @@ export default function UiEditorPage({
|
||||
}
|
||||
|
||||
function requestBack() {
|
||||
if (isDirty) {
|
||||
if (session.save.isDirty) {
|
||||
setReturnConfirmOpen(true);
|
||||
return;
|
||||
}
|
||||
@@ -147,49 +93,41 @@ export default function UiEditorPage({
|
||||
type="button"
|
||||
className="rounded-lg bg-orange-600 px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-60"
|
||||
disabled={
|
||||
isSaving ||
|
||||
controller.isLoading ||
|
||||
Boolean(controller.loadError) ||
|
||||
controller.persistedRevision === null ||
|
||||
controller.editor.isLocked
|
||||
session.save.isSaving ||
|
||||
session.save.isLoading ||
|
||||
Boolean(session.save.loadError) ||
|
||||
session.save.persistedRevision === null ||
|
||||
session.save.isLocked
|
||||
}
|
||||
onClick={() => requestSave()}
|
||||
>
|
||||
{isSaving ? '保存中…' : '保存'}
|
||||
{session.save.isSaving ? '保存中…' : '保存'}
|
||||
</button>
|
||||
</header>
|
||||
) : null}
|
||||
{saveError ? (
|
||||
{session.save.saveError ? (
|
||||
<div
|
||||
className="mx-4 mt-3 flex shrink-0 items-center justify-between gap-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800"
|
||||
role="alert"
|
||||
>
|
||||
<span>{saveError}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded px-1 py-0.5 text-red-700 hover:bg-red-100"
|
||||
aria-label="关闭保存错误提示"
|
||||
onClick={() => setSaveError(null)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<span>{session.save.saveError}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<ToolNavigation
|
||||
activeStep={controller.activeStep}
|
||||
furthestStepIndex={controller.furthestStepIndex}
|
||||
disabled={controller.isAiRunning}
|
||||
onChange={controller.requestStepChange}
|
||||
activeStep={session.workflow.activeStep}
|
||||
furthestStepIndex={session.workflow.furthestStepIndex}
|
||||
disabled={session.workflow.isAiRunning}
|
||||
onChange={session.workflow.requestStepChange}
|
||||
/>
|
||||
<div className="grid min-h-0 min-w-0 flex-1 grid-cols-[minmax(0,24fr)_minmax(0,44fr)_minmax(0,32fr)] overflow-hidden">
|
||||
<InputSidebar controller={controller} />
|
||||
<InputSidebar input={session.input} />
|
||||
<div className="flex min-h-0 min-w-0 flex-col overflow-hidden">
|
||||
<WorkflowActionCard controller={controller} />
|
||||
<PreviewWorkspace controller={controller} />
|
||||
<WorkflowActionCard workflow={session.workflow} />
|
||||
<PreviewWorkspace canvas={session.canvas} />
|
||||
</div>
|
||||
<InspectorSidebar controller={controller} />
|
||||
<InspectorSidebar inspector={session.inspector} />
|
||||
</div>
|
||||
<EditorDialogs controller={controller} />
|
||||
<EditorDialogs dialogs={session.dialogs} />
|
||||
<ThemedModal
|
||||
open={returnConfirmOpen}
|
||||
onClose={() => setReturnConfirmOpen(false)}
|
||||
@@ -197,8 +135,8 @@ export default function UiEditorPage({
|
||||
panelClassName="w-[420px] rounded-2xl p-5"
|
||||
>
|
||||
<h2 className="m-0 text-base font-semibold">返回资源?</h2>
|
||||
{saveError ? (
|
||||
<p className="mt-3 text-sm text-red-600">{saveError}</p>
|
||||
{session.save.saveError ? (
|
||||
<p className="mt-3 text-sm text-red-600">{session.save.saveError}</p>
|
||||
) : null}
|
||||
<div className="mt-5 flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
@@ -222,44 +160,44 @@ export default function UiEditorPage({
|
||||
type="button"
|
||||
className="rounded-lg bg-orange-600 px-3 py-2 text-xs font-semibold text-white disabled:opacity-60"
|
||||
disabled={
|
||||
isSaving ||
|
||||
Boolean(controller.loadError) ||
|
||||
controller.persistedRevision === null ||
|
||||
controller.editor.isLocked
|
||||
session.save.isSaving ||
|
||||
Boolean(session.save.loadError) ||
|
||||
session.save.persistedRevision === null ||
|
||||
session.save.isLocked
|
||||
}
|
||||
onClick={() => requestSave(true)}
|
||||
>
|
||||
{isSaving ? '保存中…' : '保存并返回'}
|
||||
{session.save.isSaving ? '保存中…' : '保存并返回'}
|
||||
</button>
|
||||
</div>
|
||||
</ThemedModal>
|
||||
<ThemedModal
|
||||
open={controller.pendingWorkflowStepChange !== null}
|
||||
onClose={controller.cancelStepChange}
|
||||
open={session.workflow.pendingStepChange !== null}
|
||||
onClose={session.workflow.cancelStepChange}
|
||||
ariaLabel="步骤检查"
|
||||
panelClassName="w-[440px] rounded-2xl p-5"
|
||||
>
|
||||
<h2 className="m-0 text-base font-semibold">检查发现问题</h2>
|
||||
<WorkflowIssues
|
||||
title={`离开${stepLabel(controller.pendingWorkflowStepChange?.from)}`}
|
||||
issues={controller.pendingWorkflowStepChange?.leavingIssues ?? []}
|
||||
title={`离开${stepLabel(session.workflow.pendingStepChange?.from)}`}
|
||||
issues={session.workflow.pendingStepChange?.leavingIssues ?? []}
|
||||
/>
|
||||
<WorkflowIssues
|
||||
title={`进入${stepLabel(controller.pendingWorkflowStepChange?.to)}`}
|
||||
issues={controller.pendingWorkflowStepChange?.enteringIssues ?? []}
|
||||
title={`进入${stepLabel(session.workflow.pendingStepChange?.to)}`}
|
||||
issues={session.workflow.pendingStepChange?.enteringIssues ?? []}
|
||||
/>
|
||||
<div className="mt-5 flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-xs"
|
||||
onClick={controller.cancelStepChange}
|
||||
onClick={session.workflow.cancelStepChange}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-orange-600 px-3 py-2 text-xs font-semibold text-white"
|
||||
onClick={controller.confirmStepChange}
|
||||
onClick={session.workflow.confirmStepChange}
|
||||
>
|
||||
仍然继续
|
||||
</button>
|
||||
@@ -274,7 +212,7 @@ export default function UiEditorPage({
|
||||
<h2 className="m-0 text-base font-semibold">检查发现问题</h2>
|
||||
<WorkflowIssues
|
||||
title="保存当前编辑结果"
|
||||
issues={controller.postCheckIssuesForSave()}
|
||||
issues={session.save.warnings()}
|
||||
/>
|
||||
<div className="mt-5 flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
@@ -300,7 +238,7 @@ export default function UiEditorPage({
|
||||
);
|
||||
}
|
||||
|
||||
function stepLabel(step: UiEditorPageController['activeStep'] | undefined) {
|
||||
function stepLabel(step: UiEditorWorkflowProjection['activeStep'] | undefined) {
|
||||
return UI_EDITOR_STEPS.find((item) => item.id === step)?.label ?? '当前步骤';
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,6 @@ import {
|
||||
prepareSpriteAssetBatch,
|
||||
} from '../../features/ui-editor/importAdapter';
|
||||
import { applyMergeResult } from '../../features/ui-editor/merge';
|
||||
import {
|
||||
findNodePageContext,
|
||||
pageRectFromSize,
|
||||
pageRectSize,
|
||||
} from '../../features/ui-editor/nodeTransformGeometry';
|
||||
import { applyRecognitionResult } from '../../features/ui-editor/recognition';
|
||||
import type { StageStatusField } from '../../features/ui-editor/stageStatusOverview';
|
||||
import { collectUiNodeIds } from '../../features/ui-editor/treeUtils';
|
||||
@@ -112,6 +107,31 @@ function isUiNodeEffectivelyVisible(
|
||||
return null;
|
||||
}
|
||||
|
||||
function findNodeContext(
|
||||
node: UiNode,
|
||||
nodeId: NodeId,
|
||||
parentSize: { width: number; height: number },
|
||||
): { node: UiNode; parentSize: { width: number; height: number } } | null {
|
||||
if (node.id === nodeId) return { node, parentSize };
|
||||
const width =
|
||||
parentSize.width *
|
||||
(node.layout.transform.anchor_max[0] -
|
||||
node.layout.transform.anchor_min[0]) +
|
||||
node.layout.transform.offset_max[0] -
|
||||
node.layout.transform.offset_min[0];
|
||||
const height =
|
||||
parentSize.height *
|
||||
(node.layout.transform.anchor_max[1] -
|
||||
node.layout.transform.anchor_min[1]) +
|
||||
node.layout.transform.offset_max[1] -
|
||||
node.layout.transform.offset_min[1];
|
||||
for (const child of node.children) {
|
||||
const found = findNodeContext(child, nodeId, { width, height });
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isSlaveToDescendant(
|
||||
images: State['ui_design_images'],
|
||||
candidateId: UIDesignImageId,
|
||||
@@ -138,7 +158,13 @@ export type PendingWorkflowStepChange = {
|
||||
enteringIssues: UiEditorPrerequisiteIssue[];
|
||||
};
|
||||
|
||||
export function useUiEditorPage(
|
||||
/**
|
||||
* UI 编辑会话是视图与状态/平台 adapter 之间唯一的协调边界。
|
||||
*
|
||||
* 视图只能取得与其职责匹配的 projection;不要把这个会话对象整体
|
||||
* 继续向下传递,否则会重新形成与实现等大的 controller interface。
|
||||
*/
|
||||
export function useUiEditorSession(
|
||||
projectPath: string,
|
||||
resourceId?: string,
|
||||
stateStore: UiDesignStateStore = mockUiDesignStateStore,
|
||||
@@ -150,6 +176,11 @@ export function useUiEditorPage(
|
||||
const [persistedRevision, setPersistedRevision] = useState<number | null>(
|
||||
resourceId ? null : 0,
|
||||
);
|
||||
const [savedStateSignature, setSavedStateSignature] = useState<string | null>(
|
||||
resourceId ? null : JSON.stringify(EMPTY_UI_EDITOR_STATE),
|
||||
);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [activeStep, setActiveStep] =
|
||||
useState<UiEditorStepId>('reference-analysis');
|
||||
const [furthestStepIndex, setFurthestStepIndex] = useState(0);
|
||||
@@ -203,6 +234,8 @@ export function useUiEditorPage(
|
||||
setIsLoading(false);
|
||||
setLoadError(null);
|
||||
setPersistedRevision(0);
|
||||
setSavedStateSignature(JSON.stringify(EMPTY_UI_EDITOR_STATE));
|
||||
setSaveError(null);
|
||||
setHiddenNodeIds(new Set());
|
||||
return;
|
||||
}
|
||||
@@ -225,6 +258,8 @@ export function useUiEditorPage(
|
||||
setHiddenNodeIds(new Set());
|
||||
setPreviewUrls({});
|
||||
setPersistedRevision(revision);
|
||||
setSavedStateSignature(JSON.stringify(state));
|
||||
setSaveError(null);
|
||||
if (projectPath) {
|
||||
const scopeId = createProjectResourcePreviewScopeId();
|
||||
const resources = [
|
||||
@@ -299,6 +334,11 @@ export function useUiEditorPage(
|
||||
!isSlaveToDescendant(images, id as UIDesignImageId, activeImageId),
|
||||
);
|
||||
const isAiRunning = isSuggesting || isRecognizing || isBinding || isMerging;
|
||||
const stateSignature = JSON.stringify(editor.state);
|
||||
const isDirty =
|
||||
resourceId !== undefined &&
|
||||
savedStateSignature !== null &&
|
||||
savedStateSignature !== stateSignature;
|
||||
const nextStep: UiEditorStepId | null =
|
||||
activeStep === 'reference-analysis'
|
||||
? 'structure-recognition'
|
||||
@@ -447,17 +487,10 @@ export function useUiEditorPage(
|
||||
const width = activeImage.pixel_size[0] / ppu;
|
||||
const height = activeImage.pixel_size[1] / ppu;
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
||||
const context = findNodePageContext(
|
||||
treeForActiveImage.root,
|
||||
selectedNodeId,
|
||||
pageRectFromSize([width, height]),
|
||||
);
|
||||
if (!context) return null;
|
||||
const [parentWidth, parentHeight] = pageRectSize(context.parentRect);
|
||||
return {
|
||||
node: context.node,
|
||||
parentSize: { width: parentWidth, height: parentHeight },
|
||||
};
|
||||
return findNodeContext(treeForActiveImage.root, selectedNodeId, {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}, [activeImage, selectedNodeId, treeForActiveImage]);
|
||||
|
||||
async function importAssets(imported: ImportedAsset[]) {
|
||||
@@ -955,117 +988,220 @@ export function useUiEditorPage(
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (
|
||||
!resourceId ||
|
||||
isSaving ||
|
||||
isLoading ||
|
||||
loadError ||
|
||||
persistedRevision === null ||
|
||||
editor.isLocked
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
setSaveError(null);
|
||||
setIsSaving(true);
|
||||
const snapshot = structuredClone(editor.state);
|
||||
const snapshotSignature = JSON.stringify(snapshot);
|
||||
try {
|
||||
const result = await stateStore.save(
|
||||
resourceId,
|
||||
persistedRevision,
|
||||
snapshot,
|
||||
);
|
||||
if (result.status === 'conflict') {
|
||||
setSaveError('资源已在别处更新;请重新加载后再保存。');
|
||||
return false;
|
||||
}
|
||||
setPersistedRevision(result.revision);
|
||||
if (JSON.stringify(editor.state) === snapshotSignature) {
|
||||
setSavedStateSignature(snapshotSignature);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
setSaveError(error instanceof Error ? error.message : String(error));
|
||||
return false;
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projectPath,
|
||||
resourceId,
|
||||
isLoading,
|
||||
loadError,
|
||||
persistedRevision,
|
||||
setPersistedRevision,
|
||||
editor,
|
||||
activeStep,
|
||||
furthestStepIndex,
|
||||
nextStep,
|
||||
isAiRunning,
|
||||
imageOrder,
|
||||
activeImageId,
|
||||
selectedSpriteId,
|
||||
selectedFontId,
|
||||
selectedNodeId,
|
||||
highlightedStatusField,
|
||||
focusRequest,
|
||||
importKind,
|
||||
previewUrls,
|
||||
status,
|
||||
pendingWorkflowStepChange,
|
||||
clearOpen,
|
||||
pendingRemoval,
|
||||
images,
|
||||
sprites,
|
||||
fonts,
|
||||
fontFaces,
|
||||
activeImage,
|
||||
selectedSprite,
|
||||
selectedFont,
|
||||
hiddenNodeIds,
|
||||
isNodePreviewVisible,
|
||||
setNodePreviewVisible,
|
||||
toggleNodePreviewVisibility,
|
||||
pageOptions,
|
||||
spriteReferenceCounts,
|
||||
fontReferenceCounts,
|
||||
treeForActiveImage,
|
||||
selectedNode: selectedNodeContext?.node ?? null,
|
||||
selectedNodeParentSize: selectedNodeContext?.parentSize,
|
||||
selectedNodeParent:
|
||||
selectedNodeId && treeForActiveImage
|
||||
? (findUiNodeLocation(treeForActiveImage.root, selectedNodeId)
|
||||
?.parent ?? null)
|
||||
: null,
|
||||
keepChildrenUnchanged,
|
||||
setKeepChildrenUnchanged,
|
||||
requestStepChange,
|
||||
continueToNextStep: () => {
|
||||
if (nextStep) requestStepChange(nextStep);
|
||||
input: {
|
||||
projectPath,
|
||||
isLocked: editor.isLocked,
|
||||
uiTrees: editor.state.ui_trees,
|
||||
activeStep,
|
||||
imageOrder,
|
||||
activeImageId,
|
||||
previewUrls,
|
||||
images,
|
||||
sprites,
|
||||
fonts,
|
||||
fontFaces,
|
||||
fontReferenceCounts,
|
||||
selectedFontId,
|
||||
spriteReferenceCounts,
|
||||
selectedNodeId,
|
||||
focusRequest,
|
||||
operations: {
|
||||
isBinding,
|
||||
isMerging,
|
||||
isRecognizing,
|
||||
isSuggesting,
|
||||
bindingStatus,
|
||||
mergeStatus,
|
||||
recognitionStatus,
|
||||
suggestionStatus,
|
||||
},
|
||||
checkPrerequisites,
|
||||
bindComponents,
|
||||
mergeUi,
|
||||
recognizeUi,
|
||||
suggestUiDesignSemantics,
|
||||
openImporter: setImportKind,
|
||||
selectDesignImage,
|
||||
selectSprite,
|
||||
selectFont,
|
||||
focusNode,
|
||||
highlightStatusField,
|
||||
isNodePreviewVisible,
|
||||
toggleNodePreviewVisibility,
|
||||
selectNode,
|
||||
insertNode,
|
||||
insertNodeAfter,
|
||||
deleteNode,
|
||||
moveNode,
|
||||
},
|
||||
canvas: {
|
||||
isLocked: editor.isLocked,
|
||||
activeImage,
|
||||
activeImageId,
|
||||
previewUrls,
|
||||
images,
|
||||
sprites,
|
||||
fontFaces,
|
||||
tree: treeForActiveImage ?? null,
|
||||
selectedNode: selectedNodeContext?.node ?? null,
|
||||
selectedNodeId,
|
||||
hiddenNodeIds,
|
||||
focusRequest,
|
||||
status,
|
||||
isNodePreviewVisible,
|
||||
toggleNodePreviewVisibility,
|
||||
selectNode,
|
||||
clearNodeSelection,
|
||||
updateNodeTransform,
|
||||
insertNode,
|
||||
insertNodeAfter,
|
||||
deleteNode,
|
||||
openClearDialog: () => setClearOpen(true),
|
||||
},
|
||||
inspector: {
|
||||
isLocked: editor.isLocked,
|
||||
projectPath,
|
||||
activeImage,
|
||||
activeImageId,
|
||||
selectedSprite,
|
||||
selectedSpriteId,
|
||||
selectedFont,
|
||||
selectedFontId,
|
||||
selectedNode: selectedNodeContext?.node ?? null,
|
||||
selectedNodeParentSize: selectedNodeContext?.parentSize,
|
||||
selectedNodeParent:
|
||||
selectedNodeId && treeForActiveImage
|
||||
? (findUiNodeLocation(treeForActiveImage.root, selectedNodeId)
|
||||
?.parent ?? null)
|
||||
: null,
|
||||
tree: treeForActiveImage ?? null,
|
||||
previewUrls,
|
||||
sprites,
|
||||
fonts,
|
||||
fontFaces,
|
||||
pageOptions,
|
||||
spriteReferenceCounts,
|
||||
fontReferenceCounts,
|
||||
keepChildrenUnchanged,
|
||||
highlightedStatusField,
|
||||
isNodePreviewVisible,
|
||||
setNodePreviewVisible,
|
||||
setKeepChildrenUnchanged,
|
||||
setNodeChildrenDisplayMode,
|
||||
setNodeMetadata,
|
||||
setNodeTransform,
|
||||
setNodeLayout,
|
||||
setNodeComponents,
|
||||
insertNodeComponent,
|
||||
deleteNodeComponent,
|
||||
moveNodeComponent,
|
||||
deleteNode,
|
||||
setSpriteName,
|
||||
setSpriteAssetType,
|
||||
setSpriteBorder,
|
||||
requestSpriteRemoval,
|
||||
requestFontRemoval,
|
||||
setImageName,
|
||||
setImageDescription,
|
||||
setImageRole,
|
||||
setImageSlaveTo,
|
||||
requestDesignImageRemoval,
|
||||
},
|
||||
workflow: {
|
||||
activeStep,
|
||||
furthestStepIndex,
|
||||
nextStep,
|
||||
isAiRunning,
|
||||
pendingStepChange: pendingWorkflowStepChange,
|
||||
isSuggesting,
|
||||
hasSuggested,
|
||||
suggestionStatus,
|
||||
suggestUiDesignSemantics,
|
||||
isRecognizing,
|
||||
hasRecognized,
|
||||
recognitionStatus,
|
||||
recognizeUi,
|
||||
isBinding,
|
||||
hasBound,
|
||||
bindingStatus,
|
||||
bindComponents,
|
||||
requestStepChange,
|
||||
continueToNextStep: () => {
|
||||
if (nextStep) requestStepChange(nextStep);
|
||||
},
|
||||
confirmStepChange,
|
||||
cancelStepChange: () => setPendingWorkflowStepChange(null),
|
||||
},
|
||||
dialogs: {
|
||||
projectPath,
|
||||
importKind,
|
||||
pendingRemoval,
|
||||
clearOpen,
|
||||
closeImporter: () => setImportKind(null),
|
||||
importAssets,
|
||||
closeClearDialog: () => setClearOpen(false),
|
||||
clearState,
|
||||
confirmRemoval,
|
||||
cancelRemoval: () => setPendingRemoval(null),
|
||||
},
|
||||
save: {
|
||||
isLoading,
|
||||
loadError,
|
||||
isLocked: editor.isLocked,
|
||||
persistedRevision,
|
||||
isSaving,
|
||||
isDirty,
|
||||
saveError,
|
||||
hasWarnings: () => postCheckIssuesForSave(editor.state).length > 0,
|
||||
warnings: () => postCheckIssuesForSave(editor.state),
|
||||
save,
|
||||
},
|
||||
confirmStepChange,
|
||||
cancelStepChange: () => setPendingWorkflowStepChange(null),
|
||||
postCheckIssuesForSave: () => postCheckIssuesForSave(editor.state),
|
||||
checkPrerequisites,
|
||||
selectDesignImage,
|
||||
selectSprite,
|
||||
selectFont,
|
||||
requestFontRemoval,
|
||||
selectNode,
|
||||
focusNode,
|
||||
clearNodeSelection,
|
||||
highlightStatusField,
|
||||
setNodeTransform,
|
||||
updateNodeTransform,
|
||||
setNodeMetadata,
|
||||
setNodeLayout,
|
||||
setNodeChildrenDisplayMode,
|
||||
setNodeComponents,
|
||||
insertNodeComponent,
|
||||
deleteNodeComponent,
|
||||
moveNodeComponent,
|
||||
moveNode,
|
||||
insertNode,
|
||||
insertNodeAfter,
|
||||
deleteNode,
|
||||
openImporter: setImportKind,
|
||||
closeImporter: () => setImportKind(null),
|
||||
importAssets,
|
||||
requestDesignImageRemoval,
|
||||
requestSpriteRemoval,
|
||||
confirmRemoval,
|
||||
cancelRemoval: () => setPendingRemoval(null),
|
||||
openClearDialog: () => setClearOpen(true),
|
||||
closeClearDialog: () => setClearOpen(false),
|
||||
clearState,
|
||||
setImageName,
|
||||
setImageDescription,
|
||||
setImageRole,
|
||||
setImageSlaveTo,
|
||||
setSpriteName,
|
||||
setSpriteAssetType,
|
||||
setSpriteBorder,
|
||||
isSuggesting,
|
||||
hasSuggested,
|
||||
suggestionStatus,
|
||||
suggestUiDesignSemantics,
|
||||
isRecognizing,
|
||||
hasRecognized,
|
||||
recognitionStatus,
|
||||
recognizeUi,
|
||||
isMerging,
|
||||
mergeStatus,
|
||||
mergeUi,
|
||||
isBinding,
|
||||
hasBound,
|
||||
bindingStatus,
|
||||
bindComponents,
|
||||
};
|
||||
}
|
||||
|
||||
export type UiEditorPageController = ReturnType<typeof useUiEditorPage>;
|
||||
export type UiEditorSession = ReturnType<typeof useUiEditorSession>;
|
||||
export type UiEditorInputProjection = UiEditorSession['input'];
|
||||
export type UiEditorCanvasProjection = UiEditorSession['canvas'];
|
||||
export type UiEditorInspectorProjection = UiEditorSession['inspector'];
|
||||
export type UiEditorWorkflowProjection = UiEditorSession['workflow'];
|
||||
export type UiEditorDialogsProjection = UiEditorSession['dialogs'];
|
||||
export type UiEditorSaveIntent = UiEditorSession['save'];
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# UI 编辑会话模块边界
|
||||
|
||||
更新时间:`2026-08-19`
|
||||
|
||||
`apps/ai-game-creator-shell` 的 UI 编辑器以 `useUiEditorSession` 作为视图与 adapter 的唯一协调边界。会话持有资源加载、revision、预览请求、选择、节点可见性、步骤 gate、异步操作和保存意图;`UiDesignStateStore` 与 Tauri 调用仍由该模块注入,不进入视图组件。
|
||||
|
||||
页面与子视图不得再传递完整 controller。它们按职责读取以下小 projection:
|
||||
|
||||
- `input`:资源输入、节点树及调试操作。
|
||||
- `canvas`:当前画布、选择、聚焦与节点变换。
|
||||
- `inspector`:当前检查对象及其编辑动作。
|
||||
- `workflow`:步骤 gate 与正式 AI 操作生命周期。
|
||||
- `dialogs`:导入、清空和删除确认。
|
||||
- `save`:加载 / revision 状态、脏状态、保存警告和保存意图。
|
||||
|
||||
预览树在会话边界归一化为 `UITree | null`:尚未选择界面图、正在加载或尚未识别树时均以 `null` 表示,不能把 `Array.find` 的 `undefined` 传播到视图接口。
|
||||
Reference in New Issue
Block a user