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

- 引入因果坐标内核、热流体恢复和递进长时回归\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

+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,
};