完善端口标号与MECMAS21动态图标
This commit is contained in:
1 parent
df1d676131
commit
09778972a6
7 files changed
+762
-42
No files matched your search
+227
-2
@@ -456,6 +456,13 @@ 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 SYMBOL_ENVELOPE_PADDING_PX = 2;
|
const SYMBOL_ENVELOPE_PADDING_PX = 2;
|
||||||
|
const PORT_INDEX_OUTSET_PX = 18;
|
||||||
|
const PORT_INDEX_TANGENT_OFFSET_PX = 12;
|
||||||
|
const PORT_INDEX_LABEL_HEIGHT_PX = 20;
|
||||||
|
const PORT_INDEX_LABEL_MIN_WIDTH_PX = 20;
|
||||||
|
const PORT_INDEX_LABEL_CHARACTER_WIDTH_PX = 10;
|
||||||
|
const PORT_INDEX_LABEL_GAP_PX = 1;
|
||||||
|
const PORT_INDEX_LABEL_LANE_GAP_PX = 2;
|
||||||
const CONTACT_SNAP_RADIUS_PX = 12;
|
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;
|
||||||
@@ -1585,6 +1592,13 @@ function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNod
|
|||||||
mirrored,
|
mirrored,
|
||||||
portAnchors,
|
portAnchors,
|
||||||
);
|
);
|
||||||
|
const portIndexLabels = selected
|
||||||
|
? portIndexLabelPlacements(data.ports, portPlacements, rotation)
|
||||||
|
: [];
|
||||||
|
const symbolVariantKey =
|
||||||
|
symbol === "amesim_mecmas21"
|
||||||
|
? `${data.parameters.useFriction ?? ""}:${data.parameters.stoptype ?? ""}`
|
||||||
|
: "";
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!hasDedicatedSymbol) {
|
if (!hasDedicatedSymbol) {
|
||||||
@@ -1605,7 +1619,7 @@ function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNod
|
|||||||
? currentEnvelope
|
? currentEnvelope
|
||||||
: nextEnvelope,
|
: nextEnvelope,
|
||||||
);
|
);
|
||||||
}, [hasDedicatedSymbol, symbol]);
|
}, [hasDedicatedSymbol, symbol, symbolVariantKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateNodeInternals(id);
|
updateNodeInternals(id);
|
||||||
@@ -1669,10 +1683,26 @@ function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNod
|
|||||||
height: symbolEnvelope.height,
|
height: symbolEnvelope.height,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ComponentSymbol symbol={symbol} surface="canvas" />
|
<ComponentSymbol
|
||||||
|
parameters={data.parameters}
|
||||||
|
symbol={symbol}
|
||||||
|
surface="canvas"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{portIndexLabels.map((label) => (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="port-index-label"
|
||||||
|
data-port-index={label.text}
|
||||||
|
data-port-name={label.portName}
|
||||||
|
key={label.portName}
|
||||||
|
style={{ left: label.x, top: label.y }}
|
||||||
|
>
|
||||||
|
{label.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
<div className="node-label">{data.label}</div>
|
<div className="node-label">{data.label}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1760,6 +1790,199 @@ function transformedPortPlacements(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransformedPortPlacement = ReturnType<
|
||||||
|
typeof transformedPortPlacements
|
||||||
|
>[number];
|
||||||
|
|
||||||
|
type PortIndexLabelPlacement = {
|
||||||
|
order: number;
|
||||||
|
portName: string;
|
||||||
|
position: Position;
|
||||||
|
text: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function portIndexText(port: PortDefinition, index: number) {
|
||||||
|
const numericSuffix = port.name.match(/^port_([1-9]\d*)$/)?.[1];
|
||||||
|
if (numericSuffix !== undefined) {
|
||||||
|
const parsed = Number(numericSuffix);
|
||||||
|
if (Number.isSafeInteger(parsed)) {
|
||||||
|
return String(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function portIndexLabelWidth(text: string) {
|
||||||
|
return Math.max(
|
||||||
|
PORT_INDEX_LABEL_MIN_WIDTH_PX,
|
||||||
|
text.length * PORT_INDEX_LABEL_CHARACTER_WIDTH_PX + 4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function packPortIndexLabelLane(
|
||||||
|
labels: PortIndexLabelPlacement[],
|
||||||
|
position: Position,
|
||||||
|
axisLength: number,
|
||||||
|
) {
|
||||||
|
const verticalEdge = position === Position.Left || position === Position.Right;
|
||||||
|
const coordinate = (label: PortIndexLabelPlacement) =>
|
||||||
|
verticalEdge ? label.y : label.x;
|
||||||
|
const extent = (label: PortIndexLabelPlacement) =>
|
||||||
|
verticalEdge ? PORT_INDEX_LABEL_HEIGHT_PX : portIndexLabelWidth(label.text);
|
||||||
|
const sorted = [...labels].sort(
|
||||||
|
(first, second) =>
|
||||||
|
coordinate(first) - coordinate(second) || first.order - second.order,
|
||||||
|
);
|
||||||
|
if (sorted.length < 2) {
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packedCoordinates: number[] = [];
|
||||||
|
for (const [index, label] of sorted.entries()) {
|
||||||
|
const target = coordinate(label);
|
||||||
|
if (index === 0) {
|
||||||
|
packedCoordinates.push(target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previous = sorted[index - 1];
|
||||||
|
const minimumCoordinate =
|
||||||
|
packedCoordinates[index - 1] +
|
||||||
|
extent(previous) / 2 +
|
||||||
|
extent(label) / 2 +
|
||||||
|
PORT_INDEX_LABEL_GAP_PX;
|
||||||
|
packedCoordinates.push(Math.max(target, minimumCoordinate));
|
||||||
|
}
|
||||||
|
|
||||||
|
const idealShift =
|
||||||
|
sorted.reduce(
|
||||||
|
(sum, label, index) => sum + coordinate(label) - packedCoordinates[index],
|
||||||
|
0,
|
||||||
|
) / sorted.length;
|
||||||
|
const minimumShift = extent(sorted[0]) / 2 - packedCoordinates[0];
|
||||||
|
const maximumShift =
|
||||||
|
axisLength -
|
||||||
|
extent(sorted[sorted.length - 1]) / 2 -
|
||||||
|
packedCoordinates[packedCoordinates.length - 1];
|
||||||
|
const shift =
|
||||||
|
minimumShift <= maximumShift
|
||||||
|
? Math.min(Math.max(idealShift, minimumShift), maximumShift)
|
||||||
|
: idealShift;
|
||||||
|
|
||||||
|
return sorted.map((label, index) => {
|
||||||
|
const adjustedCoordinate = packedCoordinates[index] + shift;
|
||||||
|
return verticalEdge
|
||||||
|
? { ...label, y: adjustedCoordinate }
|
||||||
|
: { ...label, x: adjustedCoordinate };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function offsetPortIndexLabelOutward(
|
||||||
|
label: PortIndexLabelPlacement,
|
||||||
|
distance: number,
|
||||||
|
) {
|
||||||
|
if (label.position === Position.Left) {
|
||||||
|
return { ...label, x: label.x - distance };
|
||||||
|
}
|
||||||
|
if (label.position === Position.Right) {
|
||||||
|
return { ...label, x: label.x + distance };
|
||||||
|
}
|
||||||
|
if (label.position === Position.Top) {
|
||||||
|
return { ...label, y: label.y - distance };
|
||||||
|
}
|
||||||
|
return { ...label, y: label.y + distance };
|
||||||
|
}
|
||||||
|
|
||||||
|
function spreadPortIndexLabels(
|
||||||
|
labels: PortIndexLabelPlacement[],
|
||||||
|
position: Position,
|
||||||
|
axisLength: number,
|
||||||
|
) {
|
||||||
|
if (labels.length === 0) {
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
const verticalEdge = position === Position.Left || position === Position.Right;
|
||||||
|
const coordinate = (label: PortIndexLabelPlacement) =>
|
||||||
|
verticalEdge ? label.y : label.x;
|
||||||
|
const extent = (label: PortIndexLabelPlacement) =>
|
||||||
|
verticalEdge ? PORT_INDEX_LABEL_HEIGHT_PX : portIndexLabelWidth(label.text);
|
||||||
|
const normalExtent = (label: PortIndexLabelPlacement) =>
|
||||||
|
verticalEdge ? portIndexLabelWidth(label.text) : PORT_INDEX_LABEL_HEIGHT_PX;
|
||||||
|
const sorted = [...labels].sort(
|
||||||
|
(first, second) =>
|
||||||
|
coordinate(first) - coordinate(second) || first.order - second.order,
|
||||||
|
);
|
||||||
|
const requiredSpan =
|
||||||
|
sorted.reduce((sum, label) => sum + extent(label), 0) +
|
||||||
|
PORT_INDEX_LABEL_GAP_PX * Math.max(0, sorted.length - 1);
|
||||||
|
const laneCount = Math.min(
|
||||||
|
sorted.length,
|
||||||
|
Math.max(1, Math.ceil(requiredSpan / axisLength)),
|
||||||
|
);
|
||||||
|
const lanes = Array.from(
|
||||||
|
{ length: laneCount },
|
||||||
|
() => [] as PortIndexLabelPlacement[],
|
||||||
|
);
|
||||||
|
sorted.forEach((label, index) => lanes[index % laneCount].push(label));
|
||||||
|
const laneStep =
|
||||||
|
Math.max(...sorted.map(normalExtent)) + PORT_INDEX_LABEL_LANE_GAP_PX;
|
||||||
|
|
||||||
|
return lanes.flatMap((lane, laneIndex) =>
|
||||||
|
packPortIndexLabelLane(lane, position, axisLength).map((label) =>
|
||||||
|
offsetPortIndexLabelOutward(
|
||||||
|
label,
|
||||||
|
PORT_INDEX_OUTSET_PX + laneIndex * laneStep,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function portIndexLabelPlacements(
|
||||||
|
ports: PortDefinition[],
|
||||||
|
placements: TransformedPortPlacement[],
|
||||||
|
rotation: NodeRotation,
|
||||||
|
) {
|
||||||
|
const indexByPortName = new Map(
|
||||||
|
ports.map((port, index) => [port.name, index] as const),
|
||||||
|
);
|
||||||
|
const labels = placements.map(({ port, position, x, y }, order) => {
|
||||||
|
const index = indexByPortName.get(port.name) ?? order;
|
||||||
|
const label: PortIndexLabelPlacement = {
|
||||||
|
order,
|
||||||
|
portName: port.name,
|
||||||
|
position,
|
||||||
|
text: portIndexText(port, index),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
if (position === Position.Left || position === Position.Right) {
|
||||||
|
label.y -= PORT_INDEX_TANGENT_OFFSET_PX;
|
||||||
|
} else {
|
||||||
|
label.x += PORT_INDEX_TANGENT_OFFSET_PX;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
const renderedSize = nodeFrameDimensions(rotation);
|
||||||
|
const spreadByOrder = new Map<number, PortIndexLabelPlacement>();
|
||||||
|
for (const position of [
|
||||||
|
Position.Left,
|
||||||
|
Position.Right,
|
||||||
|
Position.Top,
|
||||||
|
Position.Bottom,
|
||||||
|
]) {
|
||||||
|
const group = labels.filter((label) => label.position === position);
|
||||||
|
const axisLength =
|
||||||
|
position === Position.Left || position === Position.Right
|
||||||
|
? renderedSize.height
|
||||||
|
: renderedSize.width;
|
||||||
|
for (const label of spreadPortIndexLabels(group, position, axisLength)) {
|
||||||
|
spreadByOrder.set(label.order, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labels.map((label) => spreadByOrder.get(label.order) ?? label);
|
||||||
|
}
|
||||||
|
|
||||||
function rotatePoint(x: number, y: number, rotation: NodeRotation): [number, number] {
|
function rotatePoint(x: number, y: number, rotation: NodeRotation): [number, number] {
|
||||||
if (rotation === 90) {
|
if (rotation === 90) {
|
||||||
return [-y, x];
|
return [-y, x];
|
||||||
@@ -5749,6 +5972,7 @@ function FlowWorkbench() {
|
|||||||
>
|
>
|
||||||
<span className="palette-icon">
|
<span className="palette-icon">
|
||||||
<ComponentSymbol
|
<ComponentSymbol
|
||||||
|
parameters={defaultParameters(definition)}
|
||||||
surface="palette"
|
surface="palette"
|
||||||
symbol={definition.symbol}
|
symbol={definition.symbol}
|
||||||
/>
|
/>
|
||||||
@@ -6038,6 +6262,7 @@ function FlowWorkbench() {
|
|||||||
>
|
>
|
||||||
<span className="palette-icon-preview-symbol">
|
<span className="palette-icon-preview-symbol">
|
||||||
<ComponentSymbol
|
<ComponentSymbol
|
||||||
|
parameters={defaultParameters(paletteIconPreview.definition)}
|
||||||
surface="palette"
|
surface="palette"
|
||||||
symbol={paletteIconPreview.definition.symbol}
|
symbol={paletteIconPreview.definition.symbol}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { SymbolSvg } from "./componentSymbols/primitives";
|
|||||||
import { AmesimStep0Symbol, AmesimUd00Symbol } from "./componentSymbols/signals";
|
import { AmesimStep0Symbol, AmesimUd00Symbol } from "./componentSymbols/signals";
|
||||||
import { AmesimPnch012Symbol, AmesimPnch023Symbol } from "./componentSymbols/storage";
|
import { AmesimPnch012Symbol, AmesimPnch023Symbol } from "./componentSymbols/storage";
|
||||||
import type {
|
import type {
|
||||||
|
ComponentSymbolParameters,
|
||||||
ComponentSymbolPortAnchors,
|
ComponentSymbolPortAnchors,
|
||||||
ComponentSymbolPresentation,
|
ComponentSymbolPresentation,
|
||||||
SymbolDefinition,
|
SymbolDefinition,
|
||||||
@@ -38,6 +39,7 @@ type ComponentSymbolProps = {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
surface: ComponentSymbolSurface;
|
surface: ComponentSymbolSurface;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
parameters?: ComponentSymbolParameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CylinderSymbol() {
|
function CylinderSymbol() {
|
||||||
@@ -361,11 +363,13 @@ export function ComponentSymbol({
|
|||||||
symbol,
|
symbol,
|
||||||
surface,
|
surface,
|
||||||
className,
|
className,
|
||||||
|
parameters,
|
||||||
}: ComponentSymbolProps) {
|
}: ComponentSymbolProps) {
|
||||||
const resolvedSymbol: RegisteredComponentSymbol = isRegisteredComponentSymbol(symbol)
|
const resolvedSymbol: RegisteredComponentSymbol = isRegisteredComponentSymbol(symbol)
|
||||||
? symbol
|
? symbol
|
||||||
: "generic";
|
: "generic";
|
||||||
const definition = symbolRegistry[resolvedSymbol];
|
const definition = symbolRegistry[resolvedSymbol];
|
||||||
|
const SymbolRenderer = definition.render;
|
||||||
const rootClassName = [
|
const rootClassName = [
|
||||||
"component-symbol",
|
"component-symbol",
|
||||||
`component-symbol-${surface}`,
|
`component-symbol-${surface}`,
|
||||||
@@ -382,7 +386,7 @@ export function ComponentSymbol({
|
|||||||
data-component-symbol={resolvedSymbol}
|
data-component-symbol={resolvedSymbol}
|
||||||
data-symbol-presentation={definition.presentation}
|
data-symbol-presentation={definition.presentation}
|
||||||
>
|
>
|
||||||
{definition.render()}
|
<SymbolRenderer parameters={parameters} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import { useId } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SYMBOL_ACCENT,
|
SYMBOL_ACCENT,
|
||||||
SYMBOL_FILL,
|
SYMBOL_FILL,
|
||||||
SYMBOL_MUTED,
|
|
||||||
SYMBOL_STROKE,
|
SYMBOL_STROKE,
|
||||||
SymbolSvg,
|
SymbolSvg,
|
||||||
} from "./primitives";
|
} from "./primitives";
|
||||||
|
import type { ComponentSymbolRendererProps } from "./types";
|
||||||
|
|
||||||
const MECHANICAL_LINE_WIDTH = 1.8;
|
const MECHANICAL_LINE_WIDTH = 1.8;
|
||||||
|
const MECMAS21_STROKE = "#00af00";
|
||||||
|
const MECMAS21_HATCH = "#66cf66";
|
||||||
|
const MECMAS21_FILL = "#ffffff";
|
||||||
const PNRP17_STROKE = "#8b134f";
|
const PNRP17_STROKE = "#8b134f";
|
||||||
const PNRP17_FILL = "#cf9db6";
|
const PNRP17_FILL = "#cf9db6";
|
||||||
const PNRP17_LIGHT_FILL = "#f5ebf0";
|
const PNRP17_LIGHT_FILL = "#f5ebf0";
|
||||||
@@ -22,23 +27,24 @@ const mechanicalStroke = {
|
|||||||
function MechanicalSocket({ side }: { side: "left" | "right" }) {
|
function MechanicalSocket({ side }: { side: "left" | "right" }) {
|
||||||
const facingLeft = side === "left";
|
const facingLeft = side === "left";
|
||||||
return (
|
return (
|
||||||
<g {...mechanicalStroke}>
|
<g
|
||||||
<path d={facingLeft ? "M2 24h7" : "M55 24h7"} />
|
fill="none"
|
||||||
<path d={facingLeft ? "M15 19H9v10h6" : "M49 19h6v10h-6"} />
|
stroke={MECMAS21_STROKE}
|
||||||
</g>
|
strokeLinecap="butt"
|
||||||
);
|
strokeLinejoin="miter"
|
||||||
}
|
>
|
||||||
|
|
||||||
function GuideHatching({ y, upward }: { y: number; upward: boolean }) {
|
|
||||||
const direction = upward ? -1 : 1;
|
|
||||||
return (
|
|
||||||
<g stroke={SYMBOL_MUTED} strokeWidth="1">
|
|
||||||
{[14, 22, 30, 38, 46].map((x) => (
|
|
||||||
<path
|
<path
|
||||||
d={`M${x - 3} ${y + direction * 3}L${x + 3} ${y - direction * 3}`}
|
d={facingLeft ? "M2 24h3.2" : "M58.8 24H62"}
|
||||||
key={x}
|
strokeWidth="0.9"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={
|
||||||
|
facingLeft
|
||||||
|
? "M15.1 20.9H6.2v6.2h8.9"
|
||||||
|
: "M48.9 20.9h8.9v6.2h-8.9"
|
||||||
|
}
|
||||||
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -100,44 +106,133 @@ export function AmesimForcSymbol() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AmesimMecmas21Symbol() {
|
export function AmesimMecmas21Symbol({
|
||||||
|
parameters,
|
||||||
|
}: ComponentSymbolRendererProps) {
|
||||||
|
const frictionEnabled = Number(parameters?.useFriction) === 2;
|
||||||
|
const stopType = Number(parameters?.stoptype);
|
||||||
|
const limitsEnabled = stopType === 1 || stopType === 2 || stopType === 3;
|
||||||
|
const showGuides = frictionEnabled || limitsEnabled;
|
||||||
|
const variant = frictionEnabled
|
||||||
|
? limitsEnabled
|
||||||
|
? "friction-limited"
|
||||||
|
: "friction-unlimited"
|
||||||
|
: limitsEnabled
|
||||||
|
? "plain-limited"
|
||||||
|
: "plain-unlimited";
|
||||||
|
const hatchingId = `mecmas21-hatching-${useId().replace(
|
||||||
|
/[^a-zA-Z0-9_-]/g,
|
||||||
|
"",
|
||||||
|
)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SymbolSvg className="component-symbol-amesim-mecmas21" domain="mechanical">
|
<SymbolSvg
|
||||||
<GuideHatching upward y={5} />
|
className="component-symbol-amesim-mecmas21"
|
||||||
<GuideHatching upward={false} y={43} />
|
domain="mechanical"
|
||||||
<path d="M10 8h44M10 40h44" stroke={SYMBOL_MUTED} strokeWidth="1.3" />
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
height="5.05"
|
||||||
|
id={hatchingId}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
width="5.05"
|
||||||
|
>
|
||||||
|
<rect fill={MECMAS21_FILL} height="5.05" width="5.05" />
|
||||||
|
<path
|
||||||
|
d="M-1 1 1-1M0 5.05 5.05 0M4.05 6.05 6.05 4.05"
|
||||||
|
stroke={MECMAS21_HATCH}
|
||||||
|
strokeWidth="1.55"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
data-mecmas-friction={frictionEnabled ? "enabled" : "disabled"}
|
||||||
|
data-mecmas-limits={limitsEnabled ? "finite" : "unlimited"}
|
||||||
|
data-mecmas-variant={variant}
|
||||||
|
>
|
||||||
|
{showGuides ? (
|
||||||
|
<g data-mecmas-part="guides">
|
||||||
|
<rect
|
||||||
|
data-mecmas-part="upper-guide"
|
||||||
|
fill={`url(#${hatchingId})`}
|
||||||
|
height="5.25"
|
||||||
|
stroke={MECMAS21_STROKE}
|
||||||
|
strokeWidth="1.2"
|
||||||
|
width="60"
|
||||||
|
x="2"
|
||||||
|
y="2.6"
|
||||||
|
/>
|
||||||
|
{limitsEnabled ? (
|
||||||
|
<g data-mecmas-part="finite-lower-guide">
|
||||||
|
<path
|
||||||
|
d="M2 32.6H10V39.6H54V32.6H62V45.4H2Z"
|
||||||
|
fill={`url(#${hatchingId})`}
|
||||||
|
stroke={MECMAS21_STROKE}
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
) : (
|
||||||
|
<rect
|
||||||
|
data-mecmas-part="unlimited-lower-guide"
|
||||||
|
fill={`url(#${hatchingId})`}
|
||||||
|
height="5.25"
|
||||||
|
stroke={MECMAS21_STROKE}
|
||||||
|
strokeWidth="1.2"
|
||||||
|
width="60"
|
||||||
|
x="2"
|
||||||
|
y="40.23"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
) : null}
|
||||||
|
{frictionEnabled ? (
|
||||||
|
<path
|
||||||
|
d="M16.49 9.93h4.9m3.81 0h4.9m3.93 0h4.9m3.79 0h4.9M16.49 38.07h4.9m3.81 0h4.9m3.93 0h4.9m3.79 0h4.9"
|
||||||
|
data-mecmas-part="friction-markers"
|
||||||
|
stroke={MECMAS21_STROKE}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeWidth="0.92"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<g data-mecmas-part="core">
|
||||||
<MechanicalSocket side="left" />
|
<MechanicalSocket side="left" />
|
||||||
<MechanicalSocket side="right" />
|
<MechanicalSocket side="right" />
|
||||||
<rect
|
<rect
|
||||||
fill={SYMBOL_FILL}
|
data-mecmas-part="mass"
|
||||||
height="26"
|
fill={MECMAS21_FILL}
|
||||||
rx="1"
|
height="23.1"
|
||||||
stroke={SYMBOL_STROKE}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
width="34"
|
width="34"
|
||||||
x="15"
|
x="15"
|
||||||
y="11"
|
y="12.45"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
fill={SYMBOL_STROKE}
|
fill={MECMAS21_STROKE}
|
||||||
fontFamily="Arial, sans-serif"
|
fontFamily="Arial, sans-serif"
|
||||||
fontSize="12"
|
fontSize="10"
|
||||||
fontWeight="600"
|
fontWeight="500"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="31"
|
x="32"
|
||||||
y="28"
|
y="29.2"
|
||||||
>
|
>
|
||||||
M
|
M
|
||||||
</text>
|
</text>
|
||||||
<path
|
<path
|
||||||
d="M22 18h15m-5-4 5 4-5 4"
|
d="M18.94 18.8h5.88M21.9 16.6V21"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={SYMBOL_ACCENT}
|
stroke={MECMAS21_STROKE}
|
||||||
strokeLinecap="round"
|
strokeLinecap="square"
|
||||||
strokeLinejoin="round"
|
strokeWidth="1.05"
|
||||||
strokeWidth="1.8"
|
|
||||||
/>
|
/>
|
||||||
|
<path
|
||||||
|
d="M45.92 18.81 39.66 15.31 39.54 17.98H26.55v1.74h12.99l.12 2.93Z"
|
||||||
|
fill={MECMAS21_STROKE}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</SymbolSvg>
|
</SymbolSvg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ComponentType } from "react";
|
||||||
|
|
||||||
export type ComponentSymbolPresentation = "framed" | "bare";
|
export type ComponentSymbolPresentation = "framed" | "bare";
|
||||||
|
|
||||||
@@ -11,7 +11,15 @@ export type ComponentSymbolPortAnchors = Partial<
|
|||||||
Record<"left" | "right", readonly ComponentSymbolPortAnchor[]>
|
Record<"left" | "right", readonly ComponentSymbolPortAnchor[]>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ComponentSymbolParameters = Readonly<
|
||||||
|
Record<string, number | string>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ComponentSymbolRendererProps = {
|
||||||
|
parameters?: ComponentSymbolParameters;
|
||||||
|
};
|
||||||
|
|
||||||
export type SymbolDefinition = {
|
export type SymbolDefinition = {
|
||||||
presentation: ComponentSymbolPresentation;
|
presentation: ComponentSymbolPresentation;
|
||||||
render: () => ReactNode;
|
render: ComponentType<ComponentSymbolRendererProps>;
|
||||||
};
|
};
|
||||||
@@ -1637,6 +1637,28 @@ textarea {
|
|||||||
opacity: 0.18;
|
opacity: 0.18;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.port-index-label {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 2px;
|
||||||
|
border: 0;
|
||||||
|
color: #111827;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: center;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.flow-canvas .react-flow__edge-path,
|
.flow-canvas .react-flow__edge-path,
|
||||||
.flow-canvas .react-flow__connection-path {
|
.flow-canvas .react-flow__connection-path {
|
||||||
stroke: #64748b;
|
stroke: #64748b;
|
||||||
|
|||||||
@@ -96,6 +96,20 @@ 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_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") {
|
if (modelType === "amesim_pnrp17") {
|
||||||
return [
|
return [
|
||||||
physicalPort("port_1", "left", 10),
|
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) {
|
async function locatorCenter(locator: Locator) {
|
||||||
const bounds = await locator.boundingBox();
|
const bounds = await locator.boundingBox();
|
||||||
expect(bounds).not.toBeNull();
|
expect(bounds).not.toBeNull();
|
||||||
@@ -273,7 +384,43 @@ test.beforeEach(async ({ page }) => {
|
|||||||
order: Object.keys(categoryLabels).indexOf(item.category) + 1,
|
order: Object.keys(categoryLabels).indexOf(item.category) + 1,
|
||||||
},
|
},
|
||||||
ports: portsForSymbol(item.modelType),
|
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 }) => {
|
test("PNRP17 图标匹配参考结构并将五个端口放在对应接口", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
@@ -329,6 +606,89 @@ test("PNRP17 图标匹配参考结构并将五个端口放在对应接口", asyn
|
|||||||
).toHaveClass(/react-flow__handle-bottom/);
|
).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 ({
|
test("AMESim canvas nodes use icon anchors and highlight only compatible free ports", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ test("未知图形元件使用带端口和名称的默认节点,并将动作
|
|||||||
await expect(node.locator(".node-symbol-anchor")).toHaveCount(0);
|
await expect(node.locator(".node-symbol-anchor")).toHaveCount(0);
|
||||||
await expect(node.locator(".node-label")).toHaveText("generic_sensor_1");
|
await expect(node.locator(".node-label")).toHaveText("generic_sensor_1");
|
||||||
await expect(node.locator(".port-handle")).toHaveCount(2);
|
await expect(node.locator(".port-handle")).toHaveCount(2);
|
||||||
|
await expect(
|
||||||
|
node.locator('.port-index-label[data-port-name="port_a"]'),
|
||||||
|
).toHaveText("1");
|
||||||
|
await expect(
|
||||||
|
node.locator('.port-index-label[data-port-name="port_b"]'),
|
||||||
|
).toHaveText("2");
|
||||||
|
|
||||||
const consolePanel = page.getByRole("complementary", {
|
const consolePanel = page.getByRole("complementary", {
|
||||||
name: "仿真控制台",
|
name: "仿真控制台",
|
||||||
|
|||||||
Reference in new issue
Block a user