mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 00:32:39 +08:00
Fix empty world map by auto-fitting after load and sampling NEs across the extent.
World coords sit far outside the default viewport, so opening the flat map looked blank; also lower minZoom and stride-sample so the overview is visible without a truncate banner. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
48b10656d2
commit
af331e92ca
2 changed files with 65 additions and 14 deletions
|
|
@ -525,6 +525,9 @@ def get_flat_view_graph(
|
||||||
)
|
)
|
||||||
if region_folder_ids is not None:
|
if region_folder_ids is not None:
|
||||||
q = q.filter(TopoFabricNode.region_folder_id.in_(list(region_folder_ids)))
|
q = q.filter(TopoFabricNode.region_folder_id.in_(list(region_folder_ids)))
|
||||||
|
|
||||||
|
truncated = False
|
||||||
|
reason = ""
|
||||||
if sbn_id.strip():
|
if sbn_id.strip():
|
||||||
sid = sbn_id.strip()
|
sid = sbn_id.strip()
|
||||||
# Attr JSON filter is rare; keep Python filter but only after a bounded fetch.
|
# Attr JSON filter is rare; keep Python filter but only after a bounded fetch.
|
||||||
|
|
@ -535,16 +538,34 @@ def get_flat_view_graph(
|
||||||
if str((n.attrs or {}).get("ume_sbn_id") or "") == sid
|
if str((n.attrs or {}).get("ume_sbn_id") or "") == sid
|
||||||
or str(n.region_folder_id or "") == sid
|
or str(n.region_folder_id or "") == sid
|
||||||
]
|
]
|
||||||
|
if len(nodes) > WORLD_NODE_SOFT_CAP:
|
||||||
|
nodes = nodes[:WORLD_NODE_SOFT_CAP]
|
||||||
|
truncated = True
|
||||||
|
reason = "too_many_viewport_nodes"
|
||||||
else:
|
else:
|
||||||
# Cap in SQL so we never hydrate 15k ORM rows for a single browser canvas.
|
# Cap so we never hydrate 15k ORM rows. Stride across world-order so the
|
||||||
nodes = q.order_by(TopoFabricNode.id.asc()).limit(WORLD_NODE_SOFT_CAP + 1).all()
|
# overview is spread out (id-prefix order clumps one corner of the map).
|
||||||
|
id_rows = (
|
||||||
truncated = False
|
q.with_entities(TopoFabricNode.id)
|
||||||
reason = ""
|
.order_by(TopoFabricNode.world_x.asc(), TopoFabricNode.world_y.asc())
|
||||||
if len(nodes) > WORLD_NODE_SOFT_CAP:
|
.all()
|
||||||
nodes = nodes[:WORLD_NODE_SOFT_CAP]
|
)
|
||||||
truncated = True
|
total = len(id_rows)
|
||||||
reason = "too_many_viewport_nodes"
|
if total <= WORLD_NODE_SOFT_CAP:
|
||||||
|
pick_ids = [str(r[0]) for r in id_rows]
|
||||||
|
else:
|
||||||
|
stride = max(1, total // WORLD_NODE_SOFT_CAP)
|
||||||
|
pick_ids = [str(r[0]) for r in id_rows[::stride][:WORLD_NODE_SOFT_CAP]]
|
||||||
|
truncated = True
|
||||||
|
reason = "too_many_viewport_nodes"
|
||||||
|
nodes = (
|
||||||
|
db.query(TopoFabricNode).filter(TopoFabricNode.id.in_(pick_ids)).all()
|
||||||
|
if pick_ids
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
# Preserve world-order for stable layout / fitView.
|
||||||
|
order = {nid: i for i, nid in enumerate(pick_ids)}
|
||||||
|
nodes.sort(key=lambda n: order.get(str(n.id), 0))
|
||||||
|
|
||||||
fids = [n.id for n in nodes]
|
fids = [n.id for n in nodes]
|
||||||
fid_set = set(fids)
|
fid_set = set(fids)
|
||||||
|
|
|
||||||
|
|
@ -1095,6 +1095,8 @@ export function TopologyPage() {
|
||||||
const dirtyRef = useRef(false);
|
const dirtyRef = useRef(false);
|
||||||
const appliedMapIdRef = useRef("");
|
const appliedMapIdRef = useRef("");
|
||||||
const appliedGraphFpRef = useRef("");
|
const appliedGraphFpRef = useRef("");
|
||||||
|
/** Fit once after graph hydrate — world coords are far outside the default viewport. */
|
||||||
|
const pendingFitRef = useRef(false);
|
||||||
const nodesRef = useRef<Node<NeNodeData>[]>([]);
|
const nodesRef = useRef<Node<NeNodeData>[]>([]);
|
||||||
const historyRef = useRef<HistorySnap[]>([]);
|
const historyRef = useRef<HistorySnap[]>([]);
|
||||||
const redoRef = useRef<HistorySnap[]>([]);
|
const redoRef = useRef<HistorySnap[]>([]);
|
||||||
|
|
@ -1337,7 +1339,10 @@ export function TopologyPage() {
|
||||||
return { drill, flatView };
|
return { drill, flatView };
|
||||||
}, [activeRegion]);
|
}, [activeRegion]);
|
||||||
|
|
||||||
const isWorldFlatCanvas = isWorldFlatViewName(activeView?.name);
|
const isWorldFlatCanvas =
|
||||||
|
isWorldFlatViewName(activeView?.name) ||
|
||||||
|
isWorldFlatViewName(graphQuery.data?.view?.name) ||
|
||||||
|
Boolean(graphQuery.data?.view?.filter?.world_flat);
|
||||||
|
|
||||||
// Resolve World drill view id for shortcuts; do not auto-open canvas.
|
// Resolve World drill view id for shortcuts; do not auto-open canvas.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1587,11 +1592,34 @@ export function TopologyPage() {
|
||||||
pendingEdgeCreatesRef.current = new Set();
|
pendingEdgeCreatesRef.current = new Set();
|
||||||
clearDirty();
|
clearDirty();
|
||||||
bumpHistory();
|
bumpHistory();
|
||||||
// Do not auto-fit on canvas switch — user clicks 适应画布.
|
// World / large canvases use absolute coords far outside the default RF
|
||||||
|
// viewport — without an initial fit the canvas looks empty after load.
|
||||||
|
pendingFitRef.current = nextNodes.length > 0;
|
||||||
}
|
}
|
||||||
historyLockRef.current = false;
|
historyLockRef.current = false;
|
||||||
}, [canvasMode, mapId, graphQuery.data, edgeDefaults, setNodes, setEdges, clearDirty, bumpHistory]);
|
}, [canvasMode, mapId, graphQuery.data, edgeDefaults, setNodes, setEdges, clearDirty, bumpHistory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingFitRef.current || !nodes.length) return;
|
||||||
|
let cancelled = false;
|
||||||
|
let tries = 0;
|
||||||
|
const run = () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const inst = rfRef.current;
|
||||||
|
if (!inst) {
|
||||||
|
if (tries++ < 20) window.setTimeout(run, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingFitRef.current = false;
|
||||||
|
inst.fitView({ ...FIT_VIEW_OPTS, duration: 200 });
|
||||||
|
};
|
||||||
|
const t = window.setTimeout(run, 40);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearTimeout(t);
|
||||||
|
};
|
||||||
|
}, [nodes, mapId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEdges((eds) => eds.map((e) => withEdgeVisual(e, edgeDefaults)));
|
setEdges((eds) => eds.map((e) => withEdgeVisual(e, edgeDefaults)));
|
||||||
}, [edgeDefaults, setEdges]);
|
}, [edgeDefaults, setEdges]);
|
||||||
|
|
@ -3495,13 +3523,15 @@ export function TopologyPage() {
|
||||||
const truncateBannerText = useMemo(() => {
|
const truncateBannerText = useMemo(() => {
|
||||||
if (!graphTruncated) return "";
|
if (!graphTruncated) return "";
|
||||||
// World map soft-caps viewport nodes; the banner is noise there.
|
// World map soft-caps viewport nodes; the banner is noise there.
|
||||||
if (isWorldFlatViewName(activeView?.name)) return "";
|
if (isWorldFlatViewName(activeView?.name) || Boolean(graphQuery.data?.view?.filter?.world_flat)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
if (truncateReason === "membership_cap") return t("topology.truncatedMembership");
|
if (truncateReason === "membership_cap") return t("topology.truncatedMembership");
|
||||||
if (truncateReason === "membership_frozen") return t("topology.truncatedFrozen");
|
if (truncateReason === "membership_frozen") return t("topology.truncatedFrozen");
|
||||||
if (truncateReason === "too_many_view_nodes") return t("topology.truncatedNodes");
|
if (truncateReason === "too_many_view_nodes") return t("topology.truncatedNodes");
|
||||||
if (truncateReason === "too_many_edges") return t("topology.truncatedEdges");
|
if (truncateReason === "too_many_edges") return t("topology.truncatedEdges");
|
||||||
return t("topology.truncatedGeneric");
|
return t("topology.truncatedGeneric");
|
||||||
}, [graphTruncated, truncateReason, activeView?.name, t]);
|
}, [graphTruncated, truncateReason, activeView?.name, graphQuery.data?.view?.filter?.world_flat, t]);
|
||||||
const activeLeafName = useMemo(() => {
|
const activeLeafName = useMemo(() => {
|
||||||
if (!mapId) return "";
|
if (!mapId) return "";
|
||||||
if (activeView?.name) return activeView.name;
|
if (activeView?.name) return activeView.name;
|
||||||
|
|
@ -4822,7 +4852,7 @@ export function TopologyPage() {
|
||||||
connectionLineStyle={{ stroke: "#38bdf8", strokeWidth: 2 }}
|
connectionLineStyle={{ stroke: "#38bdf8", strokeWidth: 2 }}
|
||||||
defaultEdgeOptions={{ type: "straight", labelShowBg: false }}
|
defaultEdgeOptions={{ type: "straight", labelShowBg: false }}
|
||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
minZoom={0.05}
|
minZoom={isWorldFlatCanvas ? 0.005 : 0.05}
|
||||||
maxZoom={4}
|
maxZoom={4}
|
||||||
nodesDraggable={toolBehavior.nodesDraggable}
|
nodesDraggable={toolBehavior.nodesDraggable}
|
||||||
nodesConnectable={toolBehavior.nodesConnectable}
|
nodesConnectable={toolBehavior.nodesConnectable}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue