Python内核代码移除,建模界面缩放bug修复

This commit is contained in:
ljz committed 2026-09-10 11:57:39 +08:00
1 parent 743663e3a6
commit 0dcb465d84
27 files changed
+354 -6092

No files matched your search

+68 -44
View File
@@ -74,7 +74,6 @@ import {
useEdgesState,
useNodesState,
useReactFlow,
useStore,
useStoreApi,
useUpdateNodeInternals,
} from "@xyflow/react";
@@ -90,6 +89,7 @@ import {
type SimulationResultsSnapshot,
} from "./SimulationResultsView";
import { AutoFitView } from "./AutoFitView";
import { useScrollbarViewport } from "./useScrollbarViewport";
import { DockedSimulationConsole } from "./DockedSimulationConsole";
import {
ComponentSymbol,
@@ -1760,7 +1760,6 @@ function SimulationComponentNodeImpl({
selected,
}: SimulationComponentNodeProps) {
const updateNodeInternals = useUpdateNodeInternals();
const viewportZoom = useStore((flowState) => flowState.transform[2]);
const symbolContainerRef = useRef<HTMLDivElement>(null);
const rotation = normalizeNodeRotation(data.rotation);
const mirrored = Boolean(data.mirrored);
@@ -1817,7 +1816,6 @@ function SimulationComponentNodeImpl({
rotation,
)
: [];
const inverseViewportScale = viewportZoom > 0 ? 1 / viewportZoom : 1;
const nodeLayoutStyle = {
width: renderedSize.width,
height: renderedSize.height,
@@ -1905,8 +1903,7 @@ function SimulationComponentNodeImpl({
style={{
left: x,
top: y,
transform: `scale(${inverseViewportScale}) translate(-50%, -50%)`,
transformOrigin: "0 0",
transform: "translate(-50%, -50%)",
}}
title={`${port.name} (${port.domain})`}
type={portHandleType(port)}
@@ -1916,10 +1913,6 @@ function SimulationComponentNodeImpl({
className="port-handle-glyph"
preserveAspectRatio="xMidYMid meet"
shapeRendering="geometricPrecision"
style={{
transform: `scale(${viewportZoom})`,
transformOrigin: "center",
}}
viewBox="0 0 6 6"
>
{port.kind === "physical" ? (
@@ -3752,11 +3745,14 @@ function FlowViewportScrollbars({
nodes: SimulationNode[];
}) {
const reactFlow = useReactFlow<SimulationNode, SimulationEdge>();
const transform = useStore((flowState) => flowState.transform);
const canvasWidth = useStore((flowState) => flowState.width);
const canvasHeight = useStore((flowState) => flowState.height);
const {
transform,
width: canvasWidth,
height: canvasHeight,
} = useScrollbarViewport();
const horizontalRef = useRef<HTMLDivElement>(null);
const verticalRef = useRef<HTMLDivElement>(null);
const synchronizedScrollRef = useRef({ left: 0, top: 0 });
const [scrollbarViewport, setScrollbarViewport] = useState({
horizontalWidth: 0,
verticalHeight: 0,
@@ -3903,6 +3899,12 @@ function FlowViewportScrollbars({
if (vertical && Math.abs(vertical.scrollTop - expectedScrollTop) > 0.5) {
vertical.scrollTop = expectedScrollTop;
}
// 浏览器可能对滚动位置取整或裁切;记录真正写入的位置,避免稍后
// 到达的原生 scroll 事件被当作用户拖动,反向覆盖刚恢复/缩放的视口。
synchronizedScrollRef.current = {
left: horizontal?.scrollLeft ?? 0,
top: vertical?.scrollTop ?? 0,
};
}, [expectedScrollLeft, expectedScrollTop, horizontalContentWidth, verticalContentHeight]);
const setHorizontalViewportFromScroll = (nextScrollLeft: number) => {
@@ -3911,10 +3913,10 @@ function FlowViewportScrollbars({
(horizontalScrollRange > 0
? (nextScrollLeft / horizontalScrollRange) * horizontalWorldRange
: 0);
const currentViewport = reactFlow.getViewport();
void reactFlow.setViewport({
x: -nextVisibleLeft * zoom,
y: transform[1],
zoom,
...currentViewport,
x: -nextVisibleLeft * currentViewport.zoom,
});
};
const setVerticalViewportFromScroll = (nextScrollTop: number) => {
@@ -3923,10 +3925,10 @@ function FlowViewportScrollbars({
(verticalScrollRange > 0
? (nextScrollTop / verticalScrollRange) * verticalWorldRange
: 0);
const currentViewport = reactFlow.getViewport();
void reactFlow.setViewport({
x: transform[0],
y: -nextVisibleTop * zoom,
zoom,
...currentViewport,
y: -nextVisibleTop * currentViewport.zoom,
});
};
@@ -3941,7 +3943,7 @@ function FlowViewportScrollbars({
className="flow-viewport-scrollbar flow-viewport-scrollbar-horizontal nowheel nopan"
onScroll={(event) => {
const nextScrollLeft = event.currentTarget.scrollLeft;
if (Math.abs(nextScrollLeft - expectedScrollLeft) <= 0.5) {
if (Math.abs(nextScrollLeft - synchronizedScrollRef.current.left) <= 0.5) {
return;
}
setHorizontalViewportFromScroll(nextScrollLeft);
@@ -3992,7 +3994,7 @@ function FlowViewportScrollbars({
className="flow-viewport-scrollbar flow-viewport-scrollbar-vertical nowheel nopan"
onScroll={(event) => {
const nextScrollTop = event.currentTarget.scrollTop;
if (Math.abs(nextScrollTop - expectedScrollTop) <= 0.5) {
if (Math.abs(nextScrollTop - synchronizedScrollRef.current.top) <= 0.5) {
return;
}
setVerticalViewportFromScroll(nextScrollTop);
@@ -4117,12 +4119,11 @@ function PortGeometryRefresh({
nodeIds: string[];
requestKey: number;
}) {
const zoom = useStore((state) => state.transform[2]);
const updateNodeInternals = useUpdateNodeInternals();
const lastRefreshKeyRef = useRef("");
useLayoutEffect(() => {
const refreshKey = `${requestKey}:${zoom}:${nodeIds.join("\u001f")}`;
const refreshKey = `${requestKey}:${nodeIds.join("\u001f")}`;
if (lastRefreshKeyRef.current === refreshKey || nodeIds.length === 0) {
return;
}
@@ -4131,7 +4132,7 @@ function PortGeometryRefresh({
updateNodeInternals(nodeIds);
});
return () => window.cancelAnimationFrame(frame);
}, [nodeIds, requestKey, updateNodeInternals, zoom]);
}, [nodeIds, requestKey, updateNodeInternals]);
return null;
}
@@ -4257,6 +4258,7 @@ function FlowWorkbench() {
const pendingPasteRef = useRef<PendingPaste | null>(null);
const portConnectionDraftRef = useRef<PortConnectionDraft | null>(null);
const modelingViewportRef = useRef<Viewport>({ x: 0, y: 0, zoom: 1 });
const modelingCanvasRef = useRef<HTMLElement>(null);
const lastFlowPointerRef = useRef<{ x: number; y: number } | null>(null);
const pendingPasteMiddlePointerRef = useRef<PendingPasteMiddlePointer | null>(
null,
@@ -4983,6 +4985,9 @@ function FlowWorkbench() {
return;
}
if (view !== "modeling") {
if (modelingCanvasRef.current) {
modelingViewportRef.current = reactFlow.getViewport();
}
cancelPortConnection();
}
setActiveView(view);
@@ -4994,7 +4999,7 @@ function FlowWorkbench() {
window.history.pushState({ workspaceView: view }, "", nextHash);
}
},
[appendConsoleEntry, cancelPortConnection],
[appendConsoleEntry, cancelPortConnection, reactFlow],
);
const loadSimulationResultsSnapshot = useCallback(
@@ -8862,27 +8867,34 @@ function FlowWorkbench() {
"flow-canvas",
activePortConnection?.mode === "click" ? "connection-planning" : "",
].filter(Boolean).join(" ")}
onClickCapture={handleFlowClickCapture}
onDragOver={(event) => {
if (pendingPasteRef.current) {
event.dataTransfer.dropEffect = "none";
return;
ref={modelingCanvasRef}
style={
{
"--port-handle-hit-scale":
1 / Math.max(modelingViewportRef.current.zoom, 0.01),
} as CSSProperties
}
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}}
onDrop={onDrop}
onPointerCancelCapture={() => {
pendingPasteMiddlePointerRef.current = null;
setNodeDragActive(false);
contactDragSessionRef.current = null;
clearContactSnapCandidate();
handleTightSelectionPointerCancel();
}}
onPointerDownCapture={handleFlowPointerDownCapture}
onPointerMoveCapture={handleFlowPointerMoveCapture}
onPointerUpCapture={handleFlowPointerUpCapture}
>
onClickCapture={handleFlowClickCapture}
onDragOver={(event) => {
if (pendingPasteRef.current) {
event.dataTransfer.dropEffect = "none";
return;
}
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}}
onDrop={onDrop}
onPointerCancelCapture={() => {
pendingPasteMiddlePointerRef.current = null;
setNodeDragActive(false);
contactDragSessionRef.current = null;
clearContactSnapCandidate();
handleTightSelectionPointerCancel();
}}
onPointerDownCapture={handleFlowPointerDownCapture}
onPointerMoveCapture={handleFlowPointerMoveCapture}
onPointerUpCapture={handleFlowPointerUpCapture}
>
<ReactFlow
colorMode="light"
connectionMode={ConnectionMode.Loose}
@@ -8919,6 +8931,18 @@ function FlowWorkbench() {
modelingViewportRef.current = { ...viewport };
syncPendingPasteToPointer();
}}
onMoveEnd={() => {
const canvas = modelingCanvasRef.current;
if (!canvas) return;
// 较早的平移结束事件可能晚于下一次缩放到达,读取当前视口,
// 避免旧事件覆盖最新缩放或页面切换时保存的位置。
const viewport = reactFlow.getViewport();
modelingViewportRef.current = viewport;
canvas.style.setProperty(
"--port-handle-hit-scale",
String(1 / Math.max(viewport.zoom, 0.01)),
);
}}
onPaneClick={handleModelingPaneClick}
onSelectionChange={handleModelingSelectionChange}
onSelectionEnd={finishTightSelection}
+7 -3
View File
@@ -18,7 +18,14 @@ export function AutoFitView({
padding = 0.18,
requestKey,
}: AutoFitViewProps) {
const lastHandledRequestKey = useRef<number | string | null>(
fitOnMount ? null : requestKey,
);
const expectedNodesAreMeasured = useStore((state) => {
// 完成适应窗口后,后续每帧的视口通知无需再遍历整个节点表。
if (Object.is(lastHandledRequestKey.current, requestKey) || !enabled) {
return false;
}
if (expectedNodeCount <= 0 || state.nodeLookup.size !== expectedNodeCount) {
return false;
}
@@ -35,9 +42,6 @@ export function AutoFitView({
return true;
});
const { fitView } = useReactFlow();
const lastHandledRequestKey = useRef<number | string | null>(
fitOnMount ? null : requestKey,
);
useLayoutEffect(() => {
if (Object.is(lastHandledRequestKey.current, requestKey)) {
+10
View File
@@ -1330,6 +1330,14 @@ textarea {
background: #f8fafc;
}
/* 每个元件独立参与布局与合成,缩放时避免反复重绘整张复杂系统图。
* 不使用 paint/size containment:端口、序号和透明命中区仍可超出节点边界。
*/
.flow-canvas .react-flow__node {
contain: layout style;
will-change: transform;
}
.modeling-canvas-stack {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
@@ -1698,6 +1706,8 @@ textarea {
background: transparent;
content: "";
pointer-events: inherit;
transform: scale(var(--port-handle-hit-scale, 1));
transform-origin: center;
}
.port-handle-physical {
+63
View File
@@ -0,0 +1,63 @@
import { useLayoutEffect, useState } from "react";
import { useStoreApi, type Transform } from "@xyflow/react";
type ScrollbarViewport = {
transform: Transform;
width: number;
height: number;
};
const ZOOM_SCROLLBAR_SETTLE_MS = 120;
// 原生滚动条修改内容尺寸后读取 scrollLeft/Top 会强制布局。
// 缩放期间保持其几何稳定,停稳后同步;平移仍逐帧跟随画布。
export function useScrollbarViewport(): ScrollbarViewport {
const store = useStoreApi();
const [viewport, setViewport] = useState<ScrollbarViewport>(() => {
const { transform, width, height } = store.getState();
return { transform, width, height };
});
useLayoutEffect(() => {
let frame = 0;
let settleTimer = 0;
const publish = () => {
frame = 0;
const { transform, width, height } = store.getState();
setViewport((previous) =>
previous.width === width && previous.height === height &&
previous.transform.every((value, index) => value === transform[index])
? previous
: { transform, width, height },
);
};
const schedule = () => {
if (!frame) frame = requestAnimationFrame(publish);
};
const unsubscribe = store.subscribe((current, previous) => {
if (current.width !== previous.width || current.height !== previous.height) {
schedule();
} else if (current.transform !== previous.transform) {
if (current.transform[2] !== previous.transform[2] || settleTimer) {
cancelAnimationFrame(frame);
frame = 0;
clearTimeout(settleTimer);
settleTimer = window.setTimeout(() => {
settleTimer = 0;
schedule();
}, ZOOM_SCROLLBAR_SETTLE_MS);
} else {
schedule();
}
}
});
schedule();
return () => {
unsubscribe();
cancelAnimationFrame(frame);
clearTimeout(settleTimer);
};
}, [store]);
return viewport;
}
+43 -5
View File
@@ -3319,7 +3319,7 @@ test("端口图标随画布缩小且物理端口始终保持标准圆形", async
?.map(Number) ?? [];
return values[0] ?? 1;
});
const readPortShapes = async () => {
const readPortShapes = async (currentZoom: number) => {
const shapes = await scalablePorts.evaluateAll((elements) =>
elements.map((element) => {
const handleBounds = element.getBoundingClientRect();
@@ -3352,8 +3352,8 @@ test("端口图标随画布缩小且物理端口始终保持标准圆形", async
}),
);
for (const shape of shapes) {
expect(Math.abs(shape.handleWidth - 6)).toBeLessThan(0.05);
expect(Math.abs(shape.handleHeight - 6)).toBeLessThan(0.05);
expect(Math.abs(shape.handleWidth - 6 * currentZoom)).toBeLessThan(0.1);
expect(Math.abs(shape.handleHeight - 6 * currentZoom)).toBeLessThan(0.1);
expect(shape.glyphWidth).not.toBeNull();
expect(shape.glyphHeight).not.toBeNull();
expect(Math.abs(shape.glyphWidth! - shape.glyphHeight!)).toBeLessThan(0.01);
@@ -3371,7 +3371,10 @@ test("端口图标随画布缩小且物理端口始终保持标准圆形", async
return shapes;
};
const initialZoom = await readViewportZoom();
const initialShapes = await readPortShapes();
const initialShapes = await readPortShapes(initialZoom);
const initialPortStyles = await scalablePorts.evaluateAll((elements) =>
elements.map((element) => element.getAttribute("style")),
);
for (const shape of initialShapes) {
expect(Math.abs(shape.glyphWidth! - 6 * initialZoom)).toBeLessThan(0.05);
expect(Math.abs(shape.glyphHeight! - 6 * initialZoom)).toBeLessThan(0.05);
@@ -3391,7 +3394,23 @@ test("端口图标随画布缩小且物理端口始终保持标准圆形", async
.poll(async () => (await readViewportZoom()) / initialZoom)
.toBeLessThan(0.7);
const zoomedZoom = await readViewportZoom();
const zoomedShapes = await readPortShapes();
await expect
.poll(async () => {
const hitScale = await page
.locator(".flow-canvas")
.evaluate((element) =>
Number(
element.style.getPropertyValue("--port-handle-hit-scale"),
),
);
return Math.abs(hitScale - 1 / zoomedZoom);
})
.toBeLessThan(0.02);
const zoomedShapes = await readPortShapes(zoomedZoom);
const zoomedPortStyles = await scalablePorts.evaluateAll((elements) =>
elements.map((element) => element.getAttribute("style")),
);
expect(zoomedPortStyles).toEqual(initialPortStyles);
const zoomRatio = zoomedZoom / initialZoom;
for (const [index, shape] of zoomedShapes.entries()) {
expect(Math.abs(shape.glyphWidth! - 6 * zoomedZoom)).toBeLessThan(0.1);
@@ -3400,6 +3419,25 @@ test("端口图标随画布缩小且物理端口始终保持标准圆形", async
Math.abs(shape.glyphWidth! / initialShapes[index].glyphWidth! - zoomRatio),
).toBeLessThan(0.02);
}
// 可见端口随画布缩小,但透明命中区仍保持易于点击的屏幕尺寸。
const firstZoomedPort = scalablePorts.first();
const firstZoomedPortBounds = await firstZoomedPort.boundingBox();
expect(firstZoomedPortBounds).not.toBeNull();
const hitPortName = await page.evaluate(
({ x, y }) =>
document
.elementFromPoint(x, y)
?.closest(".port-handle")
?.getAttribute("data-port-name") ?? null,
{
x: firstZoomedPortBounds!.x - 3,
y: firstZoomedPortBounds!.y + firstZoomedPortBounds!.height / 2,
},
);
expect(hitPortName).toBe(
await firstZoomedPort.getAttribute("data-port-name"),
);
});
test("放大画面后接触吸附仍保留足够的画布判定半径", async ({ page }) => {
@@ -549,6 +549,82 @@ test("正交连接线可拖动内部线段,且无连接交叉点显示跨线
expect(savedRoute?.length).toBeGreaterThan(1);
});
test("缩小画布后连接线端点仍与端口图标边缘对齐", async ({ page }) => {
await page.addInitScript((storedProject) => {
window.localStorage.setItem(
"system-simulation-flow:project:demo-system",
JSON.stringify(storedProject),
);
}, wideProject);
await page.goto("/");
await page.getByRole("button", { name: "加载工程", exact: true }).click();
const viewport = page.locator(".flow-canvas .react-flow__viewport");
const readViewportZoom = () =>
viewport.evaluate((element) => {
const values =
getComputedStyle(element)
.transform.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)
?.map(Number) ?? [];
return values[0] ?? 1;
});
const initialZoom = await readViewportZoom();
const pane = page.locator(".flow-canvas .react-flow__pane");
const paneBounds = await pane.boundingBox();
expect(paneBounds).not.toBeNull();
await page.mouse.move(
paneBounds!.x + paneBounds!.width / 2,
paneBounds!.y + paneBounds!.height / 2,
);
await page.keyboard.down("Control");
await page.mouse.wheel(0, 720);
await page.keyboard.up("Control");
await expect
.poll(async () => (await readViewportZoom()) / initialZoom)
.toBeLessThan(0.7);
const zoom = await readViewportZoom();
const alignment = await page.evaluate(() => {
const path = document.querySelector<SVGPathElement>(
'.flow-canvas .react-flow__edge[data-id="edge-1"] .react-flow__edge-path',
);
const source = document.querySelector<HTMLElement>(
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"] .port-handle[data-port-name="port_b"]',
);
const target = document.querySelector<HTMLElement>(
'.flow-canvas .react-flow__node[data-id="generic_sensor_2"] .port-handle[data-port-name="port_a"]',
);
const matrix = path?.getScreenCTM();
if (!path || !source || !target || !matrix) {
return null;
}
const start = path.getPointAtLength(0).matrixTransform(matrix);
const end = path.getPointAtLength(path.getTotalLength()).matrixTransform(matrix);
const sourceBounds = source.getBoundingClientRect();
const targetBounds = target.getBoundingClientRect();
return {
sourceGapX: Math.abs(start.x - sourceBounds.right),
sourceGapY: Math.abs(
start.y - (sourceBounds.top + sourceBounds.height / 2),
),
sourceWidth: sourceBounds.width,
targetGapX: Math.abs(end.x - targetBounds.left),
targetGapY: Math.abs(
end.y - (targetBounds.top + targetBounds.height / 2),
),
targetWidth: targetBounds.width,
};
});
expect(alignment).not.toBeNull();
expect(Math.abs(alignment!.sourceWidth - 6 * zoom)).toBeLessThan(0.1);
expect(Math.abs(alignment!.targetWidth - 6 * zoom)).toBeLessThan(0.1);
expect(alignment!.sourceGapX).toBeLessThan(0.75);
expect(alignment!.sourceGapY).toBeLessThan(0.75);
expect(alignment!.targetGapX).toBeLessThan(0.75);
expect(alignment!.targetGapY).toBeLessThan(0.75);
});
test("直线首次折弯后即使原手柄卸载,拖动仍连续且保持正交", async ({
page,
}) => {
+71 -10
View File
@@ -257,18 +257,18 @@ test("普通滚轮纵向平移,Ctrl 滚轮缩放且滚动条和中键拖动保
await expect
.poll(async () => (await readModelingViewport(page)).zoom)
.not.toBeCloseTo(beforeZoom.zoom, 2);
const horizontalAfterZoom = await horizontalScrollbar.evaluate(
(element) => element.scrollLeft,
);
const verticalAfterZoom = await verticalScrollbar.evaluate(
(element) => element.scrollTop,
);
expect(
Math.hypot(
await expect.poll(async () => {
const horizontalAfterZoom = await horizontalScrollbar.evaluate(
(element) => element.scrollLeft,
);
const verticalAfterZoom = await verticalScrollbar.evaluate(
(element) => element.scrollTop,
);
return Math.hypot(
horizontalAfterZoom - horizontalBeforeZoom,
verticalAfterZoom - verticalBeforeZoom,
),
).toBeGreaterThan(1);
);
}).toBeGreaterThan(1);
const beforeMiddleDrag = await readModelingViewport(page);
await page.mouse.move(pointer.x, pointer.y);
@@ -404,6 +404,67 @@ test("导入大型工程 JSON 后非接触连线端点与端口保持对齐", as
});
test("大型工程连续缩放时滚动条不逐帧重排且停稳后同步", async ({ page }) => {
await prepareApp(page);
await page.goto("/");
await page.locator('input[type="file"]').setInputFiles(MQL_8_PROJECT_PATH);
await expectAllNodesInsideCanvas(page, ".flow-canvas", mql8Project.nodes.length);
await expect(page.getByRole("scrollbar", { name: "水平滚动画布" }))
.toHaveAttribute("aria-valuemax", /[1-9]/);
const changes = await page.evaluate(async () => {
const canvas = document.querySelector<HTMLElement>(".flow-canvas")!;
const pane = canvas.querySelector<HTMLElement>(".react-flow__pane")!;
const viewport = canvas.querySelector<HTMLElement>(".react-flow__viewport")!;
const scrollbars = canvas.querySelector<HTMLElement>(".flow-viewport-scrollbars")!;
const bounds = canvas.getBoundingClientRect();
let viewportChanges = 0;
let scrollbarChanges = 0;
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.target === viewport) viewportChanges++;
if (scrollbars.contains(mutation.target)) scrollbarChanges++;
}
});
observer.observe(viewport, { attributes: true, attributeFilter: ["style"] });
observer.observe(scrollbars, {
attributes: true, attributeFilter: ["style"], subtree: true,
});
// 连续输入覆盖整个手势,避免驱动侧等待导致其被拆成多个独立缩放。
for (let index = 0; index < 12; index++) {
pane.dispatchEvent(new WheelEvent("wheel", {
bubbles: true, cancelable: true, ctrlKey: true, deltaY: -8,
clientX: bounds.x + bounds.width / 2,
clientY: bounds.y + bounds.height / 2,
}));
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
}
observer.disconnect();
return { viewportChanges, scrollbarChanges };
});
expect(changes.viewportChanges).toBeGreaterThan(5);
expect(changes.scrollbarChanges).toBeLessThan(changes.viewportChanges / 2);
await expect.poll(async () => {
const viewport = await readModelingViewport(page);
const scale = await page.locator(".flow-canvas").evaluate((canvas) =>
Number(canvas.style.getPropertyValue("--port-handle-hit-scale")),
);
return Math.abs(scale * viewport.zoom - 1);
}).toBeLessThan(0.01);
const alignment = await readVisibleEdgeEndpointAlignment(page, mql8Project.edges);
expect(alignment.missingHandles).toEqual([]);
expect(alignment.checkedEdgeCount).toBeGreaterThan(20);
expect(alignment.maxDistance).toBeLessThanOrEqual(4);
const zoom = (await readModelingViewport(page)).zoom;
const horizontal = page.getByRole("scrollbar", { name: "水平滚动画布" });
await horizontal.focus();
const before = await readModelingViewport(page);
await horizontal.press("ArrowRight");
await expect.poll(async () => (await readModelingViewport(page)).x).toBeLessThan(before.x);
expect((await readModelingViewport(page)).zoom).toBeCloseTo(zoom, 5);
});
test("恢复自动保存工程后自动适配建模画布", async ({ page }) => {
await prepareApp(page);
await page.addInitScript((project) => {