完善通用求解器回归与前端交互

- 引入因果坐标内核、热流体恢复和递进长时回归\n- 完善正交连线、线桥、视图保持与结果曲线缩放\n- 补充依赖约束、CI、测试基线和北京时间更新日志
This commit is contained in:
lujingze committed 2026-08-18 06:42:07 +00:00
1 parent 143e8dd309
commit b435daecf2
65 files changed
+172271 -701

No files matched your search

+889 -288
View File
File diff suppressed because it is too large. Load diff
+224 -17
View File
@@ -1,18 +1,59 @@
import {
useEffect,
useRef,
type PointerEvent as ReactPointerEvent,
} from "react";
import {
BaseEdge,
getSmoothStepPath,
useReactFlow,
type EdgeProps,
type EdgeTypes,
} from "@xyflow/react";
import {
createOrthogonalSegmentDetour,
edgeSegmentAxis,
moveOrthogonalCorner,
moveOrthogonalSegment,
orthogonalEdgePoints,
orthogonalPolylinePath,
type EdgeRouteData,
type EdgeRoutePoint,
} from "./edgeRouting";
export const CONTACT_AWARE_EDGE_TYPE = "contact-aware";
export function ContactAwareEdge({
type RouteDrag = {
basePoints: EdgeRoutePoint[];
cleanup: () => void;
kind: "corner" | "detour" | "segment";
pointerId: number;
routeIndex: number;
startPointer: EdgeRoutePoint;
};
function longestSegmentIndex(points: EdgeRoutePoint[]) {
let bestIndex = 0;
let bestLength = -1;
for (let index = 0; index < points.length - 1; index += 1) {
const length = Math.hypot(
points[index + 1].x - points[index].x,
points[index + 1].y - points[index].y,
);
if (length > bestLength) {
bestIndex = index;
bestLength = length;
}
}
return bestIndex;
}
function RoutedEdge({
data,
id,
interactionWidth,
markerEnd,
markerStart,
selected,
sourcePosition,
sourceX,
sourceY,
@@ -21,32 +62,198 @@ export function ContactAwareEdge({
targetX,
targetY,
}: EdgeProps) {
const isContactEdge = data?.isContactEdge === true;
if (isContactEdge) {
return null;
}
const [edgePath] = getSmoothStepPath({
const reactFlow = useReactFlow();
const dragRef = useRef<RouteDrag | null>(null);
const edgeData = (data ?? {}) as EdgeRouteData;
useEffect(
() => () => {
dragRef.current?.cleanup();
dragRef.current = null;
},
[],
);
const points = orthogonalEdgePoints({
routePoints: edgeData.routePoints,
sourcePosition,
sourceX,
sourceY,
targetPosition,
targetX,
targetY,
borderRadius: 0,
});
const edgePath = orthogonalPolylinePath(points, edgeData.crossingJumps);
const beginRouteDrag = (
event: ReactPointerEvent<SVGElement>,
basePoints: EdgeRoutePoint[],
kind: RouteDrag["kind"],
routeIndex: number,
) => {
event.preventDefault();
event.stopPropagation();
dragRef.current?.cleanup();
const drag: RouteDrag = {
basePoints: basePoints.map((point) => ({ ...point })),
cleanup: () => undefined,
kind,
pointerId: event.pointerId,
routeIndex,
startPointer: reactFlow.screenToFlowPosition(
{ x: event.clientX, y: event.clientY },
{ snapToGrid: false },
),
};
const moveRoute = (pointerEvent: PointerEvent) => {
if (
dragRef.current !== drag ||
pointerEvent.pointerId !== drag.pointerId
) {
return;
}
pointerEvent.preventDefault();
pointerEvent.stopPropagation();
const pointer = reactFlow.screenToFlowPosition(
{ x: pointerEvent.clientX, y: pointerEvent.clientY },
{ snapToGrid: false },
);
const delta = {
x: pointer.x - drag.startPointer.x,
y: pointer.y - drag.startPointer.y,
};
const nextPoints =
drag.kind === "corner"
? moveOrthogonalCorner(drag.basePoints, drag.routeIndex, delta)
: drag.kind === "detour"
? createOrthogonalSegmentDetour(
drag.basePoints,
drag.routeIndex,
delta,
)
: moveOrthogonalSegment(
drag.basePoints,
drag.routeIndex,
delta,
);
edgeData.onRoutePointsChange?.(id, nextPoints.slice(1, -1));
};
const finishRoute = (pointerEvent: PointerEvent) => {
if (
dragRef.current !== drag ||
pointerEvent.pointerId !== drag.pointerId
) {
return;
}
pointerEvent.preventDefault();
pointerEvent.stopPropagation();
drag.cleanup();
dragRef.current = null;
};
drag.cleanup = () => {
window.removeEventListener("pointermove", moveRoute);
window.removeEventListener("pointerup", finishRoute);
window.removeEventListener("pointercancel", finishRoute);
};
dragRef.current = drag;
window.addEventListener("pointermove", moveRoute, { passive: false });
window.addEventListener("pointerup", finishRoute);
window.addEventListener("pointercancel", finishRoute);
edgeData.onRouteEditStart?.(id);
};
const editable = selected && edgeData.editable === true;
const internalSegments = points
.slice(0, -1)
.map((start, index) => ({ end: points[index + 1], index, start }))
.filter(({ index }) => index > 0 && index < points.length - 2);
const internalCorners = points
.map((point, index) => ({ index, point }))
.filter(({ index }) => index > 0 && index < points.length - 1);
const fallbackIndex = longestSegmentIndex(points);
const fallbackStart = points[fallbackIndex];
const fallbackEnd = points[fallbackIndex + 1];
return (
<BaseEdge
id={id}
interactionWidth={interactionWidth}
markerEnd={markerEnd}
markerStart={markerStart}
path={edgePath}
style={style}
/>
<>
<BaseEdge
id={id}
interactionWidth={interactionWidth}
markerEnd={markerEnd}
markerStart={markerStart}
path={edgePath}
style={style}
/>
{editable
? internalSegments.map(({ end, index, start }) => (
<g
className="manual-edge-segment-control"
key={`${id}-segment-${index}`}
>
<line
aria-label={`调整连接线段 ${index}`}
className={`manual-edge-segment-handle ${edgeSegmentAxis(start, end)}`}
data-edge-id={id}
data-segment-index={index}
onPointerDown={(event) =>
beginRouteDrag(event, points, "segment", index)
}
x1={start.x}
x2={end.x}
y1={start.y}
y2={end.y}
/>
<line
aria-hidden="true"
className="manual-edge-segment-guide"
x1={start.x}
x2={end.x}
y1={start.y}
y2={end.y}
/>
</g>
))
: null}
{editable
? internalCorners.map(({ index, point }) => (
<circle
aria-label={`双向调整连接线拐点 ${index}`}
className="manual-edge-corner-handle"
cx={point.x}
cy={point.y}
data-corner-index={index}
data-edge-id={id}
key={`${id}-corner-${index}`}
onPointerDown={(event) =>
beginRouteDrag(event, points, "corner", index)
}
r={4.5}
/>
))
: null}
{editable && internalCorners.length === 0 && fallbackEnd ? (
<circle
aria-label="双向调整连接线路由"
className="manual-edge-route-handle"
cx={(fallbackStart.x + fallbackEnd.x) / 2}
cy={(fallbackStart.y + fallbackEnd.y) / 2}
data-edge-id={id}
onPointerDown={(event) =>
beginRouteDrag(event, points, "detour", fallbackIndex)
}
r={5}
/>
) : null}
</>
);
}
export function ContactAwareEdge(props: EdgeProps) {
if (props.data?.isContactEdge === true) {
return null;
}
return <RoutedEdge {...props} />;
}
export const contactAwareEdgeTypes: EdgeTypes = {
[CONTACT_AWARE_EDGE_TYPE]: ContactAwareEdge,
};
File diff suppressed because it is too large. Load diff
+536
View File
@@ -0,0 +1,536 @@
import { Position } from "@xyflow/react";
export type EdgeRoutePoint = {
x: number;
y: number;
};
export type EdgeCrossingJump = EdgeRoutePoint & {
orientation: "horizontal" | "vertical";
};
export type EdgeRouteData = {
crossingJumps?: EdgeCrossingJump[];
editable?: boolean;
isContactEdge?: boolean;
onRouteEditStart?: (edgeId: string) => void;
onRoutePointsChange?: (edgeId: string, routePoints: EdgeRoutePoint[]) => void;
routePoints?: EdgeRoutePoint[];
};
export type EdgePointAxis = "horizontal" | "vertical";
const ROUTE_EPSILON = 0.01;
const CROSSING_JUMP_RADIUS = 6;
export function edgeAxisForPosition(position: Position): EdgePointAxis {
return position === Position.Left || position === Position.Right
? "horizontal"
: "vertical";
}
export function edgePointIsFinite(point: EdgeRoutePoint) {
return Number.isFinite(point.x) && Number.isFinite(point.y);
}
export function edgePointsMatch(
first: EdgeRoutePoint,
second: EdgeRoutePoint,
epsilon = ROUTE_EPSILON,
) {
return (
Math.abs(first.x - second.x) <= epsilon &&
Math.abs(first.y - second.y) <= epsilon
);
}
function appendDistinctPoint(
points: EdgeRoutePoint[],
point: EdgeRoutePoint,
) {
if (!edgePointIsFinite(point)) {
return;
}
const previous = points.at(-1);
if (!previous || !edgePointsMatch(previous, point)) {
points.push({ x: point.x, y: point.y });
}
}
export function simplifyOrthogonalPoints(points: EdgeRoutePoint[]) {
const distinct: EdgeRoutePoint[] = [];
points.forEach((point) => appendDistinctPoint(distinct, point));
if (distinct.length <= 2) {
return distinct;
}
const simplified: EdgeRoutePoint[] = [distinct[0]];
for (let index = 1; index < distinct.length - 1; index += 1) {
const previous = simplified.at(-1) as EdgeRoutePoint;
const current = distinct[index];
const next = distinct[index + 1];
const collinearX =
Math.abs(previous.x - current.x) <= ROUTE_EPSILON &&
Math.abs(current.x - next.x) <= ROUTE_EPSILON;
const collinearY =
Math.abs(previous.y - current.y) <= ROUTE_EPSILON &&
Math.abs(current.y - next.y) <= ROUTE_EPSILON;
if (!collinearX && !collinearY) {
simplified.push(current);
}
}
simplified.push(distinct.at(-1) as EdgeRoutePoint);
return simplified;
}
function appendOrthogonalLeg(
points: EdgeRoutePoint[],
target: EdgeRoutePoint,
firstAxis: EdgePointAxis,
) {
const source = points.at(-1);
if (!source || edgePointsMatch(source, target)) {
appendDistinctPoint(points, target);
return;
}
const changesX = Math.abs(source.x - target.x) > ROUTE_EPSILON;
const changesY = Math.abs(source.y - target.y) > ROUTE_EPSILON;
if (changesX && changesY) {
appendDistinctPoint(
points,
firstAxis === "horizontal"
? { x: target.x, y: source.y }
: { x: source.x, y: target.y },
);
}
appendDistinctPoint(points, target);
}
function appendEndpointAwareLeg(
points: EdgeRoutePoint[],
target: EdgeRoutePoint,
sourceAxis: EdgePointAxis,
targetAxis: EdgePointAxis,
) {
const source = points.at(-1);
if (!source || edgePointsMatch(source, target)) {
appendDistinctPoint(points, target);
return;
}
const changesX = Math.abs(source.x - target.x) > ROUTE_EPSILON;
const changesY = Math.abs(source.y - target.y) > ROUTE_EPSILON;
if (!changesX || !changesY) {
appendDistinctPoint(points, target);
return;
}
if (sourceAxis !== targetAxis) {
appendDistinctPoint(
points,
sourceAxis === "horizontal"
? { x: target.x, y: source.y }
: { x: source.x, y: target.y },
);
} else if (sourceAxis === "horizontal") {
const middleX = (source.x + target.x) / 2;
appendDistinctPoint(points, { x: middleX, y: source.y });
appendDistinctPoint(points, { x: middleX, y: target.y });
} else {
const middleY = (source.y + target.y) / 2;
appendDistinctPoint(points, { x: source.x, y: middleY });
appendDistinctPoint(points, { x: target.x, y: middleY });
}
appendDistinctPoint(points, target);
}
/** Builds a persisted orthogonal route through each exact user waypoint. */
export function buildOrthogonalRoutePoints(
source: EdgeRoutePoint,
target: EdgeRoutePoint,
sourcePosition: Position,
targetPosition: Position,
waypoints: EdgeRoutePoint[] = [],
) {
const sourceAxis = edgeAxisForPosition(sourcePosition);
const targetAxis = edgeAxisForPosition(targetPosition);
const points: EdgeRoutePoint[] = [{ ...source }];
let nextAxis = sourceAxis;
waypoints.filter(edgePointIsFinite).forEach((waypoint) => {
appendOrthogonalLeg(points, waypoint, nextAxis);
nextAxis = nextAxis === "horizontal" ? "vertical" : "horizontal";
});
appendEndpointAwareLeg(points, target, nextAxis, targetAxis);
return simplifyOrthogonalPoints(points).slice(1, -1);
}
/** Returns the rendered polyline, including both live port endpoints. */
export function orthogonalEdgePoints({
routePoints,
sourcePosition,
sourceX,
sourceY,
targetPosition,
targetX,
targetY,
}: {
routePoints?: EdgeRoutePoint[];
sourcePosition: Position;
sourceX: number;
sourceY: number;
targetPosition: Position;
targetX: number;
targetY: number;
}) {
const source = { x: sourceX, y: sourceY };
const target = { x: targetX, y: targetY };
const storedPoints = routePoints?.filter(edgePointIsFinite) ?? [];
if (storedPoints.length === 0) {
return [
source,
...buildOrthogonalRoutePoints(
source,
target,
sourcePosition,
targetPosition,
),
target,
];
}
const points: EdgeRoutePoint[] = [source];
const first = storedPoints[0];
if (
Math.abs(source.x - first.x) > ROUTE_EPSILON &&
Math.abs(source.y - first.y) > ROUTE_EPSILON
) {
appendDistinctPoint(
points,
edgeAxisForPosition(sourcePosition) === "horizontal"
? { x: first.x, y: source.y }
: { x: source.x, y: first.y },
);
}
storedPoints.forEach((point) => appendDistinctPoint(points, point));
const last = points.at(-1) as EdgeRoutePoint;
if (
Math.abs(last.x - target.x) > ROUTE_EPSILON &&
Math.abs(last.y - target.y) > ROUTE_EPSILON
) {
appendDistinctPoint(
points,
edgeAxisForPosition(targetPosition) === "horizontal"
? { x: last.x, y: target.y }
: { x: target.x, y: last.y },
);
}
appendDistinctPoint(points, target);
return simplifyOrthogonalPoints(points);
}
function pointOnSegmentInterior(
point: EdgeRoutePoint,
start: EdgeRoutePoint,
end: EdgeRoutePoint,
) {
const segmentLength = Math.hypot(end.x - start.x, end.y - start.y);
const fromStart = Math.hypot(point.x - start.x, point.y - start.y);
const fromEnd = Math.hypot(point.x - end.x, point.y - end.y);
return (
segmentLength > CROSSING_JUMP_RADIUS * 2 + 2 &&
fromStart > CROSSING_JUMP_RADIUS + 1 &&
fromEnd > CROSSING_JUMP_RADIUS + 1 &&
Math.abs(fromStart + fromEnd - segmentLength) <= 0.2
);
}
export function edgeSegmentAxis(
start: EdgeRoutePoint,
end: EdgeRoutePoint,
): EdgePointAxis {
return Math.abs(start.x - end.x) >= Math.abs(start.y - end.y)
? "horizontal"
: "vertical";
}
export function orthogonalPolylinePath(
points: EdgeRoutePoint[],
crossingJumps: EdgeCrossingJump[] = [],
) {
if (points.length === 0) {
return "";
}
let path = `M ${points[0].x} ${points[0].y}`;
for (let index = 0; index < points.length - 1; index += 1) {
const start = points[index];
const end = points[index + 1];
const orientation = edgeSegmentAxis(start, end);
const horizontal = orientation === "horizontal";
const direction = horizontal
? Math.sign(end.x - start.x)
: Math.sign(end.y - start.y);
if (direction === 0) {
continue;
}
const jumps = crossingJumps
.filter(
(jump) =>
jump.orientation === orientation &&
pointOnSegmentInterior(jump, start, end),
)
.sort((first, second) =>
horizontal
? (first.x - second.x) * direction
: (first.y - second.y) * direction,
);
jumps.forEach((jump) => {
const before = horizontal
? { x: jump.x - direction * CROSSING_JUMP_RADIUS, y: start.y }
: { x: start.x, y: jump.y - direction * CROSSING_JUMP_RADIUS };
const after = horizontal
? { x: jump.x + direction * CROSSING_JUMP_RADIUS, y: start.y }
: { x: start.x, y: jump.y + direction * CROSSING_JUMP_RADIUS };
path += ` L ${before.x} ${before.y}`;
path += horizontal
? ` Q ${jump.x} ${jump.y - CROSSING_JUMP_RADIUS * 1.8} ${after.x} ${after.y}`
: ` Q ${jump.x + CROSSING_JUMP_RADIUS * 1.8} ${jump.y} ${after.x} ${after.y}`;
});
path += ` L ${end.x} ${end.y}`;
}
return path;
}
export type RoutedEdgeCrossingInput = {
id: string;
nodeIds: readonly [string, string];
points: EdgeRoutePoint[];
};
function valueInsideSegment(
value: number,
first: number,
second: number,
margin = CROSSING_JUMP_RADIUS + 2,
) {
const minimum = Math.min(first, second) + margin;
const maximum = Math.max(first, second) - margin;
return value > minimum && value < maximum;
}
/**
* Derives display-only circuit-style jump arcs. The horizontal route is chosen
* consistently, so recalculation never makes the bridge alternate between
* lines as the user edits nearby geometry.
*/
export function detectEdgeCrossingJumps(
edges: RoutedEdgeCrossingInput[],
) {
const jumpsByEdgeId = new Map<string, EdgeCrossingJump[]>();
for (let firstIndex = 0; firstIndex < edges.length; firstIndex += 1) {
const first = edges[firstIndex];
for (let secondIndex = firstIndex + 1; secondIndex < edges.length; secondIndex += 1) {
const second = edges[secondIndex];
for (let firstSegment = 0; firstSegment < first.points.length - 1; firstSegment += 1) {
const firstStart = first.points[firstSegment];
const firstEnd = first.points[firstSegment + 1];
const firstAxis = edgeSegmentAxis(firstStart, firstEnd);
for (let secondSegment = 0; secondSegment < second.points.length - 1; secondSegment += 1) {
const secondStart = second.points[secondSegment];
const secondEnd = second.points[secondSegment + 1];
const secondAxis = edgeSegmentAxis(secondStart, secondEnd);
if (firstAxis === secondAxis) {
continue;
}
const horizontal =
firstAxis === "horizontal"
? { edge: first, start: firstStart, end: firstEnd }
: { edge: second, start: secondStart, end: secondEnd };
const vertical =
firstAxis === "vertical"
? { start: firstStart, end: firstEnd }
: { start: secondStart, end: secondEnd };
const crossing = { x: vertical.start.x, y: horizontal.start.y };
if (
!valueInsideSegment(
crossing.x,
horizontal.start.x,
horizontal.end.x,
) ||
!valueInsideSegment(
crossing.y,
vertical.start.y,
vertical.end.y,
)
) {
continue;
}
const current = jumpsByEdgeId.get(horizontal.edge.id) ?? [];
if (
!current.some(
(jump) =>
Math.abs(jump.x - crossing.x) <= ROUTE_EPSILON &&
Math.abs(jump.y - crossing.y) <= ROUTE_EPSILON,
)
) {
current.push({ ...crossing, orientation: "horizontal" });
jumpsByEdgeId.set(horizontal.edge.id, current);
}
}
}
}
}
return jumpsByEdgeId;
}
/**
* Moves an internal orthogonal segment along its perpendicular axis. Both live
* endpoints remain untouched; the neighbouring perpendicular legs absorb the
* displacement.
*/
export function moveOrthogonalSegment(
points: EdgeRoutePoint[],
segmentIndex: number,
delta: EdgeRoutePoint,
) {
const nextPoints = points.map((point) => ({ ...point }));
if (segmentIndex <= 0 || segmentIndex >= nextPoints.length - 2) {
return nextPoints;
}
const start = nextPoints[segmentIndex];
const end = nextPoints[segmentIndex + 1];
if (edgeSegmentAxis(start, end) === "horizontal") {
start.y += delta.y;
end.y += delta.y;
} else {
start.x += delta.x;
end.x += delta.x;
}
return simplifyOrthogonalPoints(nextPoints);
}
/**
* Moves an internal bend in both axes while preserving an orthogonal route.
* Moving the bend also slides its two neighbouring bends along their existing
* perpendicular legs, which keeps the two live port endpoints fixed.
*/
export function moveOrthogonalCorner(
points: EdgeRoutePoint[],
cornerIndex: number,
delta: EdgeRoutePoint,
) {
const nextPoints = points.map((point) => ({ ...point }));
if (cornerIndex <= 0 || cornerIndex >= nextPoints.length - 1) {
return nextPoints;
}
const previous = nextPoints[cornerIndex - 1];
const corner = nextPoints[cornerIndex];
const incomingAxis = edgeSegmentAxis(previous, corner);
const outgoingAxis = edgeSegmentAxis(
corner,
nextPoints[cornerIndex + 1],
);
corner.x += delta.x;
corner.y += delta.y;
if (cornerIndex > 1) {
if (incomingAxis === "horizontal") {
previous.y = corner.y;
} else {
previous.x = corner.x;
}
} else if (incomingAxis === "horizontal") {
const middleX =
Math.abs(previous.x - corner.x) > ROUTE_EPSILON
? (previous.x + corner.x) / 2
: previous.x + 20;
nextPoints.splice(
cornerIndex,
0,
{ x: middleX, y: previous.y },
{ x: middleX, y: corner.y },
);
cornerIndex += 2;
} else {
const middleY =
Math.abs(previous.y - corner.y) > ROUTE_EPSILON
? (previous.y + corner.y) / 2
: previous.y + 20;
nextPoints.splice(
cornerIndex,
0,
{ x: previous.x, y: middleY },
{ x: corner.x, y: middleY },
);
cornerIndex += 2;
}
const movedCorner = nextPoints[cornerIndex];
const following = nextPoints[cornerIndex + 1];
if (cornerIndex < nextPoints.length - 2) {
if (outgoingAxis === "horizontal") {
following.y = movedCorner.y;
} else {
following.x = movedCorner.x;
}
} else if (outgoingAxis === "horizontal") {
const middleX =
Math.abs(movedCorner.x - following.x) > ROUTE_EPSILON
? (movedCorner.x + following.x) / 2
: following.x - 20;
nextPoints.splice(
cornerIndex + 1,
0,
{ x: middleX, y: movedCorner.y },
{ x: middleX, y: following.y },
);
} else {
const middleY =
Math.abs(movedCorner.y - following.y) > ROUTE_EPSILON
? (movedCorner.y + following.y) / 2
: following.y - 20;
nextPoints.splice(
cornerIndex + 1,
0,
{ x: movedCorner.x, y: middleY },
{ x: following.x, y: middleY },
);
}
return simplifyOrthogonalPoints(nextPoints);
}
/** Creates a movable dogleg when a selected route is still a straight line. */
export function createOrthogonalSegmentDetour(
points: EdgeRoutePoint[],
segmentIndex: number,
delta: EdgeRoutePoint,
) {
const start = points[segmentIndex];
const end = points[segmentIndex + 1];
if (!start || !end) {
return points.map((point) => ({ ...point }));
}
const firstThird = {
x: start.x + (end.x - start.x) / 3,
y: start.y + (end.y - start.y) / 3,
};
const secondThird = {
x: start.x + ((end.x - start.x) * 2) / 3,
y: start.y + ((end.y - start.y) * 2) / 3,
};
const detour =
edgeSegmentAxis(start, end) === "horizontal"
? [
{ x: firstThird.x + delta.x, y: start.y },
{ x: firstThird.x + delta.x, y: start.y + delta.y },
{ x: secondThird.x + delta.x, y: end.y + delta.y },
{ x: secondThird.x + delta.x, y: end.y },
]
: [
{ x: start.x, y: firstThird.y + delta.y },
{ x: start.x + delta.x, y: firstThird.y + delta.y },
{ x: end.x + delta.x, y: secondThird.y + delta.y },
{ x: end.x, y: secondThird.y + delta.y },
];
return simplifyOrthogonalPoints([
...points.slice(0, segmentIndex + 1),
...detour,
...points.slice(segmentIndex + 1),
]);
}
+130
View File
@@ -3647,3 +3647,133 @@ textarea {
padding-right: 8px;
}
}
/* Result chart viewport controls and AMESim-style box zoom. */
.result-chart-window-header button:disabled,
.result-chart-window-header button:disabled:hover {
border-color: transparent;
color: #8a97a6;
background: transparent;
cursor: default;
opacity: 0.38;
}
.result-chart-window-header button.zoom.active {
border-color: #74a9d7;
color: #0f5f9f;
background: #e8f3fc;
}
.result-chart-body > svg.zoom-enabled {
cursor: crosshair;
}
.result-chart-body > svg.zoom-enabled[data-panning="true"],
.result-chart-body > svg.zoom-enabled[data-panning="true"] .result-chart-zoom-axis-hit {
cursor: grabbing;
}
.result-chart-zoom-axis-hit {
pointer-events: all;
}
.result-chart-zoom-axis-hit.x {
cursor: ew-resize;
}
.result-chart-zoom-axis-hit.y {
cursor: ns-resize;
}
.result-chart-zoom-axis-hit:focus {
outline: none;
}
.result-chart-body > svg:focus,
.result-chart-body > svg:focus-visible {
outline: none;
}
.result-chart-zoom-selection {
fill: rgba(29, 111, 184, 0.16);
stroke: #1d6fb8;
stroke-width: 1;
stroke-dasharray: 4 3;
vector-effect: non-scaling-stroke;
}
/* AMESim-style click routing, segment editing and circuit crossing bridges. */
.manual-connection-draft {
position: absolute;
top: 0;
left: 0;
overflow: visible;
pointer-events: none;
z-index: 7;
}
.manual-connection-draft .react-flow__connection-path {
fill: none;
stroke: #1675c1;
stroke-width: 2;
stroke-dasharray: 5 4;
vector-effect: non-scaling-stroke;
}
.manual-connection-waypoint {
fill: #ffffff;
stroke: #1675c1;
stroke-width: 1.5;
vector-effect: non-scaling-stroke;
}
.manual-edge-segment-handle {
stroke: transparent;
stroke-width: 14;
pointer-events: stroke;
touch-action: none;
vector-effect: non-scaling-stroke;
}
.manual-edge-segment-guide {
stroke: rgba(22, 117, 193, 0.34);
stroke-width: 1.25;
stroke-dasharray: 4 3;
pointer-events: none;
vector-effect: non-scaling-stroke;
}
.manual-edge-segment-handle:hover + .manual-edge-segment-guide,
.manual-edge-segment-handle:active + .manual-edge-segment-guide {
stroke: rgba(15, 95, 159, 0.78);
stroke-width: 2;
}
.manual-edge-segment-handle.horizontal {
cursor: ns-resize;
}
.manual-edge-segment-handle.vertical {
cursor: ew-resize;
}
.manual-edge-route-handle,
.manual-edge-corner-handle {
fill: #ffffff;
stroke: #1675c1;
stroke-width: 2;
cursor: move;
pointer-events: all;
touch-action: none;
vector-effect: non-scaling-stroke;
}
.manual-edge-corner-handle:hover,
.manual-edge-route-handle:hover {
fill: #d9edff;
stroke: #0f5f9f;
}
.flow-canvas.connection-planning .react-flow__pane {
cursor: crosshair;
}