diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b64e028..043853d 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1546,7 +1546,7 @@ function transformedPortPlacements(
return {
port,
- position: transformedPortPosition(side, rotation, mirrored),
+ position: transformedPortPosition(side, rotation, mirrored, anchor),
x: renderedX,
y: renderedY,
};
@@ -1571,12 +1571,28 @@ function transformedPortPosition(
side: PortSide,
rotation: NodeRotation,
mirrored: boolean,
+ anchor?: { x: number; y: number },
) {
let directionX = side === "left" ? -1 : 1;
+ let directionY = 0;
+
+ if (anchor) {
+ const closestEdge = [
+ { distance: anchor.x, x: -1, y: 0 },
+ { distance: SYMBOL_VIEWBOX_WIDTH - anchor.x, x: 1, y: 0 },
+ { distance: anchor.y, x: 0, y: -1 },
+ { distance: SYMBOL_VIEWBOX_HEIGHT - anchor.y, x: 0, y: 1 },
+ ].reduce((closest, candidate) =>
+ candidate.distance < closest.distance ? candidate : closest,
+ );
+ directionX = closestEdge.x;
+ directionY = closestEdge.y;
+ }
+
if (mirrored) {
directionX = -directionX;
}
- const [x, y] = rotatePoint(directionX, 0, rotation);
+ const [x, y] = rotatePoint(directionX, directionY, rotation);
if (x < -0.01) {
return Position.Left;
}
diff --git a/frontend/src/ComponentSymbol.tsx b/frontend/src/ComponentSymbol.tsx
index da1867c..530d7ff 100644
--- a/frontend/src/ComponentSymbol.tsx
+++ b/frontend/src/ComponentSymbol.tsx
@@ -3,6 +3,7 @@ import {
AmesimF000Symbol,
AmesimForcSymbol,
AmesimLmechn1Symbol,
+ AmesimPnrp17Symbol,
AmesimLstp00aSymbol,
AmesimMecmas21Symbol,
} from "./componentSymbols/mechanical";
@@ -194,6 +195,7 @@ const symbolRegistry = {
amesim_mecmas21: { presentation: "framed", render: AmesimMecmas21Symbol },
amesim_lstp00a: { presentation: "framed", render: AmesimLstp00aSymbol },
amesim_lmechn1: { presentation: "framed", render: AmesimLmechn1Symbol },
+ amesim_pnrp17: { presentation: "framed", render: AmesimPnrp17Symbol },
amesim_ideal_air_medium: {
presentation: "bare",
render: AmesimIdealAirMediumSymbol,
@@ -261,15 +263,15 @@ const symbolPortAnchors: Partial<
right: [{ x: 62, y: 24 }],
},
amesim_pnvo001_fixed: {
- left: [{ x: 2, y: 28 }],
- right: [{ x: 62, y: 28 }],
+ left: [{ x: 2, y: 24 }],
+ right: [{ x: 62, y: 24 }],
},
amesim_pnvo001: {
left: [
- { x: 2, y: 16 },
- { x: 2, y: 32 },
+ { x: 26, y: 46 },
+ { x: 2, y: 24 },
],
- right: [{ x: 62, y: 25 }],
+ right: [{ x: 62, y: 24 }],
},
amesim_pnl00r: {
left: [{ x: 2, y: 24 }],
@@ -321,6 +323,14 @@ const symbolPortAnchors: Partial<
left: [7, 12, 17, 22, 27, 32, 37, 42].map((y) => ({ x: 5, y })),
right: [{ x: 59, y: 24 }],
},
+ amesim_pnrp17: {
+ left: [
+ { x: 25.5, y: 48 },
+ { x: 2, y: 4.4 },
+ { x: 2, y: 24.1 },
+ ],
+ right: [{ x: 62, y: 4.4 }, { x: 62, y: 24.1 }],
+ },
};
function isRegisteredComponentSymbol(symbol: string): symbol is RegisteredComponentSymbol {
diff --git a/frontend/src/componentSymbols/mechanical.tsx b/frontend/src/componentSymbols/mechanical.tsx
index ffff848..7bb1ff4 100644
--- a/frontend/src/componentSymbols/mechanical.tsx
+++ b/frontend/src/componentSymbols/mechanical.tsx
@@ -7,6 +7,9 @@ import {
} from "./primitives";
const MECHANICAL_LINE_WIDTH = 1.8;
+const PNRP17_STROKE = "#8b134f";
+const PNRP17_FILL = "#cf9db6";
+const PNRP17_LIGHT_FILL = "#f5ebf0";
const mechanicalStroke = {
fill: "none",
@@ -215,3 +218,86 @@ export function AmesimLmechn1Symbol() {
);
}
+
+export function AmesimPnrp17Symbol() {
+ return (
+
+
+
+
+
+
+
+
+
+ {[15.3, 33].map((centerY) => (
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/componentSymbols/pneumatic.tsx b/frontend/src/componentSymbols/pneumatic.tsx
index 989da51..ccf3ec7 100644
--- a/frontend/src/componentSymbols/pneumatic.tsx
+++ b/frontend/src/componentSymbols/pneumatic.tsx
@@ -7,6 +7,7 @@ import {
} from "./primitives";
const PNEUMATIC_STROKE_WIDTH = 1.7;
+const PNVO001_STROKE = "#8b134f";
function OrificeBody({
centerY = 25,
@@ -49,6 +50,46 @@ export function AmesimPnor001Symbol() {
);
}
+function Pnvo001Body({
+ signalControlled = false,
+}: {
+ signalControlled?: boolean;
+}) {
+ return (
+ <>
+
+
+
+ {signalControlled ? (
+
+ ) : null}
+
+ >
+ );
+}
+
/** PNVO001 convenience variant whose opening is fixed by a parameter. */
export function AmesimPnvo001FixedSymbol() {
return (
@@ -56,14 +97,7 @@ export function AmesimPnvo001FixedSymbol() {
className="component-symbol-amesim-pnvo001-fixed-svg"
domain="pneumatic"
>
-
-
+
);
}
@@ -75,14 +109,7 @@ export function AmesimPnvo001SignalSymbol() {
className="component-symbol-amesim-pnvo001-signal-svg"
domain="pneumatic"
>
-
-
+
);
}
diff --git a/frontend/tests/e2e/component-symbols.spec.ts b/frontend/tests/e2e/component-symbols.spec.ts
index 9c5cea1..aad8850 100644
--- a/frontend/tests/e2e/component-symbols.spec.ts
+++ b/frontend/tests/e2e/component-symbols.spec.ts
@@ -28,6 +28,7 @@ const symbolCases: SymbolCase[] = [
{ category: "mechanical", domain: "mechanical", modelType: "amesim_mecmas21" },
{ category: "mechanical", domain: "mechanical", modelType: "amesim_lstp00a" },
{ category: "mechanical", domain: "mechanical", modelType: "amesim_lmechn1" },
+ { category: "mechanical", domain: "mechanical", modelType: "amesim_pnrp17" },
];
function physicalPort(
@@ -62,6 +63,22 @@ function signalPort(
};
}
+function mechanicalPort(
+ name: string,
+ side: "left" | "right",
+ order: number,
+) {
+ return {
+ name,
+ kind: "physical",
+ domain: "mechanical",
+ nominalRole: "bidirectional",
+ positiveFlowDirection: "intoComponent",
+ side,
+ order,
+ };
+}
+
function portsForSymbol(modelType: string) {
if (modelType === "amesim_pnor001") {
return [
@@ -79,6 +96,15 @@ function portsForSymbol(modelType: string) {
if (modelType === "amesim_step0") {
return [signalPort("out", "right", "output", 10)];
}
+ if (modelType === "amesim_pnrp17") {
+ return [
+ physicalPort("port_1", "left", 10),
+ mechanicalPort("port_3", "left", 20),
+ mechanicalPort("port_4", "left", 30),
+ mechanicalPort("port_2", "right", 40),
+ mechanicalPort("port_5", "right", 50),
+ ];
+ }
return [];
}
@@ -276,6 +302,33 @@ test("全部已移植 AMESim 模型使用专属图标及正确领域配色", asy
}
});
+test("PNRP17 图标匹配参考结构并将五个端口放在对应接口", async ({ page }) => {
+ await page.goto("/");
+
+ const paletteSymbol = page.locator(
+ 'aside.palette [data-component-symbol="amesim_pnrp17"]',
+ );
+ await expect(paletteSymbol).toHaveCount(1);
+ await expect(
+ paletteSymbol.locator('[stroke="#00af00"], [fill="#00af00"]'),
+ ).toHaveCount(0);
+
+ const piston = await dragSymbolToCanvas(page, "amesim_pnrp17", {
+ x: 430,
+ y: 300,
+ });
+ await expectSymbolAnchors(piston, [
+ { portName: "port_1", x: 25.5, y: 48 },
+ { portName: "port_3", x: 2, y: 4.4 },
+ { portName: "port_4", x: 2, y: 24.1 },
+ { portName: "port_2", x: 62, y: 4.4 },
+ { portName: "port_5", x: 62, y: 24.1 },
+ ]);
+ await expect(
+ piston.locator('.port-handle[data-port-name="port_1"]'),
+ ).toHaveClass(/react-flow__handle-bottom/);
+});
+
test("AMESim canvas nodes use icon anchors and highlight only compatible free ports", async ({
page,
}) => {
@@ -319,9 +372,9 @@ test("AMESim canvas nodes use icon anchors and highlight only compatible free po
];
await expectSymbolAnchors(firstOrifice, firstOrificeAnchors);
await expectSymbolAnchors(signalOrifice, [
- { portName: "res", x: 2, y: 16 },
- { portName: "port_2", x: 2, y: 32 },
- { portName: "port_3", x: 62, y: 25 },
+ { portName: "res", x: 26, y: 46 },
+ { portName: "port_2", x: 2, y: 24 },
+ { portName: "port_3", x: 62, y: 24 },
]);
await firstOrifice.locator(".node-symbol").click({ position: { x: 56, y: 42 } });
@@ -349,6 +402,7 @@ test("AMESim canvas nodes use icon anchors and highlight only compatible free po
const signalInput = signalOrifice.locator(
'.port-handle[data-port-name="res"]',
);
+ await expect(signalInput).toHaveClass(/react-flow__handle-bottom/);
const pneumaticInput = signalOrifice.locator(
'.port-handle[data-port-name="port_2"]',
);