mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 16:43:18 +08:00
Seed a unique L2 World canvas under the UME World container, show the flat map only when nested regions have NEs, pack blocks without overlap, allow drag overrides on the world map while forbidding direct NE adds, and hide the world-map truncation banner. Co-authored-by: Cursor <cursoragent@cursor.com>
564 lines
18 KiB
Python
564 lines
18 KiB
Python
"""Seed UME hierarchical canvases: World (level) + World Flat + per-SBN views."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .models import TopoFabricNode, TopoFolder, TopoView, UmeTopoNode
|
|
from .topology_common import _utcnow
|
|
from .topology_membership import VIEW_KIND_PHYSICAL
|
|
from .topology_views_tree import bootstrap_topology_tree
|
|
|
|
_log = logging.getLogger("netx.ume.topo_world")
|
|
|
|
WORLD_FOLDER_NAME = "UME World"
|
|
WORLD_DRILL_FOLDER_NAME = "World"
|
|
WORLD_FOLDER_REF = "ume:world"
|
|
WORLD_DRILL_REF = "ume:world:drill"
|
|
WORLD_VIEW_NAME = "World"
|
|
WORLD_FLAT_VIEW_NAME = "完整世界地图"
|
|
# 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 _filt(view: TopoView | None) -> dict[str, Any]:
|
|
if view is None:
|
|
return {}
|
|
return dict(view.filter or {})
|
|
|
|
|
|
def is_ume_level_view(view: TopoView | None) -> bool:
|
|
return bool(_filt(view).get("ume_level"))
|
|
|
|
|
|
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
|
|
|
|
|
|
def is_ume_world_container(folder: TopoFolder | None) -> bool:
|
|
if folder is None:
|
|
return False
|
|
ext = str(getattr(folder, "external_ref", None) or "").strip()
|
|
if ext == WORLD_FOLDER_REF:
|
|
return True
|
|
return str(folder.name or "").strip() == WORLD_FOLDER_NAME and bool(folder.is_system)
|
|
|
|
|
|
def is_world_drill_folder(folder: TopoFolder | None) -> bool:
|
|
if folder is None:
|
|
return False
|
|
ext = str(getattr(folder, "external_ref", None) or "").strip()
|
|
return ext == WORLD_DRILL_REF or (
|
|
str(folder.name or "") == WORLD_DRILL_FOLDER_NAME and bool(folder.is_system)
|
|
)
|
|
|
|
|
|
def is_world_view(view: TopoView | None) -> bool:
|
|
"""True for UME World drill-root (legacy `world` key or ume_level parent=md)."""
|
|
if view is None:
|
|
return False
|
|
filt = _filt(view)
|
|
if filt.get("world_flat"):
|
|
return False
|
|
if filt.get("ume_level") and str(filt.get("parent") or "") == "md":
|
|
return True
|
|
# Legacy flat-world filter — treat as flat, not drill root.
|
|
if bool(filt.get("world")) and not filt.get("ume_level"):
|
|
return False
|
|
return str(view.name or "") == WORLD_VIEW_NAME and not is_world_flat_view(view)
|
|
|
|
|
|
def is_ume_canvas_view(view: TopoView | None) -> bool:
|
|
"""Any UME-driven virtual canvas (level or flat) — bypass membership graph."""
|
|
if view is None:
|
|
return False
|
|
filt = _filt(view)
|
|
if filt.get("ume_level") or filt.get("world_flat"):
|
|
return True
|
|
# Legacy world viewport
|
|
return bool(filt.get("world"))
|
|
|
|
|
|
def get_world_container_folder(db: Session) -> TopoFolder | None:
|
|
for f in db.query(TopoFolder).filter(TopoFolder.kind == "region").all():
|
|
if is_ume_world_container(f):
|
|
return f
|
|
return None
|
|
|
|
|
|
def get_world_drill_folder(db: Session) -> TopoFolder | None:
|
|
row = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.external_ref == WORLD_DRILL_REF)
|
|
.first()
|
|
)
|
|
if row is not None:
|
|
return row
|
|
for f in db.query(TopoFolder).filter(TopoFolder.kind == "region").all():
|
|
if is_world_drill_folder(f):
|
|
return f
|
|
return None
|
|
|
|
|
|
def get_world_view(db: Session) -> TopoView | None:
|
|
"""Drill-down World root canvas (not the flat all-NE map)."""
|
|
for v in db.query(TopoView).all():
|
|
if is_world_view(v) or (
|
|
is_ume_level_view(v) and str(_filt(v).get("parent") or "") == "md"
|
|
):
|
|
return v
|
|
return None
|
|
|
|
|
|
def get_world_flat_view(db: Session) -> TopoView | None:
|
|
for v in db.query(TopoView).all():
|
|
if is_world_flat_view(v):
|
|
return v
|
|
return None
|
|
|
|
|
|
def _folder_subtree_ids(db: Session, folder_id: str) -> set[str]:
|
|
all_folders = db.query(TopoFolder).all()
|
|
children: dict[str | None, list[str]] = {}
|
|
for f in all_folders:
|
|
children.setdefault(f.parent_id, []).append(f.id)
|
|
ids = {folder_id}
|
|
stack = [folder_id]
|
|
while stack:
|
|
cur = stack.pop()
|
|
for cid in children.get(cur, []):
|
|
if cid not in ids:
|
|
ids.add(cid)
|
|
stack.append(cid)
|
|
return ids
|
|
|
|
|
|
def _child_region_has_nes(db: Session, child: TopoFolder) -> bool:
|
|
"""True when the child region subtree owns at least one fabric NE."""
|
|
ids = _folder_subtree_ids(db, child.id)
|
|
n = (
|
|
db.query(TopoFabricNode)
|
|
.filter(TopoFabricNode.region_folder_id.in_(list(ids)))
|
|
.limit(1)
|
|
.first()
|
|
)
|
|
if n is not None:
|
|
return True
|
|
ref = str(getattr(child, "external_ref", None) or "").strip()
|
|
if ref and not ref.startswith("ume:"):
|
|
for fn in (
|
|
db.query(TopoFabricNode)
|
|
.filter(TopoFabricNode.ume_ne_id.isnot(None), TopoFabricNode.ume_ne_id != "")
|
|
.all()
|
|
):
|
|
if str((fn.attrs or {}).get("ume_sbn_id") or "") == ref:
|
|
return True
|
|
if str(fn.region_folder_id or "") == child.id:
|
|
return True
|
|
return False
|
|
|
|
|
|
def world_map_should_exist(db: Session) -> bool:
|
|
"""Rule 2A: L2 has nested child regions and at least one of those has NEs.
|
|
|
|
If the unique L2 canvas only has direct NEs (no child regions), world map is
|
|
redundant with the L2 canvas and must not appear.
|
|
"""
|
|
drill = get_world_drill_folder(db)
|
|
if drill is None:
|
|
return False
|
|
children = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.parent_id == drill.id, TopoFolder.kind == "region")
|
|
.all()
|
|
)
|
|
if not children:
|
|
return False
|
|
return any(_child_region_has_nes(db, c) for c in children)
|
|
|
|
|
|
def reconcile_world_flat_view(db: Session) -> TopoView | None:
|
|
"""Create flat view when 2A holds; keep row but mark suppressed when not.
|
|
|
|
Soft-hide via filter.suppressed so drag overrides on TopoViewNode survive.
|
|
"""
|
|
container = get_world_container_folder(db)
|
|
if container is None:
|
|
return None
|
|
should = world_map_should_exist(db)
|
|
existing = get_world_flat_view(db)
|
|
now = _utcnow()
|
|
if should:
|
|
if existing is None:
|
|
existing = _ensure_view(
|
|
db,
|
|
folder_id=container.id,
|
|
name=WORLD_FLAT_VIEW_NAME,
|
|
filt=dict(WORLD_FLAT_FILTER),
|
|
remark="All NEs with composed flat-world coordinates (no regions)",
|
|
sort_order=1,
|
|
match=is_world_flat_view,
|
|
)
|
|
else:
|
|
existing.folder_id = container.id
|
|
filt = dict(existing.filter or {})
|
|
filt.update(WORLD_FLAT_FILTER)
|
|
filt.pop("suppressed", None)
|
|
existing.filter = filt
|
|
existing.name = WORLD_FLAT_VIEW_NAME
|
|
existing.updated_at = now
|
|
return existing
|
|
if existing is not None:
|
|
filt = dict(existing.filter or {})
|
|
filt.update(WORLD_FLAT_FILTER)
|
|
filt["suppressed"] = True
|
|
existing.filter = filt
|
|
existing.updated_at = now
|
|
return existing
|
|
|
|
|
|
def is_world_flat_visible(view: TopoView | None) -> bool:
|
|
"""Tree/hex should show flat only when not suppressed and 2A would hold."""
|
|
if view is None or not is_world_flat_view(view):
|
|
return False
|
|
if bool(_filt(view).get("suppressed")):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _ensure_view(
|
|
db: Session,
|
|
*,
|
|
folder_id: str,
|
|
name: str,
|
|
filt: dict[str, Any],
|
|
remark: str,
|
|
sort_order: int,
|
|
match,
|
|
) -> TopoView:
|
|
"""Find view under folder matching predicate, or create."""
|
|
now = _utcnow()
|
|
found: TopoView | None = None
|
|
for v in db.query(TopoView).filter(TopoView.folder_id == folder_id).all():
|
|
if match(v):
|
|
found = v
|
|
break
|
|
if found is None:
|
|
found = TopoView(
|
|
id=uuid4().hex,
|
|
folder_id=folder_id,
|
|
kind=VIEW_KIND_PHYSICAL,
|
|
role="core",
|
|
name=name,
|
|
remark=remark,
|
|
sort_order=sort_order,
|
|
filter=dict(filt),
|
|
viewport={},
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(found)
|
|
db.flush()
|
|
return found
|
|
found.kind = VIEW_KIND_PHYSICAL
|
|
found.name = name
|
|
found.remark = remark
|
|
found.sort_order = sort_order
|
|
cur = dict(found.filter or {})
|
|
cur.update(filt)
|
|
found.filter = cur
|
|
found.updated_at = now
|
|
return found
|
|
|
|
|
|
def ensure_ume_world_and_sbn_folders(db: Session) -> dict[str, Any]:
|
|
"""Create/update UME World container + unique L2 World drill + conditional Flat.
|
|
|
|
Tree shape::
|
|
|
|
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)
|
|
"""
|
|
now = _utcnow()
|
|
stats: dict[str, Any] = {
|
|
"world_folder_id": "",
|
|
"world_drill_folder_id": "",
|
|
"world_view_id": "",
|
|
"world_flat_view_id": "",
|
|
"world_map_visible": False,
|
|
"sbn_folders": 0,
|
|
"sbn_updated": 0,
|
|
"sbn_views": 0,
|
|
}
|
|
bootstrap_topology_tree(db)
|
|
root = db.query(TopoFolder).filter(TopoFolder.kind == "root").first()
|
|
if root is None:
|
|
raise RuntimeError("topology_root_missing")
|
|
|
|
world_folder = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.kind == "region", TopoFolder.name == WORLD_FOLDER_NAME)
|
|
.first()
|
|
)
|
|
if world_folder is None:
|
|
world_folder = TopoFolder(
|
|
id=uuid4().hex,
|
|
parent_id=root.id,
|
|
kind="region",
|
|
name=WORLD_FOLDER_NAME,
|
|
sort_order=0,
|
|
is_system=True,
|
|
external_ref=WORLD_FOLDER_REF,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(world_folder)
|
|
db.flush()
|
|
else:
|
|
world_folder.is_system = True
|
|
world_folder.external_ref = world_folder.external_ref or WORLD_FOLDER_REF
|
|
world_folder.updated_at = now
|
|
|
|
# Drill folder "World" under UME World — owns the level canvas + SBN children.
|
|
drill_folder = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.external_ref == WORLD_DRILL_REF)
|
|
.first()
|
|
)
|
|
if drill_folder is None:
|
|
drill_folder = (
|
|
db.query(TopoFolder)
|
|
.filter(
|
|
TopoFolder.parent_id == world_folder.id,
|
|
TopoFolder.name == WORLD_DRILL_FOLDER_NAME,
|
|
TopoFolder.is_system.is_(True),
|
|
)
|
|
.first()
|
|
)
|
|
if drill_folder is None:
|
|
drill_folder = TopoFolder(
|
|
id=uuid4().hex,
|
|
parent_id=world_folder.id,
|
|
kind="region",
|
|
name=WORLD_DRILL_FOLDER_NAME,
|
|
sort_order=0,
|
|
is_system=True,
|
|
external_ref=WORLD_DRILL_REF,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(drill_folder)
|
|
db.flush()
|
|
else:
|
|
drill_folder.parent_id = world_folder.id
|
|
drill_folder.name = WORLD_DRILL_FOLDER_NAME
|
|
drill_folder.is_system = True
|
|
drill_folder.external_ref = WORLD_DRILL_REF
|
|
drill_folder.sort_order = 0
|
|
drill_folder.updated_at = now
|
|
|
|
world_view = _ensure_view(
|
|
db,
|
|
folder_id=drill_folder.id,
|
|
name=WORLD_VIEW_NAME,
|
|
filt=dict(WORLD_LEVEL_FILTER),
|
|
remark="UME drill-down root (child regions + direct MEs)",
|
|
sort_order=0,
|
|
match=lambda v: is_world_view(v)
|
|
or (is_ume_level_view(v) and str(_filt(v).get("parent") or "") == "md")
|
|
or (bool(_filt(v).get("world")) and not _filt(v).get("world_flat")),
|
|
)
|
|
# Migrate legacy World view that still hangs on UME World container.
|
|
for v in db.query(TopoView).filter(TopoView.folder_id == world_folder.id).all():
|
|
if is_world_flat_view(v):
|
|
continue
|
|
if is_world_view(v) or (
|
|
is_ume_level_view(v) and str(_filt(v).get("parent") or "") == "md"
|
|
):
|
|
if v.id != world_view.id:
|
|
v.folder_id = drill_folder.id
|
|
vf = dict(v.filter or {})
|
|
vf.pop("world", None)
|
|
vf.pop("mode", None)
|
|
vf.update(WORLD_LEVEL_FILTER)
|
|
v.filter = vf
|
|
v.updated_at = now
|
|
wf = dict(world_view.filter or {})
|
|
wf.pop("world", None)
|
|
wf.pop("mode", None)
|
|
wf.update(WORLD_LEVEL_FILTER)
|
|
world_view.filter = wf
|
|
world_view.folder_id = drill_folder.id
|
|
|
|
stats["world_folder_id"] = world_folder.id
|
|
stats["world_drill_folder_id"] = drill_folder.id
|
|
stats["world_view_id"] = world_view.id
|
|
|
|
sbns = (
|
|
db.query(UmeTopoNode)
|
|
.filter(UmeTopoNode.node_type == "TOPO_NODE_SBN")
|
|
.all()
|
|
)
|
|
sbn_ids = {str(s.node_id) for s in sbns if str(s.node_id or "").strip()}
|
|
by_ref: dict[str, TopoFolder] = {
|
|
str(f.external_ref): f
|
|
for f in db.query(TopoFolder)
|
|
.filter(TopoFolder.external_ref.isnot(None), TopoFolder.external_ref != "")
|
|
.all()
|
|
if str(f.external_ref or "").strip() and not str(f.external_ref).startswith("ume:")
|
|
}
|
|
|
|
for sbn in sbns:
|
|
sid = str(sbn.node_id or "").strip()
|
|
if not sid:
|
|
continue
|
|
label = (sbn.user_label or sid)[:256]
|
|
folder = by_ref.get(sid)
|
|
if folder is None:
|
|
folder = TopoFolder(
|
|
id=uuid4().hex,
|
|
parent_id=drill_folder.id,
|
|
kind="region",
|
|
name=label,
|
|
sort_order=100,
|
|
is_system=True,
|
|
external_ref=sid,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(folder)
|
|
by_ref[sid] = folder
|
|
stats["sbn_folders"] += 1
|
|
else:
|
|
folder.name = label
|
|
folder.is_system = True
|
|
folder.updated_at = now
|
|
stats["sbn_updated"] += 1
|
|
|
|
db.flush()
|
|
|
|
for sbn in sbns:
|
|
sid = str(sbn.node_id or "").strip()
|
|
folder = by_ref.get(sid)
|
|
if folder is None:
|
|
continue
|
|
parent = str(sbn.parent_node or "").strip()
|
|
if parent in sbn_ids and parent in by_ref:
|
|
folder.parent_id = by_ref[parent].id
|
|
else:
|
|
# Root SBNs hang under World (drill), not UME World container.
|
|
folder.parent_id = drill_folder.id
|
|
|
|
label = (sbn.user_label or sid)[:256]
|
|
level_filt = {
|
|
"ume_level": True,
|
|
"sbn_id": sid,
|
|
"ume_x": float(sbn.x_pos) if sbn.x_pos is not None else None,
|
|
"ume_y": float(sbn.y_pos) if sbn.y_pos is not None else None,
|
|
}
|
|
_ensure_view(
|
|
db,
|
|
folder_id=folder.id,
|
|
name=label[:200] or "Topology",
|
|
filt=level_filt,
|
|
remark=f"UME SBN level canvas ({sid})",
|
|
sort_order=0,
|
|
match=lambda v, _sid=sid: is_ume_level_view(v)
|
|
and str(_filt(v).get("sbn_id") or "") == _sid,
|
|
)
|
|
stats["sbn_views"] += 1
|
|
|
|
# Tag fabric ME nodes with direct SBN for region filter / flat packing.
|
|
me_parent = {
|
|
str(n.ume_ne_id or n.node_id): str(n.parent_node or "")
|
|
for n in db.query(UmeTopoNode).filter(UmeTopoNode.node_type == "TOPO_NODE_ME").all()
|
|
if str(n.ume_ne_id or n.node_id or "").strip()
|
|
}
|
|
for fn in db.query(TopoFabricNode).filter(TopoFabricNode.ume_ne_id.isnot(None)).all():
|
|
uid = str(fn.ume_ne_id or "").strip()
|
|
parent = me_parent.get(uid, "")
|
|
if not parent:
|
|
continue
|
|
attrs = dict(fn.attrs or {})
|
|
attrs["ume_sbn_id"] = parent[:128]
|
|
folder = by_ref.get(parent)
|
|
if folder is not None:
|
|
fn.region_folder_id = folder.id
|
|
fn.region_source = "ume"
|
|
fn.attrs = attrs
|
|
|
|
db.flush()
|
|
flat_view = reconcile_world_flat_view(db)
|
|
stats["world_map_visible"] = world_map_should_exist(db)
|
|
if flat_view is not None and stats["world_map_visible"]:
|
|
stats["world_flat_view_id"] = flat_view.id
|
|
elif flat_view is not None:
|
|
stats["world_flat_view_id"] = flat_view.id
|
|
|
|
db.commit()
|
|
try:
|
|
from .ume_topology_flat_coords import recompute_flat_world_coords
|
|
|
|
stats["flat_coords"] = recompute_flat_world_coords(db)
|
|
except Exception:
|
|
_log.exception("recompute_flat_world_coords after world ensure failed")
|
|
_log.info("ume world/sbn folders ready: %s", stats)
|
|
return stats
|
|
|
|
|
|
def folder_bbox(db: Session, folder_id: str) -> dict[str, float | int] | None:
|
|
"""Bounding box of fabric nodes tagged with this SBN folder (or descendants)."""
|
|
folder = db.get(TopoFolder, folder_id)
|
|
if folder is None:
|
|
return None
|
|
all_folders = db.query(TopoFolder).all()
|
|
children: dict[str | None, list[str]] = {}
|
|
for f in all_folders:
|
|
children.setdefault(f.parent_id, []).append(f.id)
|
|
ids = {folder_id}
|
|
stack = [folder_id]
|
|
while stack:
|
|
cur = stack.pop()
|
|
for cid in children.get(cur, []):
|
|
if cid not in ids:
|
|
ids.add(cid)
|
|
stack.append(cid)
|
|
|
|
q = (
|
|
db.query(TopoFabricNode)
|
|
.filter(
|
|
TopoFabricNode.region_folder_id.in_(list(ids)),
|
|
TopoFabricNode.world_x.isnot(None),
|
|
TopoFabricNode.world_y.isnot(None),
|
|
)
|
|
.all()
|
|
)
|
|
if not q:
|
|
ref = str(folder.external_ref or "").strip()
|
|
if ref:
|
|
q = [
|
|
n
|
|
for n in db.query(TopoFabricNode)
|
|
.filter(TopoFabricNode.world_x.isnot(None), TopoFabricNode.world_y.isnot(None))
|
|
.all()
|
|
if str((n.attrs or {}).get("ume_sbn_id") or "") == ref
|
|
]
|
|
if not q:
|
|
return None
|
|
xs = [float(n.world_x) for n in q]
|
|
ys = [float(n.world_y) for n in q]
|
|
return {
|
|
"min_x": min(xs),
|
|
"max_x": max(xs),
|
|
"min_y": min(ys),
|
|
"max_y": max(ys),
|
|
"n": len(q),
|
|
}
|