Files

309 lines
11 KiB
Python
Raw Permalink Normal View History

from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
from typing import Iterable
ASITUS_RECOGNIZE_HOLES_ENV = "STEP_EDITOR_ASITUS_RECOGNIZE_HOLES"
ASITUS_DISABLE_ENV = "STEP_EDITOR_DISABLE_ASITUS"
ASITUS_TIMEOUT_ENV = "STEP_EDITOR_ASITUS_TIMEOUT"
def _project_root(project_root: Path | None = None) -> Path:
return project_root or Path(__file__).resolve().parent.parent
def default_asitus_recognize_holes_path(project_root: Path | None = None) -> Path | None:
env_path = os.environ.get(ASITUS_RECOGNIZE_HOLES_ENV, "").strip()
if env_path:
path = Path(env_path).expanduser()
return path if path.is_file() else None
if os.environ.get(ASITUS_DISABLE_ENV, "").strip().lower() in {"1", "true", "yes", "on"}:
return None
root = _project_root(project_root)
candidates = (
root / "third_party" / "asitus_probe_tools_build" / "Release" / "recognize_holes.exe",
root / "third_party" / "asitus_probe_tools_build" / "RelWithDebInfo" / "recognize_holes.exe",
root / "third_party" / "asitus_probe_tools_build" / "Debug" / "recognize_holes.exe",
root / "third_party" / "asitus_probe_build" / "Release" / "recognize_holes.exe",
root / "third_party" / "asitus_probe_build" / "RelWithDebInfo" / "recognize_holes.exe",
root / "third_party" / "asitus_probe_build" / "Debug" / "recognize_holes.exe",
)
for candidate in candidates:
if candidate.is_file():
return candidate
return None
def asitus_runtime_path_entries(project_root: Path | None = None) -> list[Path]:
root = _project_root(project_root)
third_party = root / "third_party" / "3rdparty"
candidates = (
root / "third_party" / "AnalysisSitus_build_algo_occt77" / "win64" / "vc14" / "bin",
third_party / "OCCT" / "win64" / "vc14" / "bin",
third_party / "freeimage-3.17.0-vc14-64" / "bin",
third_party / "freetype-2.5.5-vc14-64" / "bin",
third_party / "tbb_2021.5-vc14-64" / "bin",
third_party / "tcltk-86-64" / "bin",
third_party / "ffmpeg-3.3.4-64" / "bin",
third_party / "openvr-1.14.15-64" / "bin" / "win64",
third_party / "3rdparty-vc14-64" / "freeimage-3.18.0-x64" / "bin",
third_party / "3rdparty-vc14-64" / "freetype-2.13.3-x64" / "bin",
third_party / "3rdparty-vc14-64" / "tbb-2021.13.0-x64" / "bin",
third_party / "3rdparty-vc14-64" / "tcltk-8.6.15-x64" / "bin",
third_party / "3rdparty-vc14-64" / "ffmpeg-3.3.4-64" / "bin",
third_party / "3rdparty-vc14-64" / "openvr-1.14.15-64" / "bin" / "win64",
)
return [path for path in candidates if path.is_dir()]
def parse_asitus_hole_groups(payload: object) -> list[tuple[int, ...]]:
payload = _json_payload(payload)
if not isinstance(payload, dict):
return []
groups: list[tuple[int, ...]] = []
holes = payload.get("holes")
if isinstance(holes, list):
for item in holes:
if not isinstance(item, dict):
continue
group = _int_tuple(item.get("faceIds"))
if group:
groups.append(group)
if groups:
return _dedupe_groups(groups)
flat_ids = _int_tuple(payload.get("holeFaceIds"))
return [flat_ids] if flat_ids else []
def parse_asitus_probe_payload(payload: object) -> dict[str, object]:
data = _json_payload(payload)
if not isinstance(data, dict):
return {
"groups": (),
"faces": (),
"adjacency": (),
"geometric_relations": (),
"surface_summary": {},
"angle_summary": {},
"geometric_relation_summary": {},
"geometric_relation_mode": "",
}
faces: list[dict[str, object]] = []
raw_faces = data.get("faces")
if isinstance(raw_faces, list):
for item in raw_faces:
if not isinstance(item, dict):
continue
face_id = _int_or_none(item.get("id"))
if face_id is None:
continue
faces.append(
{
"id": face_id,
"surface": str(item.get("surface") or ""),
"neighbor_ids": _int_tuple(item.get("neighbors")),
}
)
adjacency: list[dict[str, object]] = []
raw_adjacency = data.get("adjacency")
if isinstance(raw_adjacency, list):
for item in raw_adjacency:
if not isinstance(item, dict):
continue
face_ids = _int_tuple(item.get("faceIds"))
if len(face_ids) != 2:
continue
adjacency.append(
{
"face_ids": face_ids,
"angle_type": str(item.get("angleType") or item.get("type") or ""),
"angle_rad": _float_or_none(item.get("angleRad")),
"edge_ids": _int_tuple(item.get("edgeIds")),
}
)
geometric_relations: list[dict[str, object]] = []
raw_geometric_relations = data.get("geometricRelations")
if isinstance(raw_geometric_relations, list):
for item in raw_geometric_relations:
if not isinstance(item, dict):
continue
face_ids = _int_tuple(item.get("faceIds"))
if len(face_ids) != 2:
continue
geometric_relations.append(
{
"face_ids": face_ids,
"relation_type": str(item.get("type") or item.get("relationType") or ""),
"residual": _float_or_none(item.get("residual")),
"source": str(item.get("source") or "analysis-situs-probe"),
}
)
return {
"groups": tuple(parse_asitus_hole_groups(data)),
"valid_brep": data.get("validBreP"),
"face_count": _int_or_none(data.get("faceCount")),
"aag_node_count": _int_or_none(data.get("aagNodeCount")),
"faces": tuple(faces),
"adjacency": tuple(adjacency),
"geometric_relations": tuple(geometric_relations),
"surface_summary": _str_int_dict(data.get("surfaceSummary")),
"angle_summary": _str_int_dict(data.get("angleSummary")),
"geometric_relation_summary": _str_int_dict(data.get("geometricRelationSummary")),
"geometric_relation_mode": str(data.get("geometricRelationMode") or ""),
}
def _json_payload(payload: object) -> object:
if not isinstance(payload, str):
return payload
text = payload.strip()
json_start = text.find("{")
if json_start > 0:
text = text[json_start:]
return json.loads(text)
def _int_or_none(value: object) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
def _float_or_none(value: object) -> float | None:
try:
return float(value)
except (TypeError, ValueError):
return None
def _str_int_dict(value: object) -> dict[str, int]:
if not isinstance(value, dict):
return {}
result: dict[str, int] = {}
for key, item in value.items():
try:
result[str(key)] = int(item)
except (TypeError, ValueError):
continue
return result
def _int_tuple(values: object) -> tuple[int, ...]:
if values is None:
return ()
if isinstance(values, (str, bytes)):
return ()
try:
items = list(values) # type: ignore[arg-type]
except TypeError:
return ()
result: list[int] = []
for item in items:
try:
result.append(int(item))
except (TypeError, ValueError):
continue
return tuple(sorted(set(result)))
def _dedupe_groups(groups: Iterable[tuple[int, ...]]) -> list[tuple[int, ...]]:
result: list[tuple[int, ...]] = []
seen: set[tuple[int, ...]] = set()
for group in groups:
if not group or group in seen:
continue
seen.add(group)
result.append(group)
return result
def run_asitus_hole_recognition(
step_path: str | Path,
*,
cli_path: str | Path | None = None,
project_root: Path | None = None,
timeout_seconds: float | None = None,
) -> dict[str, object]:
source = Path(step_path).expanduser()
if not source.is_file():
return {"ok": False, "reason": "missing-step", "groups": (), "message": f"STEP file not found: {source}"}
cli = Path(cli_path).expanduser() if cli_path else default_asitus_recognize_holes_path(project_root)
if cli is None or not cli.is_file():
return {"ok": False, "reason": "missing-cli", "groups": (), "message": "Analysis Situs recognize_holes CLI is not available."}
if timeout_seconds is None:
try:
timeout_seconds = float(os.environ.get(ASITUS_TIMEOUT_ENV, "") or 3.0)
except ValueError:
timeout_seconds = 3.0
env = os.environ.copy()
path_entries = [str(path) for path in asitus_runtime_path_entries(project_root)]
env["PATH"] = os.pathsep.join([*path_entries, env.get("PATH", "")])
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
try:
completed = subprocess.run(
[str(cli), str(source)],
cwd=str(_project_root(project_root)),
env=env,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=max(float(timeout_seconds), 0.1),
creationflags=creationflags,
check=False,
)
except subprocess.TimeoutExpired:
return {"ok": False, "reason": "timeout", "groups": (), "message": "Analysis Situs hole recognition timed out."}
except OSError as exc:
return {"ok": False, "reason": "launch-failed", "groups": (), "message": str(exc)}
if completed.returncode != 0:
message = (completed.stderr or completed.stdout or "").strip()
return {
"ok": False,
"reason": "recognizer-failed",
"returncode": completed.returncode,
"groups": (),
"message": message,
}
try:
parsed = parse_asitus_probe_payload(completed.stdout)
except (json.JSONDecodeError, TypeError, ValueError) as exc:
return {"ok": False, "reason": "bad-json", "groups": (), "message": str(exc), "stdout": completed.stdout}
groups = tuple(parsed.get("groups", ()))
return {
"ok": True,
"reason": "ok",
"groups": groups,
"hole_count": len(groups),
"valid_brep": parsed.get("valid_brep"),
"face_count": parsed.get("face_count"),
"aag_node_count": parsed.get("aag_node_count"),
"faces": tuple(parsed.get("faces", ())),
"adjacency": tuple(parsed.get("adjacency", ())),
"geometric_relations": tuple(parsed.get("geometric_relations", ())),
"surface_summary": dict(parsed.get("surface_summary", {}) or {}),
"angle_summary": dict(parsed.get("angle_summary", {}) or {}),
"geometric_relation_summary": dict(parsed.get("geometric_relation_summary", {}) or {}),
"geometric_relation_mode": str(parsed.get("geometric_relation_mode") or ""),
"cli": str(cli),
"message": f"Analysis Situs recognized {len(groups)} hole groups.",
}