Добавить имитаторы МЛЭ и ГБО с 3D-сценой, съёмкой рельефа и пресетами.

Вкладки позволяют готовить рельеф, двигать АНПА, накапливать поверхность по лучам и сохранять скриншоты окон; ГБО использует бортовые секторы 12–75° и чёрные зоны вне обзора.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 15:43:08 +03:00
co-authored by Cursor
parent 8b248e1d54
commit 6cfc16e53c
18 changed files with 5512 additions and 12 deletions
+78 -4
View File
@@ -649,13 +649,87 @@ def export_ply(points: list[list[float]]) -> str:
return header + body + ("\n" if points else "")
def export_obj(points: list[list[float]], object_name: str = "cloud") -> str:
def export_obj(
points: list[list[float]],
object_name: str = "cloud",
classes: list[int | float] | None = None,
) -> str:
safe_name = "".join(ch if ch.isalnum() or ch in "_-" else "_" for ch in (object_name or "cloud")) or "cloud"
lines = [f"# DotsToSurface point cloud ({len(points)} vertices)", f"o {safe_name}"]
for p in points:
lines.append(f"v {p[0]:.8f} {p[1]:.8f} {p[2]:.8f}")
if classes is None:
lines = [f"# DotsToSurface point cloud ({len(points)} vertices)", f"o {safe_name}"]
for p in points:
lines.append(f"v {p[0]:.8f} {p[1]:.8f} {p[2]:.8f}")
return "\n".join(lines) + "\n"
if len(classes) != len(points):
raise ValueError("classes length must match points length")
class_names = {0: "background", 1: "object"}
grouped: dict[int, list[list[float]]] = {}
for point, cls in zip(points, classes):
grouped.setdefault(int(cls), []).append(point)
lines = [
f"# DotsToSurface labeled point cloud ({len(points)} vertices)",
"# Classes: background=0, object=1",
f"o {safe_name}",
]
for cls_id in sorted(grouped.keys()):
group_name = class_names.get(cls_id, f"class_{cls_id}")
lines.append(f"o {group_name}")
lines.append(f"# class {cls_id}")
for p in grouped[cls_id]:
lines.append(f"v {p[0]:.8f} {p[1]:.8f} {p[2]:.8f}")
return "\n".join(lines) + "\n"
def _class_from_obj_group(name: str) -> float | None:
key = (name or "").strip().lower()
if key == "background":
return 0.0
if key == "object":
return 1.0
if key.startswith("class_"):
try:
return float(key.split("_", 1)[1])
except (IndexError, ValueError):
return None
return None
def parse_obj_labeled_points(text: str) -> list[list[float]]:
"""Extract [x, y, z, class] from OBJ with class groups or ``# class N`` markers."""
labeled: list[list[float]] = []
current_class = 0.0
for raw in text.splitlines():
line = raw.strip()
if not line:
continue
lower = line.lower()
if lower.startswith("# class "):
try:
current_class = float(line.split()[-1])
except ValueError:
pass
continue
if lower.startswith("o "):
cls = _class_from_obj_group(line[2:])
if cls is not None:
current_class = cls
continue
if lower.startswith("v "):
parts = line.split()
if len(parts) < 4:
continue
try:
labeled.append(
[float(parts[1]), float(parts[2]), float(parts[3]), current_class]
)
except ValueError:
continue
return labeled
def parse_obj_points(text: str) -> list[list[float]]:
"""Extract vertex positions from Wavefront OBJ (ignores faces/materials)."""
points: list[list[float]] = []