前端进度条性能优化、仿真结束后后处理优化;后端C代码生成流程优化:先识别来源,再按照已知未知量需求排序,最后局部求解

This commit is contained in:
ljz committed 2026-09-11 11:27:54 +08:00
1 parent 0dcb465d84
commit 91bd9fb252
77 files changed
+11223 -551

No files matched your search

+271
View File
@@ -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/);
});