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"]', );