完善端口标号与MECMAS21动态图标
This commit is contained in:
1 parent
df1d676131
commit
09778972a6
7 files changed
+780
-60
No files matched your search
@@ -96,6 +96,20 @@ function portsForSymbol(modelType: string) {
|
||||
if (modelType === "amesim_step0") {
|
||||
return [signalPort("out", "right", "output", 10)];
|
||||
}
|
||||
if (modelType === "amesim_mecmas21") {
|
||||
return [
|
||||
mechanicalPort("port_2", "left", 10),
|
||||
mechanicalPort("port_1", "right", 20),
|
||||
];
|
||||
}
|
||||
if (modelType === "amesim_lmechn1") {
|
||||
return [
|
||||
...Array.from({ length: 8 }, (_, index) =>
|
||||
mechanicalPort(`port_${index + 1}`, "left", (index + 1) * 10),
|
||||
),
|
||||
mechanicalPort("port_9", "right", 90),
|
||||
];
|
||||
}
|
||||
if (modelType === "amesim_pnrp17") {
|
||||
return [
|
||||
physicalPort("port_1", "left", 10),
|
||||
@@ -162,6 +176,103 @@ async function expectSymbolAnchors(
|
||||
}
|
||||
}
|
||||
|
||||
async function expectPortIndexLabels(
|
||||
node: Locator,
|
||||
expected: Record<string, string>,
|
||||
) {
|
||||
const labels = node.locator(".port-index-label");
|
||||
await expect(labels).toHaveCount(Object.keys(expected).length);
|
||||
for (const [portName, text] of Object.entries(expected)) {
|
||||
const label = node.locator(
|
||||
`.port-index-label[data-port-name="${portName}"]`,
|
||||
);
|
||||
await expect(label).toHaveCount(1);
|
||||
await expect(label).toHaveText(text);
|
||||
await expect(label).toHaveAttribute("data-port-index", text);
|
||||
}
|
||||
const firstLabel = labels.first();
|
||||
await expect(firstLabel).toHaveCSS("font-size", "18px");
|
||||
await expect(firstLabel).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
||||
await expect(firstLabel).toHaveCSS("border-top-style", "none");
|
||||
await expect(firstLabel).toHaveCSS("box-shadow", "none");
|
||||
await expect(firstLabel).toHaveCSS("pointer-events", "none");
|
||||
}
|
||||
|
||||
async function portIndexLabelLayout(node: Locator) {
|
||||
return node.evaluate((element) => {
|
||||
const labels = Array.from(
|
||||
element.querySelectorAll<HTMLElement>(".port-index-label"),
|
||||
).map((label) => {
|
||||
const portName = label.dataset.portName ?? "";
|
||||
const handle = element.querySelector<HTMLElement>(
|
||||
`.port-handle[data-port-name="${portName}"]`,
|
||||
);
|
||||
const bounds = label.getBoundingClientRect();
|
||||
const handleBounds = handle?.getBoundingClientRect();
|
||||
const labelCenter = {
|
||||
x: bounds.left + bounds.width / 2,
|
||||
y: bounds.top + bounds.height / 2,
|
||||
};
|
||||
const handleCenter = handleBounds
|
||||
? {
|
||||
x: handleBounds.left + handleBounds.width / 2,
|
||||
y: handleBounds.top + handleBounds.height / 2,
|
||||
}
|
||||
: null;
|
||||
const outward =
|
||||
handle && handleCenter
|
||||
? handle.classList.contains("react-flow__handle-left")
|
||||
? labelCenter.x < handleCenter.x
|
||||
: handle.classList.contains("react-flow__handle-right")
|
||||
? labelCenter.x > handleCenter.x
|
||||
: handle.classList.contains("react-flow__handle-top")
|
||||
? labelCenter.y < handleCenter.y
|
||||
: labelCenter.y > handleCenter.y
|
||||
: false;
|
||||
return {
|
||||
bounds: {
|
||||
bottom: bounds.bottom,
|
||||
left: bounds.left,
|
||||
right: bounds.right,
|
||||
top: bounds.top,
|
||||
},
|
||||
distance: handleBounds
|
||||
? Math.hypot(
|
||||
labelCenter.x - handleCenter!.x,
|
||||
labelCenter.y - handleCenter!.y,
|
||||
)
|
||||
: Number.POSITIVE_INFINITY,
|
||||
outward,
|
||||
};
|
||||
});
|
||||
let overlaps = 0;
|
||||
for (let firstIndex = 0; firstIndex < labels.length; firstIndex += 1) {
|
||||
for (
|
||||
let secondIndex = firstIndex + 1;
|
||||
secondIndex < labels.length;
|
||||
secondIndex += 1
|
||||
) {
|
||||
const first = labels[firstIndex].bounds;
|
||||
const second = labels[secondIndex].bounds;
|
||||
if (
|
||||
Math.min(first.right, second.right) >
|
||||
Math.max(first.left, second.left) &&
|
||||
Math.min(first.bottom, second.bottom) >
|
||||
Math.max(first.top, second.top)
|
||||
) {
|
||||
overlaps += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
maximumDistance: Math.max(...labels.map((label) => label.distance)),
|
||||
minimumDistance: Math.min(...labels.map((label) => label.distance)),
|
||||
overlaps,
|
||||
outwardViolations: labels.filter((label) => !label.outward).length,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function locatorCenter(locator: Locator) {
|
||||
const bounds = await locator.boundingBox();
|
||||
expect(bounds).not.toBeNull();
|
||||
@@ -273,7 +384,43 @@ test.beforeEach(async ({ page }) => {
|
||||
order: Object.keys(categoryLabels).indexOf(item.category) + 1,
|
||||
},
|
||||
ports: portsForSymbol(item.modelType),
|
||||
parameters: [],
|
||||
parameters:
|
||||
item.modelType === "amesim_mecmas21"
|
||||
? [
|
||||
{
|
||||
name: "useFriction",
|
||||
label: "启用摩擦",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
maximum: 2,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "否" },
|
||||
{ value: 2, label: "是" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "stoptype",
|
||||
label: "限位类型",
|
||||
quantity: "dimensionless",
|
||||
unit: "",
|
||||
default: 4,
|
||||
minimum: 1,
|
||||
maximum: 4,
|
||||
minimumExclusive: false,
|
||||
editor: "choice",
|
||||
options: [
|
||||
{ value: 1, label: "理想限位" },
|
||||
{ value: 2, label: "弹性限位" },
|
||||
{ value: 3, label: "恢复碰撞" },
|
||||
{ value: 4, label: "无限位" },
|
||||
],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
})),
|
||||
},
|
||||
],
|
||||
@@ -302,6 +449,136 @@ test("全部已移植 AMESim 模型使用专属图标及正确领域配色", asy
|
||||
}
|
||||
});
|
||||
|
||||
test("MECMAS21 图标按摩擦和限位参数切换四种参考外观", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const node = await dragSymbolToCanvas(page, "amesim_mecmas21", {
|
||||
x: 430,
|
||||
y: 300,
|
||||
});
|
||||
const symbol = node.locator(
|
||||
'[data-component-symbol="amesim_mecmas21"]',
|
||||
);
|
||||
const state = symbol.locator("[data-mecmas-variant]");
|
||||
const core = symbol.locator('[data-mecmas-part="core"]');
|
||||
const envelope = node.locator('[data-symbol-envelope="true"]');
|
||||
const useFriction = page.getByLabel("启用摩擦", { exact: true });
|
||||
const stoptype = page.getByLabel("限位类型", { exact: true });
|
||||
const envelopeHeight = async () =>
|
||||
(await envelope.boundingBox())?.height ?? 0;
|
||||
|
||||
await expectSymbolAnchors(node, [
|
||||
{ portName: "port_2", x: 2, y: 24 },
|
||||
{ portName: "port_1", x: 62, y: 24 },
|
||||
]);
|
||||
|
||||
const expectVariant = async ({
|
||||
friction,
|
||||
guides,
|
||||
limits,
|
||||
lowerGuide,
|
||||
variant,
|
||||
}: {
|
||||
friction: "disabled" | "enabled";
|
||||
guides: boolean;
|
||||
limits: "finite" | "unlimited";
|
||||
lowerGuide: "finite" | "none" | "unlimited";
|
||||
variant: string;
|
||||
}) => {
|
||||
await expect(state).toHaveAttribute("data-mecmas-variant", variant);
|
||||
await expect(state).toHaveAttribute("data-mecmas-friction", friction);
|
||||
await expect(state).toHaveAttribute("data-mecmas-limits", limits);
|
||||
await expect(
|
||||
symbol.locator('[data-mecmas-part="friction-markers"]'),
|
||||
).toHaveCount(friction === "enabled" ? 1 : 0);
|
||||
await expect(symbol.locator('[data-mecmas-part="guides"]')).toHaveCount(
|
||||
guides ? 1 : 0,
|
||||
);
|
||||
await expect(
|
||||
symbol.locator('[data-mecmas-part="finite-lower-guide"]'),
|
||||
).toHaveCount(lowerGuide === "finite" ? 1 : 0);
|
||||
await expect(
|
||||
symbol.locator('[data-mecmas-part="unlimited-lower-guide"]'),
|
||||
).toHaveCount(lowerGuide === "unlimited" ? 1 : 0);
|
||||
await expect(symbol.locator("svg")).toHaveAttribute("viewBox", "0 0 64 48");
|
||||
};
|
||||
|
||||
await expectVariant({
|
||||
friction: "disabled",
|
||||
guides: false,
|
||||
limits: "unlimited",
|
||||
lowerGuide: "none",
|
||||
variant: "plain-unlimited",
|
||||
});
|
||||
await expect.poll(envelopeHeight).toBeLessThan(60);
|
||||
const fixedCore = await core.evaluate((element: SVGGElement) => {
|
||||
const bounds = element.getBBox();
|
||||
return {
|
||||
bounds: {
|
||||
height: bounds.height,
|
||||
width: bounds.width,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
},
|
||||
markup: element.innerHTML,
|
||||
};
|
||||
});
|
||||
const compactEnvelopeHeight = await envelopeHeight();
|
||||
|
||||
await useFriction.selectOption("2");
|
||||
await expectVariant({
|
||||
friction: "enabled",
|
||||
guides: true,
|
||||
limits: "unlimited",
|
||||
lowerGuide: "unlimited",
|
||||
variant: "friction-unlimited",
|
||||
});
|
||||
await expect.poll(envelopeHeight).toBeGreaterThan(compactEnvelopeHeight + 20);
|
||||
|
||||
await stoptype.selectOption("2");
|
||||
await expectVariant({
|
||||
friction: "enabled",
|
||||
guides: true,
|
||||
limits: "finite",
|
||||
lowerGuide: "finite",
|
||||
variant: "friction-limited",
|
||||
});
|
||||
|
||||
await useFriction.selectOption("1");
|
||||
await expectVariant({
|
||||
friction: "disabled",
|
||||
guides: true,
|
||||
limits: "finite",
|
||||
lowerGuide: "finite",
|
||||
variant: "plain-limited",
|
||||
});
|
||||
|
||||
await stoptype.selectOption("4");
|
||||
await expectVariant({
|
||||
friction: "disabled",
|
||||
guides: false,
|
||||
limits: "unlimited",
|
||||
lowerGuide: "none",
|
||||
variant: "plain-unlimited",
|
||||
});
|
||||
await expect.poll(envelopeHeight).toBeLessThan(compactEnvelopeHeight + 1);
|
||||
expect(await envelopeHeight()).toBeCloseTo(compactEnvelopeHeight, 1);
|
||||
expect(
|
||||
await core.evaluate((element: SVGGElement) => {
|
||||
const bounds = element.getBBox();
|
||||
return {
|
||||
bounds: {
|
||||
height: bounds.height,
|
||||
width: bounds.width,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
},
|
||||
markup: element.innerHTML,
|
||||
};
|
||||
}),
|
||||
).toEqual(fixedCore);
|
||||
});
|
||||
|
||||
test("PNRP17 图标匹配参考结构并将五个端口放在对应接口", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
@@ -329,6 +606,89 @@ test("PNRP17 图标匹配参考结构并将五个端口放在对应接口", asyn
|
||||
).toHaveClass(/react-flow__handle-bottom/);
|
||||
});
|
||||
|
||||
test("选中元件时按接口编号显示固定且不重叠的数字标记", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
const mecmas = await dragSymbolToCanvas(page, "amesim_mecmas21", {
|
||||
x: 260,
|
||||
y: 220,
|
||||
});
|
||||
await expectPortIndexLabels(mecmas, { port_1: "1", port_2: "2" });
|
||||
let layout = await portIndexLabelLayout(mecmas);
|
||||
expect(layout.overlaps).toBe(0);
|
||||
expect(layout.outwardViolations).toBe(0);
|
||||
expect(layout.minimumDistance).toBeGreaterThan(18);
|
||||
expect(layout.maximumDistance).toBeLessThan(52);
|
||||
|
||||
const signalOrifice = await dragSymbolToCanvas(page, "amesim_pnvo001", {
|
||||
x: 520,
|
||||
y: 220,
|
||||
});
|
||||
await expect(mecmas.locator(".port-index-label")).toHaveCount(0);
|
||||
await expectPortIndexLabels(signalOrifice, {
|
||||
res: "1",
|
||||
port_2: "2",
|
||||
port_3: "3",
|
||||
});
|
||||
layout = await portIndexLabelLayout(signalOrifice);
|
||||
expect(layout.overlaps).toBe(0);
|
||||
expect(layout.outwardViolations).toBe(0);
|
||||
expect(layout.maximumDistance).toBeLessThan(52);
|
||||
|
||||
const multiPortNode = await dragSymbolToCanvas(page, "amesim_lmechn1", {
|
||||
x: 780,
|
||||
y: 300,
|
||||
});
|
||||
await expect(signalOrifice.locator(".port-index-label")).toHaveCount(0);
|
||||
await expectPortIndexLabels(
|
||||
multiPortNode,
|
||||
Object.fromEntries(
|
||||
Array.from({ length: 9 }, (_, index) => [
|
||||
`port_${index + 1}`,
|
||||
String(index + 1),
|
||||
]),
|
||||
),
|
||||
);
|
||||
layout = await portIndexLabelLayout(multiPortNode);
|
||||
expect(layout.overlaps).toBe(0);
|
||||
expect(layout.outwardViolations).toBe(0);
|
||||
expect(layout.minimumDistance).toBeGreaterThan(18);
|
||||
expect(layout.maximumDistance).toBeLessThan(52);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true })
|
||||
.click();
|
||||
await page
|
||||
.getByRole("button", { name: "水平镜像 (Shift+H)", exact: true })
|
||||
.click();
|
||||
await expectPortIndexLabels(
|
||||
multiPortNode,
|
||||
Object.fromEntries(
|
||||
Array.from({ length: 9 }, (_, index) => [
|
||||
`port_${index + 1}`,
|
||||
String(index + 1),
|
||||
]),
|
||||
),
|
||||
);
|
||||
layout = await portIndexLabelLayout(multiPortNode);
|
||||
expect(layout.overlaps).toBe(0);
|
||||
expect(layout.outwardViolations).toBe(0);
|
||||
expect(layout.maximumDistance).toBeLessThan(52);
|
||||
|
||||
const outsideLabelBounds = await multiPortNode
|
||||
.locator('.port-index-label[data-port-name="port_9"]')
|
||||
.boundingBox();
|
||||
expect(outsideLabelBounds).not.toBeNull();
|
||||
await page.mouse.click(
|
||||
outsideLabelBounds!.x + outsideLabelBounds!.width / 2,
|
||||
outsideLabelBounds!.y + outsideLabelBounds!.height / 2,
|
||||
);
|
||||
await expect(multiPortNode).not.toHaveClass(/selected/);
|
||||
await expect(multiPortNode.locator(".port-index-label")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("AMESim canvas nodes use icon anchors and highlight only compatible free ports", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in new issue
Block a user