Compare commits
2
Commits
1f4027573f
...
cf249d6c0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf249d6c0d | ||
|
|
11093613c5 |
No files matched your search
+175
-8
@@ -420,11 +420,98 @@ const NODE_SYMBOL_WIDTH = 112;
|
|||||||
const NODE_SYMBOL_HEIGHT = 84;
|
const NODE_SYMBOL_HEIGHT = 84;
|
||||||
const SYMBOL_VIEWBOX_WIDTH = 64;
|
const SYMBOL_VIEWBOX_WIDTH = 64;
|
||||||
const SYMBOL_VIEWBOX_HEIGHT = 48;
|
const SYMBOL_VIEWBOX_HEIGHT = 48;
|
||||||
const CONTACT_SNAP_RADIUS_PX = 24;
|
const SYMBOL_ENVELOPE_PADDING_PX = 2;
|
||||||
|
const CONTACT_SNAP_RADIUS_PX = 12;
|
||||||
const CONTACT_DISCONNECT_DISTANCE_PX = 8;
|
const CONTACT_DISCONNECT_DISTANCE_PX = 8;
|
||||||
const CONTACT_GEOMETRY_TOLERANCE = 0.5;
|
const CONTACT_GEOMETRY_TOLERANCE = 0.5;
|
||||||
const CONTACT_DELETE_SEPARATION = 16;
|
const CONTACT_DELETE_SEPARATION = 16;
|
||||||
|
|
||||||
|
type SymbolEnvelope = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SYMBOL_ENVELOPE: SymbolEnvelope = {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: NODE_SYMBOL_WIDTH,
|
||||||
|
height: NODE_SYMBOL_HEIGHT,
|
||||||
|
};
|
||||||
|
|
||||||
|
function measuredSymbolEnvelope(svg: SVGSVGElement): SymbolEnvelope {
|
||||||
|
const viewBox = svg.viewBox.baseVal;
|
||||||
|
const geometryBounds = svg.getBBox();
|
||||||
|
let bounds: Pick<DOMRect, "x" | "y" | "width" | "height"> = geometryBounds;
|
||||||
|
try {
|
||||||
|
const paintBounds = svg.getBBox({
|
||||||
|
fill: true,
|
||||||
|
stroke: true,
|
||||||
|
markers: true,
|
||||||
|
clipped: false,
|
||||||
|
});
|
||||||
|
const includesPaintOutsideGeometry =
|
||||||
|
paintBounds.x < geometryBounds.x ||
|
||||||
|
paintBounds.y < geometryBounds.y ||
|
||||||
|
paintBounds.x + paintBounds.width >
|
||||||
|
geometryBounds.x + geometryBounds.width ||
|
||||||
|
paintBounds.y + paintBounds.height >
|
||||||
|
geometryBounds.y + geometryBounds.height;
|
||||||
|
if (includesPaintOutsideGeometry) {
|
||||||
|
bounds = paintBounds;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Older SVG implementations may not support the getBBox options object.
|
||||||
|
}
|
||||||
|
if (bounds === geometryBounds) {
|
||||||
|
const maximumStrokeWidth = Array.from(svg.querySelectorAll<SVGElement>("*"))
|
||||||
|
.map((element) => getComputedStyle(element))
|
||||||
|
.filter((style) => style.stroke !== "none")
|
||||||
|
.reduce(
|
||||||
|
(maximum, style) =>
|
||||||
|
Math.max(maximum, Number.parseFloat(style.strokeWidth) || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if (maximumStrokeWidth > 0) {
|
||||||
|
const strokeRadius = maximumStrokeWidth / 2;
|
||||||
|
bounds = {
|
||||||
|
x: geometryBounds.x - strokeRadius,
|
||||||
|
y: geometryBounds.y - strokeRadius,
|
||||||
|
width: geometryBounds.width + maximumStrokeWidth,
|
||||||
|
height: geometryBounds.height + maximumStrokeWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
viewBox.width <= 0 ||
|
||||||
|
viewBox.height <= 0 ||
|
||||||
|
bounds.width <= 0 ||
|
||||||
|
bounds.height <= 0
|
||||||
|
) {
|
||||||
|
return DEFAULT_SYMBOL_ENVELOPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = NODE_SYMBOL_WIDTH / viewBox.width;
|
||||||
|
const scaleY = NODE_SYMBOL_HEIGHT / viewBox.height;
|
||||||
|
const left = (bounds.x - viewBox.x) * scaleX - SYMBOL_ENVELOPE_PADDING_PX;
|
||||||
|
const top = (bounds.y - viewBox.y) * scaleY - SYMBOL_ENVELOPE_PADDING_PX;
|
||||||
|
const right =
|
||||||
|
(bounds.x + bounds.width - viewBox.x) * scaleX +
|
||||||
|
SYMBOL_ENVELOPE_PADDING_PX;
|
||||||
|
const bottom =
|
||||||
|
(bounds.y + bounds.height - viewBox.y) * scaleY +
|
||||||
|
SYMBOL_ENVELOPE_PADDING_PX;
|
||||||
|
const roundPixel = (value: number) => Math.round(value * 100) / 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: roundPixel(left),
|
||||||
|
top: roundPixel(top),
|
||||||
|
width: roundPixel(Math.max(1, right - left)),
|
||||||
|
height: roundPixel(Math.max(1, bottom - top)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function defaultModelingPaneLayout(): ModelingPaneLayout {
|
function defaultModelingPaneLayout(): ModelingPaneLayout {
|
||||||
return {
|
return {
|
||||||
paletteWidth: 230,
|
paletteWidth: 230,
|
||||||
@@ -1396,6 +1483,10 @@ function assignMissingAmesimGasDefinitionIndices(
|
|||||||
|
|
||||||
function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNode>) {
|
function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNode>) {
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
const symbolContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [symbolEnvelope, setSymbolEnvelope] = useState<SymbolEnvelope>(
|
||||||
|
DEFAULT_SYMBOL_ENVELOPE,
|
||||||
|
);
|
||||||
const rotation = normalizeNodeRotation(data.rotation);
|
const rotation = normalizeNodeRotation(data.rotation);
|
||||||
const mirrored = Boolean(data.mirrored);
|
const mirrored = Boolean(data.mirrored);
|
||||||
const vertical = rotation === 90 || rotation === 270;
|
const vertical = rotation === 90 || rotation === 270;
|
||||||
@@ -1412,6 +1503,27 @@ function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNod
|
|||||||
portAnchors,
|
portAnchors,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!hasDedicatedSymbol) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const svg = symbolContainerRef.current?.querySelector<SVGSVGElement>(
|
||||||
|
".component-symbol-svg",
|
||||||
|
);
|
||||||
|
if (!svg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextEnvelope = measuredSymbolEnvelope(svg);
|
||||||
|
setSymbolEnvelope((currentEnvelope) =>
|
||||||
|
currentEnvelope.left === nextEnvelope.left &&
|
||||||
|
currentEnvelope.top === nextEnvelope.top &&
|
||||||
|
currentEnvelope.width === nextEnvelope.width &&
|
||||||
|
currentEnvelope.height === nextEnvelope.height
|
||||||
|
? currentEnvelope
|
||||||
|
: nextEnvelope,
|
||||||
|
);
|
||||||
|
}, [hasDedicatedSymbol, symbol]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateNodeInternals(id);
|
updateNodeInternals(id);
|
||||||
}, [mirrored, rotation, id, updateNodeInternals]);
|
}, [mirrored, rotation, id, updateNodeInternals]);
|
||||||
@@ -1459,10 +1571,21 @@ function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNod
|
|||||||
<div className="node-symbol-anchor" aria-hidden="true">
|
<div className="node-symbol-anchor" aria-hidden="true">
|
||||||
<div
|
<div
|
||||||
className="node-symbol"
|
className="node-symbol"
|
||||||
|
ref={symbolContainerRef}
|
||||||
style={{
|
style={{
|
||||||
transform: `rotate(${rotation}deg) scaleX(${mirrored ? -1 : 1})`,
|
transform: `rotate(${rotation}deg) scaleX(${mirrored ? -1 : 1})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className="node-symbol-envelope"
|
||||||
|
data-symbol-envelope="true"
|
||||||
|
style={{
|
||||||
|
left: symbolEnvelope.left,
|
||||||
|
top: symbolEnvelope.top,
|
||||||
|
width: symbolEnvelope.width,
|
||||||
|
height: symbolEnvelope.height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<ComponentSymbol symbol={symbol} surface="canvas" />
|
<ComponentSymbol symbol={symbol} surface="canvas" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1546,7 +1669,7 @@ function transformedPortPlacements(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
port,
|
port,
|
||||||
position: transformedPortPosition(side, rotation, mirrored),
|
position: transformedPortPosition(side, rotation, mirrored, anchor),
|
||||||
x: renderedX,
|
x: renderedX,
|
||||||
y: renderedY,
|
y: renderedY,
|
||||||
};
|
};
|
||||||
@@ -1571,12 +1694,28 @@ function transformedPortPosition(
|
|||||||
side: PortSide,
|
side: PortSide,
|
||||||
rotation: NodeRotation,
|
rotation: NodeRotation,
|
||||||
mirrored: boolean,
|
mirrored: boolean,
|
||||||
|
anchor?: { x: number; y: number },
|
||||||
) {
|
) {
|
||||||
let directionX = side === "left" ? -1 : 1;
|
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) {
|
if (mirrored) {
|
||||||
directionX = -directionX;
|
directionX = -directionX;
|
||||||
}
|
}
|
||||||
const [x, y] = rotatePoint(directionX, 0, rotation);
|
const [x, y] = rotatePoint(directionX, directionY, rotation);
|
||||||
if (x < -0.01) {
|
if (x < -0.01) {
|
||||||
return Position.Left;
|
return Position.Left;
|
||||||
}
|
}
|
||||||
@@ -3195,6 +3334,9 @@ function FlowWorkbench() {
|
|||||||
const renderedNodes = useMemo(
|
const renderedNodes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
|
const hasDedicatedSymbol = hasComponentSymbol(
|
||||||
|
node.data.symbol ?? node.data.componentType,
|
||||||
|
);
|
||||||
const portConnectionStates = Object.fromEntries(
|
const portConnectionStates = Object.fromEntries(
|
||||||
node.data.ports.map((port) => {
|
node.data.ports.map((port) => {
|
||||||
const key = portConnectionKey(node.id, port.name);
|
const key = portConnectionKey(node.id, port.name);
|
||||||
@@ -3226,11 +3368,18 @@ function FlowWorkbench() {
|
|||||||
return [port.name, state];
|
return [port.name, state];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const className = [
|
||||||
|
node.className,
|
||||||
|
hasDedicatedSymbol ? "component-icon-node" : "",
|
||||||
|
validationSeverityByNode.has(node.id)
|
||||||
|
? `validation-${validationSeverityByNode.get(node.id)}`
|
||||||
|
: "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
className: validationSeverityByNode.has(node.id)
|
className: className || undefined,
|
||||||
? `validation-${validationSeverityByNode.get(node.id)}`
|
|
||||||
: node.className,
|
|
||||||
data: {
|
data: {
|
||||||
...node.data,
|
...node.data,
|
||||||
portConnectionStates,
|
portConnectionStates,
|
||||||
@@ -3775,14 +3924,32 @@ function FlowWorkbench() {
|
|||||||
id,
|
id,
|
||||||
type: "simulationComponent",
|
type: "simulationComponent",
|
||||||
position,
|
position,
|
||||||
|
selected: true,
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
recordHistory();
|
recordHistory();
|
||||||
setNodes((currentNodes) => [...currentNodes, node]);
|
setNodes((currentNodes) => [
|
||||||
|
...currentNodes.map((currentNode) => ({
|
||||||
|
...currentNode,
|
||||||
|
selected: false,
|
||||||
|
})),
|
||||||
|
node,
|
||||||
|
]);
|
||||||
|
setEdges((currentEdges) =>
|
||||||
|
currentEdges.map((edge) => ({ ...edge, selected: false })),
|
||||||
|
);
|
||||||
setSelectedNodeId(id);
|
setSelectedNodeId(id);
|
||||||
|
setSelectedEdgeId(null);
|
||||||
appendConsoleEntry("info", `已添加组件:${id}`);
|
appendConsoleEntry("info", `已添加组件:${id}`);
|
||||||
},
|
},
|
||||||
[appendConsoleEntry, componentDefinitions, reactFlow, recordHistory, setNodes],
|
[
|
||||||
|
appendConsoleEntry,
|
||||||
|
componentDefinitions,
|
||||||
|
reactFlow,
|
||||||
|
recordHistory,
|
||||||
|
setEdges,
|
||||||
|
setNodes,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSelectedNode = (updater: (node: SimulationNode) => SimulationNode) => {
|
const updateSelectedNode = (updater: (node: SimulationNode) => SimulationNode) => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useLayoutEffect } from "react";
|
import { useLayoutEffect, useRef } from "react";
|
||||||
import { useReactFlow, useStore } from "@xyflow/react";
|
import { useReactFlow, useStore } from "@xyflow/react";
|
||||||
|
|
||||||
type AutoFitViewProps = {
|
type AutoFitViewProps = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
expectedNodeCount: number;
|
expectedNodeCount: number;
|
||||||
|
fitOnMount?: boolean;
|
||||||
minZoom?: number;
|
minZoom?: number;
|
||||||
padding?: number;
|
padding?: number;
|
||||||
requestKey: number | string;
|
requestKey: number | string;
|
||||||
@@ -12,6 +13,7 @@ type AutoFitViewProps = {
|
|||||||
export function AutoFitView({
|
export function AutoFitView({
|
||||||
enabled = true,
|
enabled = true,
|
||||||
expectedNodeCount,
|
expectedNodeCount,
|
||||||
|
fitOnMount = false,
|
||||||
minZoom = 0.1,
|
minZoom = 0.1,
|
||||||
padding = 0.18,
|
padding = 0.18,
|
||||||
requestKey,
|
requestKey,
|
||||||
@@ -33,15 +35,41 @@ export function AutoFitView({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const { fitView } = useReactFlow();
|
const { fitView } = useReactFlow();
|
||||||
|
const lastHandledRequestKey = useRef<number | string | null>(
|
||||||
|
fitOnMount ? null : requestKey,
|
||||||
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!enabled || !expectedNodesAreMeasured) {
|
if (Object.is(lastHandledRequestKey.current, requestKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void fitView({ duration: 0, minZoom, padding });
|
if (!enabled || expectedNodeCount <= 0) {
|
||||||
|
lastHandledRequestKey.current = requestKey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectedNodesAreMeasured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let secondFrame = 0;
|
||||||
|
const firstFrame = window.requestAnimationFrame(() => {
|
||||||
|
secondFrame = window.requestAnimationFrame(() => {
|
||||||
|
lastHandledRequestKey.current = requestKey;
|
||||||
|
void fitView({ duration: 0, minZoom, padding });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(firstFrame);
|
||||||
|
if (secondFrame !== 0) {
|
||||||
|
window.cancelAnimationFrame(secondFrame);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
enabled,
|
enabled,
|
||||||
|
expectedNodeCount,
|
||||||
expectedNodesAreMeasured,
|
expectedNodesAreMeasured,
|
||||||
fitView,
|
fitView,
|
||||||
minZoom,
|
minZoom,
|
||||||
|
|||||||
@@ -263,15 +263,15 @@ const symbolPortAnchors: Partial<
|
|||||||
right: [{ x: 62, y: 24 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
},
|
},
|
||||||
amesim_pnvo001_fixed: {
|
amesim_pnvo001_fixed: {
|
||||||
left: [{ x: 2, y: 28 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
right: [{ x: 62, y: 28 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
},
|
},
|
||||||
amesim_pnvo001: {
|
amesim_pnvo001: {
|
||||||
left: [
|
left: [
|
||||||
{ x: 2, y: 16 },
|
{ x: 26, y: 46 },
|
||||||
{ x: 2, y: 32 },
|
{ x: 2, y: 24 },
|
||||||
],
|
],
|
||||||
right: [{ x: 62, y: 25 }],
|
right: [{ x: 62, y: 24 }],
|
||||||
},
|
},
|
||||||
amesim_pnl00r: {
|
amesim_pnl00r: {
|
||||||
left: [{ x: 2, y: 24 }],
|
left: [{ x: 2, y: 24 }],
|
||||||
@@ -325,11 +325,11 @@ const symbolPortAnchors: Partial<
|
|||||||
},
|
},
|
||||||
amesim_pnrp17: {
|
amesim_pnrp17: {
|
||||||
left: [
|
left: [
|
||||||
{ x: 2, y: 24 },
|
{ x: 25.5, y: 48 },
|
||||||
{ x: 2, y: 10 },
|
{ x: 2, y: 4.4 },
|
||||||
{ x: 2, y: 38 },
|
{ x: 2, y: 24.1 },
|
||||||
],
|
],
|
||||||
right: [{ x: 62, y: 10 }, { x: 62, y: 38 }],
|
right: [{ x: 62, y: 4.4 }, { x: 62, y: 24.1 }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1147,6 +1147,7 @@ export function SimulationResultsView({
|
|||||||
<AutoFitView
|
<AutoFitView
|
||||||
enabled={readOnlyNodes.length > 0}
|
enabled={readOnlyNodes.length > 0}
|
||||||
expectedNodeCount={readOnlyNodes.length}
|
expectedNodeCount={readOnlyNodes.length}
|
||||||
|
fitOnMount
|
||||||
requestKey={snapshot.id}
|
requestKey={snapshot.id}
|
||||||
/>
|
/>
|
||||||
<Background gap={18} />
|
<Background gap={18} />
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
} from "./primitives";
|
} from "./primitives";
|
||||||
|
|
||||||
const MECHANICAL_LINE_WIDTH = 1.8;
|
const MECHANICAL_LINE_WIDTH = 1.8;
|
||||||
|
const PNRP17_STROKE = "#8b134f";
|
||||||
|
const PNRP17_FILL = "#cf9db6";
|
||||||
|
const PNRP17_LIGHT_FILL = "#f5ebf0";
|
||||||
|
|
||||||
const mechanicalStroke = {
|
const mechanicalStroke = {
|
||||||
fill: "none",
|
fill: "none",
|
||||||
@@ -220,50 +223,81 @@ export function AmesimPnrp17Symbol() {
|
|||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-amesim-pnrp17" domain="mechanical">
|
<SymbolSvg className="component-symbol-amesim-pnrp17" domain="mechanical">
|
||||||
<path
|
<path
|
||||||
d="M2 24h17M2 10h11M2 38h11M51 10h11M51 38h11"
|
d="M13.4 1.1H51.4V3h4.1v2.9h-4.1v1.9H18.2v13.8h-10V7h5.2V5.9H9.3V3h4.1Z"
|
||||||
{...mechanicalStroke}
|
fill={PNRP17_FILL}
|
||||||
/>
|
stroke={PNRP17_STROKE}
|
||||||
<path
|
strokeLinejoin="miter"
|
||||||
d="M7 6v8h6M7 34v8h6M57 6v8h-6M57 34v8h-6"
|
|
||||||
{...mechanicalStroke}
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
fill={SYMBOL_FILL}
|
|
||||||
height="34"
|
|
||||||
rx="1"
|
|
||||||
stroke={SYMBOL_STROKE}
|
|
||||||
strokeWidth="2"
|
|
||||||
width="32"
|
|
||||||
x="18"
|
|
||||||
y="7"
|
|
||||||
/>
|
|
||||||
<path d="M38 8v32M38 24h12" {...mechanicalStroke} />
|
|
||||||
<path
|
|
||||||
d="M22 18h12M28 14l6 4-6 4"
|
|
||||||
fill="none"
|
|
||||||
stroke={SYMBOL_ACCENT}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="1.8"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M4 21v6M7 20v8M10 21v6"
|
|
||||||
fill="none"
|
|
||||||
stroke={SYMBOL_ACCENT}
|
|
||||||
strokeWidth="1.2"
|
strokeWidth="1.2"
|
||||||
/>
|
/>
|
||||||
<text
|
<path
|
||||||
dominantBaseline="central"
|
d="M8.2 26.7h12.7v13.8h2v6.7H8.2Z"
|
||||||
fill={SYMBOL_STROKE}
|
fill={PNRP17_FILL}
|
||||||
fontFamily="Arial, sans-serif"
|
stroke={PNRP17_STROKE}
|
||||||
fontSize="8"
|
strokeLinejoin="miter"
|
||||||
fontWeight="600"
|
strokeWidth="1.2"
|
||||||
textAnchor="middle"
|
/>
|
||||||
x="28"
|
<path
|
||||||
y="32"
|
d="M28 40.5h28.5v6.7H28Z"
|
||||||
>
|
fill={PNRP17_FILL}
|
||||||
P
|
stroke={PNRP17_STROKE}
|
||||||
</text>
|
strokeLinejoin="miter"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
fill={PNRP17_LIGHT_FILL}
|
||||||
|
height="34"
|
||||||
|
stroke={PNRP17_STROKE}
|
||||||
|
strokeWidth="1.2"
|
||||||
|
width="7.7"
|
||||||
|
x="40.8"
|
||||||
|
y="7.2"
|
||||||
|
/>
|
||||||
|
<rect fill={PNRP17_STROKE} height="34" width="1.8" x="38.8" y="7.2" />
|
||||||
|
<rect
|
||||||
|
fill={PNRP17_LIGHT_FILL}
|
||||||
|
height="2.8"
|
||||||
|
stroke={PNRP17_STROKE}
|
||||||
|
strokeWidth="1.2"
|
||||||
|
width="31.5"
|
||||||
|
x="9.3"
|
||||||
|
y="22.7"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
fill={PNRP17_LIGHT_FILL}
|
||||||
|
height="2.8"
|
||||||
|
stroke={PNRP17_STROKE}
|
||||||
|
strokeWidth="1.2"
|
||||||
|
width="7.7"
|
||||||
|
x="47.8"
|
||||||
|
y="22.7"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2 4.4h7.3m46.2 0H62M2 24.1h7.3m46.2 0H62M25.5 43.4V48"
|
||||||
|
stroke={PNRP17_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
/>
|
||||||
|
{[15.3, 33].map((centerY) => (
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
key={centerY}
|
||||||
|
stroke={PNRP17_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
strokeWidth="1.1"
|
||||||
|
>
|
||||||
|
<path d={`M28.8 ${centerY}h-3.4`} />
|
||||||
|
<path
|
||||||
|
d={`M25.4 ${centerY - 2.5} 21.5 ${centerY}l3.9 2.5Z`}
|
||||||
|
fill="#ffffff"
|
||||||
|
/>
|
||||||
|
<path d={`M30.9 ${centerY}h3.4`} />
|
||||||
|
<path
|
||||||
|
d={`M34.3 ${centerY - 2.5} 38.3 ${centerY} 34.3 ${centerY + 2.5}Z`}
|
||||||
|
fill="#ffffff"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "./primitives";
|
} from "./primitives";
|
||||||
|
|
||||||
const PNEUMATIC_STROKE_WIDTH = 1.7;
|
const PNEUMATIC_STROKE_WIDTH = 1.7;
|
||||||
|
const PNVO001_STROKE = "#8b134f";
|
||||||
|
|
||||||
function OrificeBody({
|
function OrificeBody({
|
||||||
centerY = 25,
|
centerY = 25,
|
||||||
@@ -49,6 +50,46 @@ export function AmesimPnor001Symbol() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Pnvo001Body({
|
||||||
|
signalControlled = false,
|
||||||
|
}: {
|
||||||
|
signalControlled?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M0 24.5h64"
|
||||||
|
stroke={PNVO001_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="2.1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.5 14.4Q33.4 27 51.2 14.4M15.5 34.8Q33.2 22.4 51.2 34.8"
|
||||||
|
stroke={PNVO001_STROKE}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="3.1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19 46.7 44.5 10"
|
||||||
|
stroke={PNVO001_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="2.6"
|
||||||
|
/>
|
||||||
|
{signalControlled ? (
|
||||||
|
<path
|
||||||
|
d="M26 48V36.6l7.2 10.1"
|
||||||
|
stroke={PNVO001_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2.3"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<path d="M50.6 0 38.6 8.2 48 12.3Z" fill={PNVO001_STROKE} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** PNVO001 convenience variant whose opening is fixed by a parameter. */
|
/** PNVO001 convenience variant whose opening is fixed by a parameter. */
|
||||||
export function AmesimPnvo001FixedSymbol() {
|
export function AmesimPnvo001FixedSymbol() {
|
||||||
return (
|
return (
|
||||||
@@ -56,14 +97,7 @@ export function AmesimPnvo001FixedSymbol() {
|
|||||||
className="component-symbol-amesim-pnvo001-fixed-svg"
|
className="component-symbol-amesim-pnvo001-fixed-svg"
|
||||||
domain="pneumatic"
|
domain="pneumatic"
|
||||||
>
|
>
|
||||||
<OrificeBody centerY={28} />
|
<Pnvo001Body />
|
||||||
<path
|
|
||||||
d="M43 4 34 20m9-16-2 7-5-3"
|
|
||||||
stroke={SYMBOL_ACCENT}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="1.7"
|
|
||||||
/>
|
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -75,14 +109,7 @@ export function AmesimPnvo001SignalSymbol() {
|
|||||||
className="component-symbol-amesim-pnvo001-signal-svg"
|
className="component-symbol-amesim-pnvo001-signal-svg"
|
||||||
domain="pneumatic"
|
domain="pneumatic"
|
||||||
>
|
>
|
||||||
<OrificeBody centerY={25} leftPortY={32} />
|
<Pnvo001Body signalControlled />
|
||||||
<path
|
|
||||||
d="M2 16h11l21 5m0 0-7-1m7 1-5 5"
|
|
||||||
stroke={SYMBOL_ACCENT}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="1.7"
|
|
||||||
/>
|
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-8
@@ -1253,6 +1253,12 @@ textarea {
|
|||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flow-canvas .react-flow__nodesselection-rect {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sim-node {
|
.sim-node {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 132px;
|
width: 132px;
|
||||||
@@ -1280,9 +1286,30 @@ textarea {
|
|||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sim-node.selected .sim-node-frame {
|
.sim-node.selected {
|
||||||
border-color: #1d6fb8;
|
outline: 1px dashed #111827;
|
||||||
box-shadow: 0 0 0 3px rgba(29, 111, 184, 0.15);
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-canvas .react-flow__node.component-icon-node {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-canvas .react-flow__node.component-icon-node .sim-node {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-canvas .react-flow__node.component-icon-node .node-symbol-envelope,
|
||||||
|
.flow-canvas
|
||||||
|
.react-flow__node.component-icon-node
|
||||||
|
.port-handle.port-connection-idle,
|
||||||
|
.flow-canvas
|
||||||
|
.react-flow__node.component-icon-node
|
||||||
|
.port-handle.port-connection-origin,
|
||||||
|
.flow-canvas
|
||||||
|
.react-flow__node.component-icon-node
|
||||||
|
.port-handle.port-connection-compatible {
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node.validation-error .sim-node-frame {
|
.react-flow__node.validation-error .sim-node-frame {
|
||||||
@@ -1306,6 +1333,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node-symbol {
|
.node-symbol {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 112px;
|
width: 112px;
|
||||||
height: 84px;
|
height: 84px;
|
||||||
@@ -1314,10 +1342,27 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node-symbol > .component-symbol {
|
.node-symbol > .component-symbol {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-symbol-envelope {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-node.component-icon-only.selected {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-node.component-icon-only.selected .node-symbol-envelope {
|
||||||
|
outline: 1px dashed #111827;
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.sim-node.symbol-presentation-bare .sim-node-frame,
|
.sim-node.symbol-presentation-bare .sim-node-frame,
|
||||||
.sim-node.symbol-presentation-bare.selected .sim-node-frame,
|
.sim-node.symbol-presentation-bare.selected .sim-node-frame,
|
||||||
.sim-node.component-icon-only .sim-node-frame,
|
.sim-node.component-icon-only .sim-node-frame,
|
||||||
@@ -1334,11 +1379,6 @@ textarea {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sim-node.symbol-presentation-bare.selected .component-symbol-svg,
|
|
||||||
.sim-node.component-icon-only.selected .component-symbol-svg {
|
|
||||||
filter: drop-shadow(0 0 3px rgba(29, 111, 184, 0.62));
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-flow__node.validation-error
|
.react-flow__node.validation-error
|
||||||
.sim-node.component-icon-only
|
.sim-node.component-icon-only
|
||||||
.component-symbol-svg {
|
.component-symbol-svg {
|
||||||
|
|||||||
@@ -63,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) {
|
function portsForSymbol(modelType: string) {
|
||||||
if (modelType === "amesim_pnor001") {
|
if (modelType === "amesim_pnor001") {
|
||||||
return [
|
return [
|
||||||
@@ -80,6 +96,15 @@ function portsForSymbol(modelType: string) {
|
|||||||
if (modelType === "amesim_step0") {
|
if (modelType === "amesim_step0") {
|
||||||
return [signalPort("out", "right", "output", 10)];
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,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 ({
|
test("AMESim canvas nodes use icon anchors and highlight only compatible free ports", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -320,9 +372,9 @@ test("AMESim canvas nodes use icon anchors and highlight only compatible free po
|
|||||||
];
|
];
|
||||||
await expectSymbolAnchors(firstOrifice, firstOrificeAnchors);
|
await expectSymbolAnchors(firstOrifice, firstOrificeAnchors);
|
||||||
await expectSymbolAnchors(signalOrifice, [
|
await expectSymbolAnchors(signalOrifice, [
|
||||||
{ portName: "res", x: 2, y: 16 },
|
{ portName: "res", x: 26, y: 46 },
|
||||||
{ portName: "port_2", x: 2, y: 32 },
|
{ portName: "port_2", x: 2, y: 24 },
|
||||||
{ portName: "port_3", x: 62, y: 25 },
|
{ portName: "port_3", x: 62, y: 24 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await firstOrifice.locator(".node-symbol").click({ position: { x: 56, y: 42 } });
|
await firstOrifice.locator(".node-symbol").click({ position: { x: 56, y: 42 } });
|
||||||
@@ -350,6 +402,7 @@ test("AMESim canvas nodes use icon anchors and highlight only compatible free po
|
|||||||
const signalInput = signalOrifice.locator(
|
const signalInput = signalOrifice.locator(
|
||||||
'.port-handle[data-port-name="res"]',
|
'.port-handle[data-port-name="res"]',
|
||||||
);
|
);
|
||||||
|
await expect(signalInput).toHaveClass(/react-flow__handle-bottom/);
|
||||||
const pneumaticInput = signalOrifice.locator(
|
const pneumaticInput = signalOrifice.locator(
|
||||||
'.port-handle[data-port-name="port_2"]',
|
'.port-handle[data-port-name="port_2"]',
|
||||||
);
|
);
|
||||||
@@ -375,6 +428,8 @@ test("AMESim canvas nodes use icon anchors and highlight only compatible free po
|
|||||||
"data-connection-state",
|
"data-connection-state",
|
||||||
"incompatible",
|
"incompatible",
|
||||||
);
|
);
|
||||||
|
await expect(sameNodePort).toHaveCSS("pointer-events", "none");
|
||||||
|
await expect(signalInput).toHaveCSS("pointer-events", "none");
|
||||||
|
|
||||||
await page.locator(".flow-canvas .react-flow__pane").click({
|
await page.locator(".flow-canvas .react-flow__pane").click({
|
||||||
position: { x: 20, y: 20 },
|
position: { x: 20, y: 20 },
|
||||||
@@ -402,9 +457,23 @@ test("AMESim canvas nodes use icon anchors and highlight only compatible free po
|
|||||||
await expect(compatiblePhysicalPort).toHaveAttribute("data-connected", "true");
|
await expect(compatiblePhysicalPort).toHaveAttribute("data-connected", "true");
|
||||||
await expect(firstPhysicalOutput).toHaveCSS("opacity", "0");
|
await expect(firstPhysicalOutput).toHaveCSS("opacity", "0");
|
||||||
await expect(compatiblePhysicalPort).toHaveCSS("opacity", "0");
|
await expect(compatiblePhysicalPort).toHaveCSS("opacity", "0");
|
||||||
|
await expect(firstPhysicalOutput).toHaveCSS("pointer-events", "none");
|
||||||
|
await expect(compatiblePhysicalPort).toHaveCSS("pointer-events", "none");
|
||||||
|
|
||||||
const physicalEdge = page.locator(".flow-canvas .react-flow__edge").first();
|
const physicalEdge = page.locator(".flow-canvas .react-flow__edge").first();
|
||||||
await physicalEdge.locator(".react-flow__edge-interaction").click({ force: true });
|
const edgeClickPoint = await physicalEdge
|
||||||
|
.locator(".react-flow__edge-interaction")
|
||||||
|
.evaluate((path: SVGPathElement) => {
|
||||||
|
const point = path.getPointAtLength(path.getTotalLength() / 2);
|
||||||
|
const matrix = path.getScreenCTM();
|
||||||
|
if (!matrix) {
|
||||||
|
throw new Error("Unable to resolve the edge screen position");
|
||||||
|
}
|
||||||
|
const screenPoint = new DOMPoint(point.x, point.y).matrixTransform(matrix);
|
||||||
|
return { x: screenPoint.x, y: screenPoint.y };
|
||||||
|
});
|
||||||
|
await page.mouse.click(edgeClickPoint.x, edgeClickPoint.y);
|
||||||
|
await expect(physicalEdge).toHaveClass(/selected|editor-edge-selected/);
|
||||||
await page.getByRole("button", { name: "删除 (Delete)", exact: true }).click();
|
await page.getByRole("button", { name: "删除 (Delete)", exact: true }).click();
|
||||||
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
|
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
|
||||||
await expect(firstPhysicalOutput).toHaveAttribute("data-connected", "false");
|
await expect(firstPhysicalOutput).toHaveAttribute("data-connected", "false");
|
||||||
@@ -514,6 +583,19 @@ test("兼容接口接近时吸附为无连线接触连接,拖开后自动断
|
|||||||
'.port-handle[data-port-name="port_1"]',
|
'.port-handle[data-port-name="port_1"]',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await dragNodePortNearTarget(
|
||||||
|
page,
|
||||||
|
secondOrifice,
|
||||||
|
secondPort,
|
||||||
|
firstPort,
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
await expect(firstPort).toHaveAttribute("data-connection-state", "idle");
|
||||||
|
await expect(secondPort).toHaveAttribute("data-connection-state", "idle");
|
||||||
|
expect(await distanceBetween(firstPort, secondPort)).toBeGreaterThan(14);
|
||||||
|
await page.mouse.up();
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
|
||||||
|
|
||||||
await dragNodePortNearTarget(
|
await dragNodePortNearTarget(
|
||||||
page,
|
page,
|
||||||
secondOrifice,
|
secondOrifice,
|
||||||
@@ -748,3 +830,58 @@ test("旧工程保存的通用图标会按当前目录定义升级", async ({ pa
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(restoredNode.locator('[data-component-symbol="pipe"]')).toHaveCount(0);
|
await expect(restoredNode.locator('[data-component-symbol="pipe"]')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("专用图标的选择框和鼠标命中范围贴合实际图形包络", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const helium = await dragSymbolToCanvas(page, "amesim_helium_medium", {
|
||||||
|
x: 430,
|
||||||
|
y: 280,
|
||||||
|
});
|
||||||
|
const simNode = helium.locator(".sim-node");
|
||||||
|
const envelope = helium.locator('[data-symbol-envelope="true"]');
|
||||||
|
const circle = helium.locator(".component-symbol-svg circle");
|
||||||
|
|
||||||
|
await expect(helium).toHaveClass(/component-icon-node/);
|
||||||
|
await expect(helium).toHaveCSS("pointer-events", "none");
|
||||||
|
await expect(simNode).toHaveCSS("pointer-events", "none");
|
||||||
|
await expect(envelope).toHaveCSS("outline-style", "dashed");
|
||||||
|
await expect(envelope).toHaveCSS("pointer-events", "auto");
|
||||||
|
await expect(simNode).toHaveCSS("outline-style", "none");
|
||||||
|
|
||||||
|
const [nodeBounds, envelopeBounds, circleBounds, paneBounds] =
|
||||||
|
await Promise.all([
|
||||||
|
helium.boundingBox(),
|
||||||
|
envelope.boundingBox(),
|
||||||
|
circle.boundingBox(),
|
||||||
|
page.locator(".flow-canvas .react-flow__pane").boundingBox(),
|
||||||
|
]);
|
||||||
|
expect(nodeBounds).not.toBeNull();
|
||||||
|
expect(envelopeBounds).not.toBeNull();
|
||||||
|
expect(circleBounds).not.toBeNull();
|
||||||
|
expect(paneBounds).not.toBeNull();
|
||||||
|
expect(envelopeBounds!.width).toBeLessThan(nodeBounds!.width * 0.7);
|
||||||
|
expect(
|
||||||
|
Math.abs(envelopeBounds!.width - envelopeBounds!.height),
|
||||||
|
).toBeLessThan(3);
|
||||||
|
expect(envelopeBounds!.width - circleBounds!.width).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(envelopeBounds!.width - circleBounds!.width).toBeLessThanOrEqual(4);
|
||||||
|
|
||||||
|
await page.mouse.click(paneBounds!.x + 8, paneBounds!.y + 8);
|
||||||
|
await expect(helium).not.toHaveClass(/selected/);
|
||||||
|
|
||||||
|
const oldTransparentMargin = {
|
||||||
|
x: nodeBounds!.x + 5,
|
||||||
|
y: envelopeBounds!.y + envelopeBounds!.height / 2,
|
||||||
|
};
|
||||||
|
expect(oldTransparentMargin.x).toBeLessThan(envelopeBounds!.x - 8);
|
||||||
|
await page.mouse.click(oldTransparentMargin.x, oldTransparentMargin.y);
|
||||||
|
await expect(helium).not.toHaveClass(/selected/);
|
||||||
|
|
||||||
|
await page.mouse.click(
|
||||||
|
envelopeBounds!.x + envelopeBounds!.width / 2,
|
||||||
|
envelopeBounds!.y + envelopeBounds!.height / 2,
|
||||||
|
);
|
||||||
|
await expect(helium).toHaveClass(/selected/);
|
||||||
|
await expect(envelope).toHaveCSS("outline-style", "dashed");
|
||||||
|
});
|
||||||
@@ -6,6 +6,31 @@ import {
|
|||||||
wideProject,
|
wideProject,
|
||||||
} from "./fixtures";
|
} from "./fixtures";
|
||||||
|
|
||||||
|
test("拖入组件不自动适配画布,手动适配按钮仍然生效", async ({ page }) => {
|
||||||
|
await prepareApp(page);
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const viewport = page.locator(".flow-canvas .react-flow__viewport");
|
||||||
|
const zoomOut = page.locator(".flow-canvas .react-flow__controls-zoomout");
|
||||||
|
await zoomOut.click();
|
||||||
|
await zoomOut.click();
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const viewportBeforeDrop = await viewport.getAttribute("style");
|
||||||
|
expect(viewportBeforeDrop).toBeTruthy();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "通用测试元件", exact: true })
|
||||||
|
.dragTo(page.locator(".flow-canvas .react-flow__pane"));
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(1);
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await expect(viewport).toHaveAttribute("style", viewportBeforeDrop!);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "适配视图", exact: true }).click();
|
||||||
|
await expect
|
||||||
|
.poll(() => viewport.getAttribute("style"))
|
||||||
|
.not.toBe(viewportBeforeDrop);
|
||||||
|
});
|
||||||
|
|
||||||
test("加载本地工程后自动适配建模画布", async ({ page }) => {
|
test("加载本地工程后自动适配建模画布", async ({ page }) => {
|
||||||
await prepareApp(page);
|
await prepareApp(page);
|
||||||
await page.addInitScript((project) => {
|
await page.addInitScript((project) => {
|
||||||
@@ -28,6 +53,19 @@ test("加载本地工程后自动适配建模画布", async ({ page }) => {
|
|||||||
await expect(consolePanel).toContainText("工程已加载");
|
await expect(consolePanel).toContainText("工程已加载");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("导入工程 JSON 后自动适配建模画布", async ({ page }) => {
|
||||||
|
await prepareApp(page);
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: "wide-project.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify(wideProject)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectAllNodesInsideCanvas(page, ".flow-canvas");
|
||||||
|
});
|
||||||
|
|
||||||
test("恢复自动保存工程后自动适配建模画布", async ({ page }) => {
|
test("恢复自动保存工程后自动适配建模画布", async ({ page }) => {
|
||||||
await prepareApp(page);
|
await prepareApp(page);
|
||||||
await page.addInitScript((project) => {
|
await page.addInitScript((project) => {
|
||||||
@@ -80,6 +118,7 @@ test("切换到结果页时自动适配只读系统图", async ({ page }) => {
|
|||||||
).toHaveCount(0);
|
).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByRole("tab", { name: "建模" }).click();
|
await page.getByRole("tab", { name: "建模" }).click();
|
||||||
|
await expectAllNodesInsideCanvas(page, ".flow-canvas");
|
||||||
await expect(freeModelingHandle).toHaveAttribute(
|
await expect(freeModelingHandle).toHaveAttribute(
|
||||||
"data-connection-state",
|
"data-connection-state",
|
||||||
"idle",
|
"idle",
|
||||||
|
|||||||
@@ -44,3 +44,115 @@ test("未知图形元件使用带端口和名称的默认节点,并将动作
|
|||||||
await expect(page.locator(".validation-section")).toHaveCount(0);
|
await expect(page.locator(".validation-section")).toHaveCount(0);
|
||||||
await expect(page.locator(".properties")).not.toContainText("仿真结果");
|
await expect(page.locator(".properties")).not.toContainText("仿真结果");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("拖入组件后仅选择新组件,变换操作不会继续作用于旧选择", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
const paletteItem = page.getByRole("button", { name: /通用测试元件/ });
|
||||||
|
const pane = page.locator(".flow-canvas .react-flow__pane");
|
||||||
|
|
||||||
|
await paletteItem.dragTo(pane, { targetPosition: { x: 220, y: 220 } });
|
||||||
|
const firstNode = page.locator(
|
||||||
|
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"]',
|
||||||
|
);
|
||||||
|
await expect(firstNode).toHaveClass(/selected/);
|
||||||
|
await expect(firstNode.locator(".sim-node")).toHaveCSS(
|
||||||
|
"outline-style",
|
||||||
|
"dashed",
|
||||||
|
);
|
||||||
|
|
||||||
|
await paletteItem.dragTo(pane, { targetPosition: { x: 480, y: 340 } });
|
||||||
|
const secondNode = page.locator(
|
||||||
|
'.flow-canvas .react-flow__node[data-id="generic_sensor_2"]',
|
||||||
|
);
|
||||||
|
await expect(firstNode).not.toHaveClass(/selected/);
|
||||||
|
await expect(secondNode).toHaveClass(/selected/);
|
||||||
|
await expect(firstNode.locator(".sim-node")).toHaveCSS("outline-style", "none");
|
||||||
|
await expect(secondNode.locator(".sim-node")).toHaveCSS(
|
||||||
|
"outline-style",
|
||||||
|
"dashed",
|
||||||
|
);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(firstNode.locator(".sim-node")).not.toHaveClass(
|
||||||
|
/orientation-vertical/,
|
||||||
|
);
|
||||||
|
await expect(secondNode.locator(".sim-node")).toHaveClass(
|
||||||
|
/orientation-vertical/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("框选结束后隐藏整体包围框并保留每个组件的虚线选中框", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
const paletteItem = page.getByRole("button", { name: /通用测试元件/ });
|
||||||
|
const pane = page.locator(".flow-canvas .react-flow__pane");
|
||||||
|
|
||||||
|
await paletteItem.dragTo(pane, { targetPosition: { x: 220, y: 220 } });
|
||||||
|
await paletteItem.dragTo(pane, { targetPosition: { x: 480, y: 340 } });
|
||||||
|
const firstNode = page.locator(
|
||||||
|
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"]',
|
||||||
|
);
|
||||||
|
const secondNode = page.locator(
|
||||||
|
'.flow-canvas .react-flow__node[data-id="generic_sensor_2"]',
|
||||||
|
);
|
||||||
|
const [paneBounds, firstBounds, secondBounds] = await Promise.all([
|
||||||
|
pane.boundingBox(),
|
||||||
|
firstNode.boundingBox(),
|
||||||
|
secondNode.boundingBox(),
|
||||||
|
]);
|
||||||
|
expect(paneBounds).not.toBeNull();
|
||||||
|
expect(firstBounds).not.toBeNull();
|
||||||
|
expect(secondBounds).not.toBeNull();
|
||||||
|
|
||||||
|
const startX = Math.max(
|
||||||
|
paneBounds!.x + 4,
|
||||||
|
Math.min(firstBounds!.x, secondBounds!.x) - 14,
|
||||||
|
);
|
||||||
|
const startY = Math.max(
|
||||||
|
paneBounds!.y + 4,
|
||||||
|
Math.min(firstBounds!.y, secondBounds!.y) - 14,
|
||||||
|
);
|
||||||
|
const endX = Math.min(
|
||||||
|
paneBounds!.x + paneBounds!.width - 4,
|
||||||
|
Math.max(
|
||||||
|
firstBounds!.x + firstBounds!.width,
|
||||||
|
secondBounds!.x + secondBounds!.width,
|
||||||
|
) + 14,
|
||||||
|
);
|
||||||
|
const endY = Math.min(
|
||||||
|
paneBounds!.y + paneBounds!.height - 4,
|
||||||
|
Math.max(
|
||||||
|
firstBounds!.y + firstBounds!.height,
|
||||||
|
secondBounds!.y + secondBounds!.height,
|
||||||
|
) + 14,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.mouse.move(startX, startY);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(endX, endY, { steps: 12 });
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await expect(firstNode).toHaveClass(/selected/);
|
||||||
|
await expect(secondNode).toHaveClass(/selected/);
|
||||||
|
await expect(firstNode.locator(".sim-node")).toHaveCSS(
|
||||||
|
"outline-style",
|
||||||
|
"dashed",
|
||||||
|
);
|
||||||
|
await expect(secondNode.locator(".sim-node")).toHaveCSS(
|
||||||
|
"outline-style",
|
||||||
|
"dashed",
|
||||||
|
);
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__selection")).toHaveCount(0);
|
||||||
|
|
||||||
|
const aggregateSelection = page.locator(
|
||||||
|
".flow-canvas .react-flow__nodesselection-rect",
|
||||||
|
);
|
||||||
|
await expect(aggregateSelection).toHaveCount(1);
|
||||||
|
await expect(aggregateSelection).toHaveCSS("border-top-width", "0px");
|
||||||
|
await expect(aggregateSelection).toHaveCSS("pointer-events", "none");
|
||||||
|
});
|
||||||
Reference in new issue
Block a user