修复 UI 节点隐藏状态与移动语义
将隐藏状态收束为页面会话级 Set<NodeId》。 抽取节点移动请求 DTO 与树遍历 helper。 恢复同树重挂的页面位置稳定换算并补充回归测试。
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import type { Node as UiNode } from './types/Node';
|
||||
import type { NodeId } from './types/NodeId';
|
||||
|
||||
export function visitUiNodes(
|
||||
root: UiNode,
|
||||
visit: (node: UiNode) => void,
|
||||
): void {
|
||||
visit(root);
|
||||
for (const child of root.children) visitUiNodes(child, visit);
|
||||
}
|
||||
|
||||
export function collectUiNodeIds(root: UiNode): Set<NodeId> {
|
||||
const ids = new Set<NodeId>();
|
||||
visitUiNodes(root, (node) => ids.add(node.id));
|
||||
return ids;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { NodeId } from './NodeId';
|
||||
import type { UIDesignImageId } from './UIDesignImageId';
|
||||
|
||||
/** UI 节点移动的完整上下文,避免在树视图与状态层之间散落五个并行参数。 */
|
||||
export type UiNodeMoveRequest = {
|
||||
sourceTreeId: UIDesignImageId;
|
||||
targetTreeId: UIDesignImageId;
|
||||
nodeId: NodeId;
|
||||
targetParentId: NodeId;
|
||||
targetIndex: number;
|
||||
};
|
||||
@@ -14,6 +14,7 @@ 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 = {
|
||||
ui_trees: [],
|
||||
@@ -1151,13 +1152,13 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
);
|
||||
|
||||
const moveNode = useCallback(
|
||||
(
|
||||
sourceTreeId: UIDesignImageId,
|
||||
targetTreeId: UIDesignImageId,
|
||||
nodeId: NodeId,
|
||||
targetParentId: NodeId,
|
||||
targetIndex: number,
|
||||
): UiEditorOperationResult => {
|
||||
({
|
||||
sourceTreeId,
|
||||
targetTreeId,
|
||||
nodeId,
|
||||
targetParentId,
|
||||
targetIndex,
|
||||
}: UiNodeMoveRequest): UiEditorOperationResult => {
|
||||
const blocked = guard();
|
||||
if (blocked) return blocked;
|
||||
if (!Number.isInteger(targetIndex) || targetIndex < 0) {
|
||||
@@ -1189,6 +1190,44 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
) {
|
||||
return { ok: false, reason: 'invalid' };
|
||||
}
|
||||
let nextTransform: Node['transform'] | null = null;
|
||||
if (sourceTreeId === targetTreeId) {
|
||||
const image = current.ui_design_images[sourceTreeId];
|
||||
if (!image) return { ok: false, reason: 'missing' };
|
||||
const logicalSize: Rect = {
|
||||
min: [0, 0],
|
||||
size: [
|
||||
image.pixel_size[0] / image.pixels_per_unit,
|
||||
image.pixel_size[1] / image.pixels_per_unit,
|
||||
],
|
||||
};
|
||||
if (
|
||||
!isValidRect(logicalSize) ||
|
||||
logicalSize.size.some((value) => value <= 0)
|
||||
) {
|
||||
return { ok: false, reason: 'invalid' };
|
||||
}
|
||||
const pageRect = findNodePageRect(sourceTree.root, nodeId, logicalSize);
|
||||
const targetPageRect = findNodePageRect(
|
||||
targetTree.root,
|
||||
targetParentId,
|
||||
logicalSize,
|
||||
);
|
||||
if (
|
||||
!pageRect ||
|
||||
pageRect.size.some((value) => value <= 0) ||
|
||||
!targetPageRect ||
|
||||
targetPageRect.size.some((value) => value <= 0)
|
||||
) {
|
||||
return { ok: false, reason: 'invalid' };
|
||||
}
|
||||
nextTransform = setOffsetsForPageRect(
|
||||
source.node.transform,
|
||||
pageRect,
|
||||
targetPageRect,
|
||||
);
|
||||
if (!nextTransform) return { ok: false, reason: 'invalid' };
|
||||
}
|
||||
const next = cloneState(current);
|
||||
const nextSourceTree = next.ui_trees.find(
|
||||
(candidate) => candidate.src_ui_design === sourceTreeId,
|
||||
@@ -1208,8 +1247,9 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) {
|
||||
? nextSource.parent.children.splice(nextSource.index, 1)
|
||||
: [];
|
||||
if (!moved) return { ok: false, reason: 'invalid' };
|
||||
// TODO: Cross-tree moves intentionally preserve the local transform.
|
||||
// Coordinate conversion between design images can be added separately.
|
||||
// 跨树时只保留节点原本的局部 transform;源、目标界面图的画布和父级坐标空间独立,
|
||||
// “保留 transform”不等于承诺页面位置稳定。只有同树移动才在上面做页面矩形换算。
|
||||
if (nextTransform) moved.transform = nextTransform;
|
||||
nextTarget.children.splice(targetIndex, 0, moved);
|
||||
commit(next);
|
||||
return { ok: true, value: undefined };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Image as ImageIcon, Plus, ScanSearch, Type } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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';
|
||||
@@ -10,15 +11,6 @@ import { UiTreePanel } from './UiTreePanel';
|
||||
|
||||
const SUPER_ROOT_ID = '__ui-editor-super-root__';
|
||||
|
||||
function visitTreeNodes(
|
||||
node: UiNode,
|
||||
treeId: UIDesignImageId,
|
||||
result: Map<NodeId, UIDesignImageId>,
|
||||
) {
|
||||
result.set(node.id, treeId);
|
||||
for (const child of node.children) visitTreeNodes(child, treeId, result);
|
||||
}
|
||||
|
||||
export function InputSidebar({
|
||||
controller,
|
||||
}: {
|
||||
@@ -44,21 +36,13 @@ export function InputSidebar({
|
||||
const treeIdByNodeId = useMemo(() => {
|
||||
const result = new Map<NodeId, UIDesignImageId>();
|
||||
for (const tree of editor.state.ui_trees) {
|
||||
visitTreeNodes(tree.root, tree.src_ui_design, result);
|
||||
visitUiNodes(tree.root, (node) =>
|
||||
result.set(node.id, tree.src_ui_design),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [editor.state.ui_trees]);
|
||||
|
||||
const hiddenNodeIdsByTree = useMemo(() => {
|
||||
const result = new Map<UIDesignImageId, ReadonlySet<NodeId>>();
|
||||
for (const [treeId, hiddenNodeIds] of Object.entries(
|
||||
controller.hiddenNodeIdsByImage,
|
||||
)) {
|
||||
result.set(treeId, new Set(Object.keys(hiddenNodeIds)));
|
||||
}
|
||||
return result;
|
||||
}, [controller.hiddenNodeIdsByImage]);
|
||||
|
||||
const superRoot = useMemo<UiNode | null>(() => {
|
||||
if (editor.state.ui_trees.length === 0) return null;
|
||||
return {
|
||||
@@ -330,15 +314,13 @@ export function InputSidebar({
|
||||
selectedNodeId={selectedNodeId}
|
||||
focusRequest={focusRequest}
|
||||
treeIdForNode={(nodeId) => treeIdByNodeId.get(nodeId) ?? null}
|
||||
hiddenNodeIdsForTree={(treeId) =>
|
||||
hiddenNodeIdsByTree.get(treeId) ?? new Set<NodeId>()
|
||||
}
|
||||
hiddenNodeIds={controller.hiddenNodeIds}
|
||||
onSelectNode={(treeId, nodeId) => {
|
||||
controller.selectDesignImage(treeId);
|
||||
controller.selectNode(nodeId);
|
||||
}}
|
||||
onToggleNodeVisibility={(treeId, nodeId) =>
|
||||
controller.toggleNodePreviewVisibility(nodeId, treeId)
|
||||
onToggleNodeVisibility={(nodeId) =>
|
||||
controller.toggleNodePreviewVisibility(nodeId)
|
||||
}
|
||||
onInsertNode={(treeId, parentId) => {
|
||||
controller.insertNode(parentId, treeId);
|
||||
@@ -349,21 +331,7 @@ export function InputSidebar({
|
||||
onDeleteNode={(treeId, nodeId) => {
|
||||
controller.deleteNode(nodeId, treeId);
|
||||
}}
|
||||
onMoveNode={(
|
||||
sourceTreeId,
|
||||
targetTreeId,
|
||||
nodeId,
|
||||
targetParentId,
|
||||
targetIndex,
|
||||
) => {
|
||||
controller.moveNode(
|
||||
sourceTreeId,
|
||||
targetTreeId,
|
||||
nodeId,
|
||||
targetParentId,
|
||||
targetIndex,
|
||||
);
|
||||
}}
|
||||
onMoveNode={(request) => controller.moveNode(request)}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -16,28 +16,21 @@ import {
|
||||
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 { UiNodeMoveRequest } from '../../../features/ui-editor/types/UiNodeMoveRequest';
|
||||
import type { UiEditorNodeFocusRequest } from '../model';
|
||||
|
||||
const EMPTY_HIDDEN_NODE_IDS = new Set<NodeId>();
|
||||
|
||||
type UiTreePanelProps = {
|
||||
root: UiNode | null;
|
||||
selectedNodeId: NodeId | null;
|
||||
focusRequest: UiEditorNodeFocusRequest | null;
|
||||
treeIdForNode: (nodeId: NodeId) => UIDesignImageId | null;
|
||||
hiddenNodeIdsForTree: (treeId: UIDesignImageId) => ReadonlySet<NodeId>;
|
||||
hiddenNodeIds: ReadonlySet<NodeId>;
|
||||
onSelectNode: (treeId: UIDesignImageId, id: NodeId) => void;
|
||||
onToggleNodeVisibility: (treeId: UIDesignImageId, id: NodeId) => void;
|
||||
onToggleNodeVisibility: (id: NodeId) => void;
|
||||
onInsertNode: (treeId: UIDesignImageId, parentId: NodeId) => void;
|
||||
onInsertNodeAfter: (treeId: UIDesignImageId, nodeId: NodeId) => void;
|
||||
onDeleteNode: (treeId: UIDesignImageId, nodeId: NodeId) => void;
|
||||
onMoveNode: (
|
||||
sourceTreeId: UIDesignImageId,
|
||||
targetTreeId: UIDesignImageId,
|
||||
nodeId: NodeId,
|
||||
targetParentId: NodeId,
|
||||
targetIndex: number,
|
||||
) => void;
|
||||
onMoveNode: (request: UiNodeMoveRequest) => void;
|
||||
};
|
||||
|
||||
function TreeRow({
|
||||
@@ -118,7 +111,7 @@ export function UiTreePanel({
|
||||
selectedNodeId,
|
||||
focusRequest,
|
||||
treeIdForNode,
|
||||
hiddenNodeIdsForTree,
|
||||
hiddenNodeIds,
|
||||
onSelectNode,
|
||||
onToggleNodeVisibility,
|
||||
onInsertNode,
|
||||
@@ -171,7 +164,13 @@ export function UiTreePanel({
|
||||
dragIds,
|
||||
siblingIds: destinationIds,
|
||||
});
|
||||
onMoveNode(sourceTreeId, targetTreeId, nodeId, parentId, targetIndex);
|
||||
onMoveNode({
|
||||
sourceTreeId,
|
||||
targetTreeId,
|
||||
nodeId,
|
||||
targetParentId: parentId,
|
||||
targetIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const pageRootIds = new Set(root?.children.map((child) => child.id) ?? []);
|
||||
@@ -212,7 +211,6 @@ export function UiTreePanel({
|
||||
aria-label="UI 节点树"
|
||||
>
|
||||
{(props) => {
|
||||
const treeId = treeIdForNode(props.node.data.id);
|
||||
return (
|
||||
<TreeRow
|
||||
{...props}
|
||||
@@ -220,15 +218,8 @@ export function UiTreePanel({
|
||||
const nodeTreeId = treeIdForNode(nodeId);
|
||||
if (nodeTreeId) onSelectNode(nodeTreeId, nodeId);
|
||||
}}
|
||||
hiddenNodeIds={
|
||||
treeId
|
||||
? hiddenNodeIdsForTree(treeId)
|
||||
: EMPTY_HIDDEN_NODE_IDS
|
||||
}
|
||||
onToggleNodeVisibility={(nodeId) => {
|
||||
const nodeTreeId = treeIdForNode(nodeId);
|
||||
if (nodeTreeId) onToggleNodeVisibility(nodeTreeId, nodeId);
|
||||
}}
|
||||
hiddenNodeIds={hiddenNodeIds}
|
||||
onToggleNodeVisibility={onToggleNodeVisibility}
|
||||
onOpenContextMenu={(event, nodeId) =>
|
||||
nodeId === root.id
|
||||
? undefined
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
validateVisualBindingResult,
|
||||
} from '../../features/ui-editor/prerequisites';
|
||||
import { applyRecognitionResult } from '../../features/ui-editor/recognition';
|
||||
import { collectUiNodeIds } from '../../features/ui-editor/treeUtils';
|
||||
import type { BindingDTO } from '../../features/ui-editor/types/BindingDTO';
|
||||
import type { Component } from '../../features/ui-editor/types/Component';
|
||||
import type { FontAssetId } from '../../features/ui-editor/types/FontAssetId';
|
||||
@@ -31,6 +32,7 @@ import type { SpriteBorder } from '../../features/ui-editor/types/SpriteBorder';
|
||||
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 {
|
||||
mockUiDesignStateStore,
|
||||
type UiDesignStateStore,
|
||||
@@ -58,18 +60,6 @@ import { useUiEditorNodeFocus } from './useUiEditorNodeFocus';
|
||||
|
||||
const ASSET_BATCH_SIZE = 5;
|
||||
|
||||
type HiddenNodeIdsByImage = Record<UIDesignImageId, Record<NodeId, true>>;
|
||||
|
||||
function collectNodeIds(root: UiNode): Set<NodeId> {
|
||||
const ids = new Set<NodeId>();
|
||||
const visit = (node: UiNode) => {
|
||||
ids.add(node.id);
|
||||
for (const child of node.children) visit(child);
|
||||
};
|
||||
visit(root);
|
||||
return ids;
|
||||
}
|
||||
|
||||
export type PendingWorkflowStepChange = {
|
||||
from: UiEditorStepId;
|
||||
to: UiEditorStepId;
|
||||
@@ -102,8 +92,10 @@ export function useUiEditorPage(
|
||||
null,
|
||||
);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<NodeId | null>(null);
|
||||
const [hiddenNodeIdsByImage, setHiddenNodeIdsByImage] =
|
||||
useState<HiddenNodeIdsByImage>({});
|
||||
// NodeId 在 State 内全局唯一;隐藏状态直接记录节点 ID,跨树移动时随节点保留。
|
||||
const [hiddenNodeIds, setHiddenNodeIds] = useState<Set<NodeId>>(
|
||||
() => new Set(),
|
||||
);
|
||||
const { focusRequest, focusNode } = useUiEditorNodeFocus({
|
||||
activateImage: setActiveImageId,
|
||||
clearSpriteSelection: () => setSelectedSpriteId(null),
|
||||
@@ -137,7 +129,7 @@ export function useUiEditorPage(
|
||||
setIsLoading(false);
|
||||
setLoadError(null);
|
||||
setPersistedRevision(0);
|
||||
setHiddenNodeIdsByImage({});
|
||||
setHiddenNodeIds(new Set());
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
@@ -156,7 +148,7 @@ export function useUiEditorPage(
|
||||
setActiveImageId(ids[0] ?? null);
|
||||
setSelectedNodeId(null);
|
||||
setSelectedSpriteId(null);
|
||||
setHiddenNodeIdsByImage({});
|
||||
setHiddenNodeIds(new Set());
|
||||
setPreviewUrls({});
|
||||
setPersistedRevision(revision);
|
||||
if (projectPath) {
|
||||
@@ -277,83 +269,44 @@ export function useUiEditorPage(
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
setHiddenNodeIdsByImage((current) => {
|
||||
let changed = false;
|
||||
const next: HiddenNodeIdsByImage = {};
|
||||
for (const [treeId, hiddenNodeIds] of Object.entries(current)) {
|
||||
const tree = editor.state.ui_trees.find(
|
||||
(candidate) => candidate.src_ui_design === treeId,
|
||||
);
|
||||
if (!tree) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
const validNodeIds = collectNodeIds(tree.root);
|
||||
const filtered = Object.fromEntries(
|
||||
Object.keys(hiddenNodeIds)
|
||||
.filter((nodeId) => validNodeIds.has(nodeId))
|
||||
.map((nodeId) => [nodeId, true]),
|
||||
) as Record<NodeId, true>;
|
||||
if (
|
||||
Object.keys(filtered).length !== Object.keys(hiddenNodeIds).length
|
||||
) {
|
||||
changed = true;
|
||||
}
|
||||
if (Object.keys(filtered).length > 0) {
|
||||
next[treeId as UIDesignImageId] = filtered;
|
||||
}
|
||||
const validNodeIds = new Set<NodeId>();
|
||||
for (const tree of editor.state.ui_trees) {
|
||||
for (const nodeId of collectUiNodeIds(tree.root)) {
|
||||
validNodeIds.add(nodeId);
|
||||
}
|
||||
return changed ? next : current;
|
||||
}
|
||||
setHiddenNodeIds((current) => {
|
||||
const next = new Set(
|
||||
[...current].filter((nodeId) => validNodeIds.has(nodeId)),
|
||||
);
|
||||
return next.size === current.size ? current : next;
|
||||
});
|
||||
}, [editor.state.ui_trees]);
|
||||
|
||||
const hiddenNodeIds = useMemo<ReadonlySet<NodeId>>(
|
||||
() =>
|
||||
new Set(
|
||||
activeImageId
|
||||
? Object.keys(hiddenNodeIdsByImage[activeImageId] ?? {})
|
||||
: [],
|
||||
),
|
||||
[activeImageId, hiddenNodeIdsByImage],
|
||||
);
|
||||
|
||||
const isNodePreviewVisible = useCallback(
|
||||
(nodeId: NodeId, treeId = activeImageId) =>
|
||||
!(treeId ? hiddenNodeIdsByImage[treeId]?.[nodeId] : false),
|
||||
[activeImageId, hiddenNodeIdsByImage],
|
||||
(nodeId: NodeId) => !hiddenNodeIds.has(nodeId),
|
||||
[hiddenNodeIds],
|
||||
);
|
||||
|
||||
const setNodePreviewVisible = useCallback(
|
||||
(nodeId: NodeId, visible: boolean, treeId = activeImageId) => {
|
||||
if (!treeId) return;
|
||||
setHiddenNodeIdsByImage((current) => {
|
||||
const currentForImage = current[treeId] ?? {};
|
||||
const currentlyVisible = !currentForImage[nodeId];
|
||||
(nodeId: NodeId, visible: boolean) => {
|
||||
setHiddenNodeIds((current) => {
|
||||
const currentlyVisible = !current.has(nodeId);
|
||||
if (currentlyVisible === visible) return current;
|
||||
const nextForImage = { ...currentForImage };
|
||||
if (visible) delete nextForImage[nodeId];
|
||||
else nextForImage[nodeId] = true;
|
||||
const next = { ...current };
|
||||
if (Object.keys(nextForImage).length > 0) {
|
||||
next[treeId] = nextForImage;
|
||||
} else {
|
||||
delete next[treeId];
|
||||
}
|
||||
const next = new Set(current);
|
||||
if (visible) next.delete(nodeId);
|
||||
else next.add(nodeId);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[activeImageId],
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleNodePreviewVisibility = useCallback(
|
||||
(nodeId: NodeId, treeId = activeImageId) => {
|
||||
setNodePreviewVisible(
|
||||
nodeId,
|
||||
!isNodePreviewVisible(nodeId, treeId),
|
||||
treeId,
|
||||
);
|
||||
(nodeId: NodeId) => {
|
||||
setNodePreviewVisible(nodeId, !isNodePreviewVisible(nodeId));
|
||||
},
|
||||
[activeImageId, isNodePreviewVisible, setNodePreviewVisible],
|
||||
[isNodePreviewVisible, setNodePreviewVisible],
|
||||
);
|
||||
|
||||
const findNodeContext = useCallback(function findNodeContext(
|
||||
@@ -460,12 +413,6 @@ export function useUiEditorPage(
|
||||
if (id === selectedSpriteId) setSelectedSpriteId(null);
|
||||
if (id === selectedFontId) setSelectedFontId(null);
|
||||
if (id !== activeImageId) return;
|
||||
setHiddenNodeIdsByImage((current) => {
|
||||
if (!(id in current)) return current;
|
||||
const next = { ...current };
|
||||
delete next[id as UIDesignImageId];
|
||||
return next;
|
||||
});
|
||||
setSelectedNodeId(null);
|
||||
const index = imageOrder.indexOf(id);
|
||||
const remaining = imageOrder.filter((item) => item !== id);
|
||||
@@ -598,7 +545,7 @@ export function useUiEditorPage(
|
||||
setActiveImageId(null);
|
||||
setSelectedSpriteId(null);
|
||||
setSelectedNodeId(null);
|
||||
setHiddenNodeIdsByImage({});
|
||||
setHiddenNodeIds(new Set());
|
||||
setPreviewUrls({});
|
||||
setClearOpen(false);
|
||||
}
|
||||
@@ -687,20 +634,8 @@ export function useUiEditorPage(
|
||||
return result;
|
||||
}
|
||||
|
||||
function moveNode(
|
||||
sourceTreeId: UIDesignImageId,
|
||||
targetTreeId: UIDesignImageId,
|
||||
nodeId: NodeId,
|
||||
targetParentId: NodeId,
|
||||
targetIndex: number,
|
||||
) {
|
||||
const result = editor.moveNode(
|
||||
sourceTreeId,
|
||||
targetTreeId,
|
||||
nodeId,
|
||||
targetParentId,
|
||||
targetIndex,
|
||||
);
|
||||
function moveNode(request: UiNodeMoveRequest) {
|
||||
const result = editor.moveNode(request);
|
||||
if (!result.ok) {
|
||||
setStatus(
|
||||
result.reason === 'locked'
|
||||
@@ -928,7 +863,6 @@ export function useUiEditorPage(
|
||||
activeImage,
|
||||
selectedSprite,
|
||||
selectedFont,
|
||||
hiddenNodeIdsByImage,
|
||||
hiddenNodeIds,
|
||||
isNodePreviewVisible,
|
||||
setNodePreviewVisible,
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('UiEditorPage', () => {
|
||||
expect(result.current.selectedSpriteId).toBe('panel');
|
||||
});
|
||||
|
||||
it('keeps node preview visibility transient and scoped to each interface image', () => {
|
||||
it('keeps node preview visibility transient and attached to global node IDs', () => {
|
||||
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
|
||||
act(() => {
|
||||
result.current.editor.addDesignImages([
|
||||
@@ -209,7 +209,7 @@ describe('UiEditorPage', () => {
|
||||
expect(JSON.stringify(result.current.editor.state)).toBe(stateBeforeToggle);
|
||||
|
||||
act(() => result.current.selectDesignImage('page-b'));
|
||||
expect(result.current.hiddenNodeIds.size).toBe(0);
|
||||
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
|
||||
act(() => result.current.selectDesignImage('page-a'));
|
||||
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
|
||||
@@ -122,7 +122,13 @@ describe('useUiEditorState', () => {
|
||||
|
||||
act(() => {
|
||||
expect(
|
||||
result.current.moveNode('source', 'target', 'moved', 'target-root', 0),
|
||||
result.current.moveNode({
|
||||
sourceTreeId: 'source',
|
||||
targetTreeId: 'target',
|
||||
nodeId: 'moved',
|
||||
targetParentId: 'target-root',
|
||||
targetIndex: 0,
|
||||
}),
|
||||
).toEqual({ ok: true, value: undefined });
|
||||
});
|
||||
|
||||
@@ -133,6 +139,72 @@ describe('useUiEditorState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps page position stable when reparenting within one UI tree', () => {
|
||||
const moved = nodeWithSprite('moved');
|
||||
moved.transform = {
|
||||
anchor_min: [0, 0],
|
||||
anchor_max: [1, 1],
|
||||
offset_min: [0, 0],
|
||||
offset_max: [0, 0],
|
||||
};
|
||||
const initial: State = {
|
||||
...structuredClone(EMPTY_UI_EDITOR_STATE),
|
||||
ui_design_images: { page: image('Page') },
|
||||
ui_trees: [
|
||||
{
|
||||
src_ui_design: 'page',
|
||||
root: pageRoot('page-root', [
|
||||
{
|
||||
...pageRoot('source-parent'),
|
||||
transform: {
|
||||
anchor_min: [0, 0],
|
||||
anchor_max: [0.5, 0.5],
|
||||
offset_min: [0, 0],
|
||||
offset_max: [0, 0],
|
||||
},
|
||||
children: [moved],
|
||||
},
|
||||
{
|
||||
...pageRoot('target-parent'),
|
||||
transform: {
|
||||
anchor_min: [0.5, 0.5],
|
||||
anchor_max: [1, 1],
|
||||
offset_min: [0, 0],
|
||||
offset_max: [0, 0],
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() => useUiEditorState(initial));
|
||||
|
||||
act(() => {
|
||||
expect(
|
||||
result.current.moveNode({
|
||||
sourceTreeId: 'page',
|
||||
targetTreeId: 'page',
|
||||
nodeId: 'moved',
|
||||
targetParentId: 'target-parent',
|
||||
targetIndex: 0,
|
||||
}),
|
||||
).toEqual({ ok: true, value: undefined });
|
||||
});
|
||||
|
||||
expect(
|
||||
result.current.state.ui_trees[0]?.root.children[0]?.children,
|
||||
).toEqual([]);
|
||||
expect(
|
||||
result.current.state.ui_trees[0]?.root.children[1]?.children[0]
|
||||
?.transform,
|
||||
).toMatchObject({
|
||||
anchor_min: [0, 0],
|
||||
anchor_max: [1, 1],
|
||||
offset_min: [-50, -40],
|
||||
offset_max: [-50, -40],
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes focused operations over one shared state', () => {
|
||||
const { result } = renderHook(() => useUiEditorState());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user