前端进度条性能优化、仿真结束后后处理优化;后端C代码生成流程优化:先识别来源,再按照已知未知量需求排序,最后局部求解
This commit is contained in:
1 parent
0dcb465d84
commit
91bd9fb252
77 files changed
+11223
-551
No files matched your search
@@ -0,0 +1,271 @@
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||
import { Position } from "@xyflow/react";
|
||||
import { componentCatalog, expectAllNodesInsideCanvas, prepareApp, wideProject } from "./fixtures";
|
||||
import { confirmOrthogonalCorner, extendOrthogonalRoute } from "../../src/edgeRouting";
|
||||
|
||||
const PROJECT_KEY = "system-simulation-flow:project:demo-system";
|
||||
|
||||
test("每次左键只确认一段并转向,空白预览不插入自动中点", () => {
|
||||
const source = { x: 0, y: 0 };
|
||||
const pointer = { x: 120, y: 65 };
|
||||
const fixed = confirmOrthogonalCorner([source], pointer, Position.Right);
|
||||
expect(fixed).toEqual([source, { x: 120, y: 0 }]);
|
||||
expect(extendOrthogonalRoute([source], pointer, Position.Right)).toEqual([
|
||||
source, { x: 120, y: 0 }, pointer,
|
||||
]);
|
||||
expect(extendOrthogonalRoute(fixed, pointer, Position.Right)).toEqual([
|
||||
source, { x: 120, y: 0 }, pointer,
|
||||
]);
|
||||
expect(confirmOrthogonalCorner(fixed, { x: 200, y: 95 }, Position.Right)).toEqual([
|
||||
source, { x: 120, y: 0 }, { x: 120, y: 95 },
|
||||
]);
|
||||
expect(confirmOrthogonalCorner(fixed, { x: 300, y: 0 }, Position.Right)).toBe(fixed);
|
||||
expect(confirmOrthogonalCorner(fixed, { x: NaN, y: 65 }, Position.Right)).toBe(fixed);
|
||||
expect(confirmOrthogonalCorner([source], pointer, Position.Bottom)).toEqual([
|
||||
source, { x: 0, y: 65 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("各方向端口收尾只调整未确认末段,并保持正交", () => {
|
||||
for (const sourcePosition of Object.values(Position)) {
|
||||
for (const targetPosition of Object.values(Position)) {
|
||||
const fixed = confirmOrthogonalCorner([{ x: 0, y: 0 }], { x: 100, y: 80 }, sourcePosition);
|
||||
const target = { x: 270, y: 230 };
|
||||
const route = extendOrthogonalRoute(fixed, target, sourcePosition, targetPosition);
|
||||
expect(route.slice(0, fixed.length)).toEqual(fixed);
|
||||
expect(route.at(-1)).toEqual(target);
|
||||
for (let i = 1; i < route.length; i++) {
|
||||
expect(route[i].x === route[i - 1].x || route[i].y === route[i - 1].y).toBe(true);
|
||||
expect(route[i]).not.toEqual(route[i - 1]);
|
||||
}
|
||||
const previous = route.at(-2)!;
|
||||
if (targetPosition === Position.Left || targetPosition === Position.Right) {
|
||||
expect(previous.y).toBe(target.y);
|
||||
} else {
|
||||
expect(previous.x).toBe(target.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function loadRoutingProject(page: Page, rotation = 0, reverseSignal = false) {
|
||||
await prepareApp(page);
|
||||
if (reverseSignal) {
|
||||
const catalog = structuredClone(componentCatalog);
|
||||
const definition = catalog.libraries.flatMap((library) => library.components)
|
||||
.find((component) => component.type === "generic_sensor")!;
|
||||
definition.ports = definition.ports.map((port) => ({
|
||||
...port, kind: "signal", domain: "signal",
|
||||
nominalRole: port.side === "left" ? "input" : "output",
|
||||
}));
|
||||
await page.route("**/api/components/catalog", (route) => route.fulfill({
|
||||
contentType: "application/json", body: JSON.stringify(catalog),
|
||||
}));
|
||||
}
|
||||
const nodes = [
|
||||
["origin", 0, 0], ["target", 650, 0], ["obstacle", 280, 260],
|
||||
["edge-source", 0, 450], ["edge-target", 650, 450],
|
||||
].map(([id, x, y]) => ({
|
||||
...structuredClone(wideProject.nodes[0]), id: String(id),
|
||||
position: { x: Number(x), y: Number(y) },
|
||||
data: { ...structuredClone(wideProject.nodes[0].data), label: String(id),
|
||||
rotation: id === "origin" ? rotation : 0 },
|
||||
}));
|
||||
const project = { ...structuredClone(wideProject), nodes, edges: [{
|
||||
...wideProject.edges[0], id: "existing-edge",
|
||||
source: "edge-source", target: "edge-target",
|
||||
}] };
|
||||
await page.addInitScript(({ key, value }) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}, { key: PROJECT_KEY, value: project });
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(5);
|
||||
await expectAllNodesInsideCanvas(page, ".flow-canvas", 5);
|
||||
const output = page.locator('.react-flow__node[data-id="origin"] .port-handle[data-port-name="port_b"]');
|
||||
const input = page.locator('.react-flow__node[data-id="target"] .port-handle[data-port-name="port_a"]');
|
||||
return {
|
||||
origin: reverseSignal ? input : output,
|
||||
target: reverseSignal ? output : input,
|
||||
draft: page.locator(".manual-connection-draft .react-flow__connection-path"),
|
||||
};
|
||||
}
|
||||
|
||||
async function center(locator: Locator) {
|
||||
const box = await locator.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
return { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 };
|
||||
}
|
||||
|
||||
async function pathPoints(path: Locator) {
|
||||
const values = (await path.getAttribute("d"))!.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)!.map(Number);
|
||||
return Array.from({ length: values.length / 2 }, (_, index) => ({
|
||||
x: values[index * 2], y: values[index * 2 + 1],
|
||||
}));
|
||||
}
|
||||
|
||||
for (const { rotation, reverseSignal } of [
|
||||
{ rotation: 0, reverseSignal: false },
|
||||
{ rotation: 90, reverseSignal: false },
|
||||
{ rotation: 0, reverseSignal: true },
|
||||
]) {
|
||||
test(`逐段定转折后已确认部分保持固定(${reverseSignal ? "信号输入端反向起画" : `旋转 ${rotation}°`})`, async ({ page }) => {
|
||||
const { origin, target, draft } = await loadRoutingProject(page, rotation, reverseSignal);
|
||||
const start = await center(origin);
|
||||
const waypoint = {
|
||||
x: Math.round(start.x + (reverseSignal ? -120 : 120)),
|
||||
y: Math.round(start.y + 65),
|
||||
};
|
||||
await origin.click();
|
||||
await page.mouse.move(waypoint.x, waypoint.y);
|
||||
await expect(draft).toBeVisible();
|
||||
const beforeClick = await pathPoints(draft);
|
||||
// 初始为 L 形橡皮筋,一次点击只确认第一个转角,不固定末端。
|
||||
expect(beforeClick).toHaveLength(3);
|
||||
const firstFixed = beforeClick.slice(0, -1);
|
||||
await page.mouse.click(waypoint.x, waypoint.y);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "1");
|
||||
expect(await pathPoints(draft)).toEqual(beforeClick);
|
||||
|
||||
for (const offset of [{x: 100, y: 30}, {x: -45, y: 110}, {x: 90, y: -55}]) {
|
||||
await page.mouse.move(waypoint.x + offset.x, waypoint.y + offset.y);
|
||||
await expect.poll(async () => (await pathPoints(draft)).slice(0, firstFixed.length)).toEqual(firstFixed);
|
||||
}
|
||||
const secondPoint = { x: waypoint.x + 90, y: waypoint.y - 55 };
|
||||
const secondPreview = await pathPoints(draft);
|
||||
const secondFixed = secondPreview.slice(0, -1);
|
||||
await page.mouse.click(secondPoint.x, secondPoint.y);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "2");
|
||||
expect(await pathPoints(draft)).toEqual(secondPreview);
|
||||
await page.mouse.move(secondPoint.x + 40, secondPoint.y + 110);
|
||||
await expect.poll(async () => (await pathPoints(draft)).slice(0, secondFixed.length)).toEqual(secondFixed);
|
||||
|
||||
await target.hover();
|
||||
const completedPreview = await pathPoints(draft);
|
||||
expect(completedPreview.slice(0, secondFixed.length)).toEqual(secondFixed);
|
||||
await target.click();
|
||||
await expect(draft).toHaveCount(0);
|
||||
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2);
|
||||
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||
const persisted = await page.evaluate((key) => {
|
||||
const project = JSON.parse(localStorage.getItem(key)!);
|
||||
return project.edges.find((edge: { id: string }) => edge.id !== "existing-edge").data.routePoints;
|
||||
}, PROJECT_KEY);
|
||||
expect(persisted).toEqual((reverseSignal ? completedPreview.reverse() : completedPreview).slice(1, -1));
|
||||
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2);
|
||||
const finalizedPath = page.locator('.flow-canvas .react-flow__edge:not([data-id="existing-edge"]) .react-flow__edge-path');
|
||||
const maximumDeviation = await finalizedPath.evaluate((path: SVGPathElement, fixed) => {
|
||||
const samples = Array.from({ length: Math.ceil(path.getTotalLength()) + 1 }, (_, index) =>
|
||||
path.getPointAtLength(index),
|
||||
);
|
||||
return Math.max(...fixed.map((point) =>
|
||||
Math.min(...samples.map((sample) => Math.hypot(sample.x - point.x, sample.y - point.y))),
|
||||
));
|
||||
}, secondFixed.slice(1));
|
||||
expect(maximumDeviation).toBeLessThan(1);
|
||||
});
|
||||
}
|
||||
|
||||
test("右键逐段退回并恢复绘制方向,退到起点后再次右键取消", async ({ page }) => {
|
||||
const { origin, target, draft } = await loadRoutingProject(page);
|
||||
const start = await center(origin);
|
||||
await origin.click();
|
||||
await page.mouse.click(start.x + 120, start.y + 65);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "1");
|
||||
const firstFixed = (await pathPoints(draft)).slice(0, 2);
|
||||
await page.mouse.click(start.x + 200, start.y + 120);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "2");
|
||||
await page.mouse.click(start.x + 250, start.y + 180, { button: "right" });
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "1");
|
||||
const afterUndo = await pathPoints(draft);
|
||||
expect(afterUndo.slice(0, 2)).toEqual(firstFixed);
|
||||
expect(afterUndo[2].x).toBe(firstFixed[1].x); // 恢复为纵向绘制
|
||||
await page.mouse.click(start.x + 250, start.y + 180, { button: "right" });
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "0");
|
||||
const atStart = await pathPoints(draft);
|
||||
expect(atStart[1].y).toBe(atStart[0].y); // 恢复起始端口的横向
|
||||
await page.mouse.click(start.x + 250, start.y + 180, { button: "right" });
|
||||
await expect(draft).toHaveCount(0);
|
||||
await expect(origin).toHaveAttribute("data-connection-state", "idle");
|
||||
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
|
||||
|
||||
// 取消的草稿不生成线路,下一次连接不受 React Flow 的点击状态干扰。
|
||||
await origin.click();
|
||||
await target.click();
|
||||
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2);
|
||||
await expect(draft).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("绘线时缩放、滚动条和中键平移不会确认额外转角或改变已确认路径", async ({ page }) => {
|
||||
const { origin, target, draft } = await loadRoutingProject(page);
|
||||
const start = await center(origin);
|
||||
await origin.click();
|
||||
await page.mouse.click(start.x + 120, start.y + 65);
|
||||
const fixed = (await pathPoints(draft)).slice(0, 2);
|
||||
const viewport = page.locator(".flow-canvas .react-flow__viewport");
|
||||
const beforeZoom = await viewport.getAttribute("style");
|
||||
await page.keyboard.down("Control");
|
||||
await page.mouse.wheel(0, 240);
|
||||
await page.keyboard.up("Control");
|
||||
await expect(viewport).not.toHaveAttribute("style", beforeZoom!);
|
||||
expect((await pathPoints(draft)).slice(0, 2)).toEqual(fixed);
|
||||
|
||||
const scrollbar = page.getByRole("scrollbar", { name: "水平滚动画布" });
|
||||
const beforeScroll = await viewport.getAttribute("style");
|
||||
await scrollbar.focus();
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await expect(viewport).not.toHaveAttribute("style", beforeScroll!);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "1");
|
||||
expect((await pathPoints(draft)).slice(0, 2)).toEqual(fixed);
|
||||
|
||||
const beforePan = await viewport.getAttribute("style");
|
||||
await page.mouse.move(start.x + 180, start.y + 180);
|
||||
await page.mouse.down({ button: "middle" });
|
||||
await page.mouse.move(start.x + 220, start.y + 200, { steps: 4 });
|
||||
await page.mouse.up({ button: "middle" });
|
||||
await expect(viewport).not.toHaveAttribute("style", beforePan!);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "1");
|
||||
expect((await pathPoints(draft)).slice(0, 2)).toEqual(fixed);
|
||||
await target.click();
|
||||
await expect(draft).toHaveCount(0);
|
||||
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2);
|
||||
});
|
||||
|
||||
test("连接模式点击元件和已有连线只添加折点,取消后恢复选择", async ({ page }) => {
|
||||
const { origin, target, draft } = await loadRoutingProject(page);
|
||||
const obstacle = page.locator('.react-flow__node[data-id="obstacle"]');
|
||||
const obstacleCenter = await center(obstacle.locator(".sim-node"));
|
||||
const edge = page.locator('.react-flow__edge[data-id="existing-edge"]');
|
||||
const edgeCenter = await edge.locator(".react-flow__edge-path").evaluate((path: SVGPathElement) => {
|
||||
const p = path.getPointAtLength(path.getTotalLength() / 2).matrixTransform(path.getScreenCTM()!);
|
||||
return { x: p.x, y: p.y };
|
||||
});
|
||||
await page.mouse.click(edgeCenter.x, edgeCenter.y);
|
||||
await expect(edge).toHaveClass(/selected|editor-edge-selected/);
|
||||
await origin.click();
|
||||
await expect(origin).toHaveAttribute("data-connection-state", "origin");
|
||||
await page.mouse.click(obstacleCenter.x, obstacleCenter.y);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "1");
|
||||
await expect(obstacle).not.toHaveClass(/selected/);
|
||||
await page.mouse.click(edgeCenter.x, edgeCenter.y);
|
||||
await expect(draft).toHaveAttribute("data-waypoint-count", "2");
|
||||
await expect(edge).not.toHaveClass(/selected|editor-edge-selected/);
|
||||
await expect(page.locator(".manual-edge-segment-handle, .manual-edge-corner-handle, .manual-edge-route-handle")).toHaveCount(0);
|
||||
await expect(origin).toHaveAttribute("data-connection-state", "origin");
|
||||
await expect(target).toHaveAttribute("data-connection-state", "compatible");
|
||||
|
||||
const beforeDrag = await obstacle.getAttribute("style");
|
||||
await page.mouse.move(obstacleCenter.x, obstacleCenter.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(obstacleCenter.x + 35, obstacleCenter.y + 25, { steps: 3 });
|
||||
await page.mouse.up();
|
||||
expect(await obstacle.getAttribute("style")).toBe(beforeDrag);
|
||||
await expect(page.locator(".react-flow__selection")).toHaveCount(0);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(draft).toHaveCount(0);
|
||||
await page.mouse.click(obstacleCenter.x, obstacleCenter.y);
|
||||
await expect(obstacle).toHaveClass(/selected/);
|
||||
await page.mouse.click(edgeCenter.x, edgeCenter.y);
|
||||
await expect(edge).toHaveClass(/selected|editor-edge-selected/);
|
||||
});
|
||||
Reference in new issue
Block a user