完善通用求解器回归与前端交互
- 引入因果坐标内核、热流体恢复和递进长时回归\n- 完善正交连线、线桥、视图保持与结果曲线缩放\n- 补充依赖约束、CI、测试基线和北京时间更新日志
This commit is contained in:
1 parent
143e8dd309
commit
b435daecf2
65 files changed
+172271
-701
No files matched your search
@@ -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,
|
||||
};
|
||||
Reference in new issue
Block a user