mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Render world map overview as a screen-space starfield and rename it to 世界地图.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8444d85005
commit
fdab188cc1
11 changed files with 470 additions and 67 deletions
|
|
@ -34,7 +34,9 @@ def _is_world_flat(view: TopoView) -> bool:
|
|||
filt = dict(view.filter or {})
|
||||
if filt.get("world_flat"):
|
||||
return True
|
||||
return str(view.name or "").strip() == "完整世界地图"
|
||||
from .ume_topology_world import is_world_flat_view_name
|
||||
|
||||
return is_world_flat_view_name(str(view.name or "").strip())
|
||||
|
||||
|
||||
def primary_canvas_view(db: Session, folder_id: str) -> TopoView | None:
|
||||
|
|
|
|||
|
|
@ -302,6 +302,13 @@ class ViewEdgeOut(BaseModel):
|
|||
discovered_at: datetime | None = None
|
||||
|
||||
|
||||
class WorldScatterPointOut(BaseModel):
|
||||
"""Lightweight display-space point for world-map starfield (no labels/edges)."""
|
||||
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
|
||||
|
||||
class WorldTransformOut(BaseModel):
|
||||
"""Flat world map: display = (world - origin) * scale. scale is 1.0 (1:1)."""
|
||||
|
||||
|
|
@ -314,6 +321,8 @@ class WorldTransformOut(BaseModel):
|
|||
full_max_y: float = 0.0
|
||||
total: int = 0
|
||||
lod: str = "overview"
|
||||
# Dock ME count when fabric world coords are empty — UI heal hint.
|
||||
dock_me_count: int = 0
|
||||
|
||||
|
||||
class TopologyViewGraphOut(BaseModel):
|
||||
|
|
@ -324,6 +333,8 @@ class TopologyViewGraphOut(BaseModel):
|
|||
truncate_reason: str = ""
|
||||
outside_peers: list[dict[str, str]] = Field(default_factory=list)
|
||||
world_transform: WorldTransformOut | None = None
|
||||
# Overview starfield: all (or capped) display coords; RF nodes stay empty far out.
|
||||
scatter: list[WorldScatterPointOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ViewPopulateOut(BaseModel):
|
||||
|
|
|
|||
|
|
@ -20,12 +20,19 @@ WORLD_DRILL_FOLDER_NAME = "World"
|
|||
WORLD_FOLDER_REF = "ume:world"
|
||||
WORLD_DRILL_REF = "ume:world:drill"
|
||||
WORLD_VIEW_NAME = "World"
|
||||
WORLD_FLAT_VIEW_NAME = "完整世界地图"
|
||||
WORLD_FLAT_VIEW_NAME = "世界地图"
|
||||
# Pre-rename label kept so existing TopoView rows still match.
|
||||
WORLD_FLAT_VIEW_NAME_LEGACY = "完整世界地图"
|
||||
# Drill-down root: children of MD (root SBNs + rare direct MEs).
|
||||
WORLD_LEVEL_FILTER = {"ume_level": True, "parent": "md"}
|
||||
WORLD_FLAT_FILTER = {"world_flat": True}
|
||||
|
||||
|
||||
def is_world_flat_view_name(name: str | None) -> bool:
|
||||
n = str(name or "").strip()
|
||||
return n == WORLD_FLAT_VIEW_NAME or n == WORLD_FLAT_VIEW_NAME_LEGACY
|
||||
|
||||
|
||||
def _filt(view: TopoView | None) -> dict[str, Any]:
|
||||
if view is None:
|
||||
return {}
|
||||
|
|
@ -40,7 +47,7 @@ def is_ume_level_view(view: TopoView | None) -> bool:
|
|||
|
||||
def is_world_flat_view(view: TopoView | None) -> bool:
|
||||
filt = _filt(view)
|
||||
return bool(filt.get("world_flat")) or str(getattr(view, "name", "") or "") == WORLD_FLAT_VIEW_NAME
|
||||
return bool(filt.get("world_flat")) or is_world_flat_view_name(str(getattr(view, "name", "") or ""))
|
||||
|
||||
|
||||
def is_ume_world_container(folder: TopoFolder | None) -> bool:
|
||||
|
|
@ -326,7 +333,7 @@ def ensure_ume_world_and_sbn_folders(db: Session) -> dict[str, Any]:
|
|||
UME World (nav only — hex browse)
|
||||
├─ World (unique L2 folder ume:world:drill — SBNs hang here)
|
||||
│ └─ <root SBNs…>
|
||||
└─ 完整世界地图 (flat view — only when rule 2A holds)
|
||||
└─ 世界地图 (flat view — only when rule 2A holds)
|
||||
"""
|
||||
now = _utcnow()
|
||||
stats: dict[str, Any] = {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from .topology_schemas import (
|
|||
TopologyViewGraphOut,
|
||||
ViewEdgeOut,
|
||||
ViewNodeOut,
|
||||
WorldScatterPointOut,
|
||||
WorldTransformOut,
|
||||
)
|
||||
from .topology_views_tree import _get_view_or_404, _view_out
|
||||
|
|
@ -34,10 +35,12 @@ from .ume_topology_world import (
|
|||
|
||||
# Browser-safe caps. Flat world used to ship 2000 RF nodes (~1MB JSON) and freeze
|
||||
# the UI (fitView never settles; workbench feels stuck). Keep membership graphs at
|
||||
# the shared hard cap; flat overview/detail use LOD-specific caps.
|
||||
# the shared hard cap; flat overview uses a lightweight scatter starfield (all
|
||||
# coords) while detail uses viewport RF nodes.
|
||||
WORLD_NODE_SOFT_CAP = VIEW_GRAPH_NODE_HARD_CAP
|
||||
WORLD_FLAT_NODE_SOFT_CAP = 400 # legacy alias → detail
|
||||
WORLD_FLAT_OVERVIEW_CAP = 1500
|
||||
WORLD_FLAT_OVERVIEW_CAP = 1500 # legacy RF sample; overview now prefers scatter
|
||||
WORLD_FLAT_SCATTER_CAP = 40000
|
||||
# Close-up must be able to show a whole large SBN like the region canvas (≤ hard cap).
|
||||
WORLD_FLAT_DETAIL_CAP = VIEW_GRAPH_NODE_HARD_CAP
|
||||
# Viewport span (display/world units) at/under which we prefer a full region layout
|
||||
|
|
@ -481,7 +484,7 @@ def get_flat_view_graph(
|
|||
lod: str = "auto",
|
||||
status: str = "active",
|
||||
) -> TopologyViewGraphOut:
|
||||
"""Flat world map with LOD: overview (global dots) or detail (viewport sample).
|
||||
"""Flat world map with LOD: overview (scatter starfield) or detail (viewport RF).
|
||||
|
||||
Client bbox is in **display** coordinates (same as returned node x/y). Folder
|
||||
auto-bbox (when only folder_id is set) stays in packed world coordinates.
|
||||
|
|
@ -524,8 +527,27 @@ def get_flat_view_graph(
|
|||
.one()
|
||||
)
|
||||
total_all = int(extent_row[4] or 0)
|
||||
dock_me_count = 0
|
||||
if not total_all:
|
||||
return TopologyViewGraphOut(view=_view_out(view), nodes=[], edges=[])
|
||||
try:
|
||||
dock_me_count = int(
|
||||
db.query(func.count(UmeTopoNode.node_id))
|
||||
.filter(UmeTopoNode.node_type == "TOPO_NODE_ME")
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
except Exception:
|
||||
dock_me_count = 0
|
||||
return TopologyViewGraphOut(
|
||||
view=_view_out(view),
|
||||
nodes=[],
|
||||
edges=[],
|
||||
world_transform=WorldTransformOut(
|
||||
total=0,
|
||||
lod=str(lod or "overview").strip().lower() or "overview",
|
||||
dock_me_count=dock_me_count,
|
||||
),
|
||||
)
|
||||
|
||||
full_min_x = float(extent_row[0])
|
||||
full_max_x = float(extent_row[1])
|
||||
|
|
@ -549,6 +571,54 @@ def get_flat_view_graph(
|
|||
if lod_norm == "auto":
|
||||
lod_norm = "detail" if client_bbox else "overview"
|
||||
|
||||
def _world_transform(*, total: int | None = None) -> WorldTransformOut:
|
||||
return WorldTransformOut(
|
||||
origin_x=full_min_x,
|
||||
origin_y=full_min_y,
|
||||
scale=scale,
|
||||
full_min_x=full_min_x,
|
||||
full_max_x=full_max_x,
|
||||
full_min_y=full_min_y,
|
||||
full_max_y=full_max_y,
|
||||
total=int(total if total is not None else total_all),
|
||||
lod=lod_norm,
|
||||
dock_me_count=0,
|
||||
)
|
||||
|
||||
# Overview: lightweight starfield only (no RF nodes / edges). Screen-space
|
||||
# canvas draws these so fitView zoom no longer shrinks dots to sub-pixels.
|
||||
if lod_norm == "overview" and not sbn_id.strip():
|
||||
scatter_q = db.query(TopoFabricNode.world_x, TopoFabricNode.world_y).filter(
|
||||
TopoFabricNode.world_x.isnot(None),
|
||||
TopoFabricNode.world_y.isnot(None),
|
||||
)
|
||||
if region_folder_ids is not None:
|
||||
scatter_q = scatter_q.filter(TopoFabricNode.region_folder_id.in_(list(region_folder_ids)))
|
||||
rows = (
|
||||
scatter_q.order_by(TopoFabricNode.world_x.asc(), TopoFabricNode.world_y.asc())
|
||||
.limit(WORLD_FLAT_SCATTER_CAP + 1)
|
||||
.all()
|
||||
)
|
||||
truncated = len(rows) > WORLD_FLAT_SCATTER_CAP
|
||||
if truncated:
|
||||
rows = rows[:WORLD_FLAT_SCATTER_CAP]
|
||||
scatter = [
|
||||
WorldScatterPointOut(
|
||||
x=(float(wx or 0) - full_min_x) * scale,
|
||||
y=(float(wy or 0) - full_min_y) * scale,
|
||||
)
|
||||
for wx, wy in rows
|
||||
]
|
||||
return TopologyViewGraphOut(
|
||||
view=_view_out(view),
|
||||
nodes=[],
|
||||
edges=[],
|
||||
truncated=truncated,
|
||||
truncate_reason="too_many_scatter_points" if truncated else "",
|
||||
world_transform=_world_transform(total=total_all),
|
||||
scatter=scatter,
|
||||
)
|
||||
|
||||
if client_bbox:
|
||||
# Display == world - origin; pad detail viewport so pan feels continuous.
|
||||
pad = 0.2 if lod_norm == "detail" else 0.0
|
||||
|
|
@ -760,15 +830,6 @@ def get_flat_view_graph(
|
|||
edges=edges_out,
|
||||
truncated=truncated,
|
||||
truncate_reason=reason,
|
||||
world_transform=WorldTransformOut(
|
||||
origin_x=full_min_x,
|
||||
origin_y=full_min_y,
|
||||
scale=scale,
|
||||
full_min_x=full_min_x,
|
||||
full_max_x=full_max_x,
|
||||
full_min_y=full_min_y,
|
||||
full_max_y=full_max_y,
|
||||
total=total_all,
|
||||
lod=lod_norm,
|
||||
),
|
||||
world_transform=_world_transform(),
|
||||
scatter=[],
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue