#! python3
# -*- coding: utf-8 -*-
"""
Mesh booleans with coincident faces: version-neutral test script.

Runs unmodified in:
  Rhino 7  (IronPython 2.7, _EditPythonScript / _RunPythonScript)
  Rhino 8  (ScriptEditor; Python 3 or IronPython 2)
  Rhino 9  (ScriptEditor; Python 3 or IronPython 2)

Every result is judged against the geometrically correct answer, never against another
Rhino version:
  CORRECT = every returned piece is a closed, manifold mesh with non-zero volume,
            and the volumes are exactly the expected ones
  WRONG   = anything else (details printed)
  ERROR   = the call raised an exception
  N/A     = not available in this Rhino version (MeshBooleanOptions)

The script builds all geometry itself. At the start it removes objects left by earlier
runs of this script (only those; nothing else in the document is touched). The native-command
test temporarily adds objects and always removes them. With BAKE = True, the inputs (blue) and
results (orange) of the tests listed in BAKE_TESTS are left in the document, one row per test,
named and labelled, and the Perspective view is zoomed to them.

Input meshes: by default Brep boxes converted with Mesh.CreateFromBrep using
MeshingParameters.Coarse.
Script-built double-precision boxes are used only where a variant needs them (T10, T11, T16)
and for the small-gap and Tolerance tests (sections 3 and 4).

Terminology
  box face  = one of the 6 planar faces of a box
  mesh face = polygon of a mesh (Mesh.Faces; triangle or quad)
  naked edge = edge used by 1 mesh face; non-manifold edge = edge used by 3 or more
"""
from __future__ import print_function

import sys
import math
import clr
import System
try:
    clr.AddReference("System.Drawing")
except Exception:
    pass
import System.Drawing
import Rhino
import Rhino.Geometry as rg
from System.Collections.Generic import List

SCRIPT_VERSION = "2026-09-28 final-6"
BAKE = True
# Tests whose inputs and results are left in the document, one row per test (along +Y).
# Example: BAKE_TESTS = ["T01", "T04", "T09", "T13"]
BAKE_TESTS = ["T01", "T04", "T09", "T12", "T13", "T16"]
RUN_NATIVE_COMMAND = True

EPS_VOL = 1e-6
A_LO, A_HI = (-5.0, -5.0, -5.0), (5.0, 5.0, 5.0)
ROT37 = rg.Transform.Rotation(math.radians(37.0), rg.Vector3d(1.0, 2.0, 3.0), rg.Point3d(0.3, 0.7, -0.2))
ROT90 = rg.Transform.Rotation(1.0, 0.0, rg.Vector3d.ZAxis, rg.Point3d.Origin)  # exact: sin=1, cos=0


# ============================================================================ geometry
def L(meshes):
    lst = List[rg.Mesh]()
    for m in meshes:
        lst.Add(m)
    return lst


def _corners(lo, hi):
    return [(lo[0], lo[1], lo[2]), (hi[0], lo[1], lo[2]), (hi[0], hi[1], lo[2]), (lo[0], hi[1], lo[2]),
            (lo[0], lo[1], hi[2]), (hi[0], lo[1], hi[2]), (hi[0], hi[1], hi[2]), (lo[0], hi[1], hi[2])]


_BOX_FACES = [(0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (2, 3, 7, 6), (0, 4, 7, 3), (1, 2, 6, 5)]


def box_manual(lo, hi, welded=False):
    """Box, double-precision vertices, outward normals.
    Default: unwelded (4 vertices per box face, 24 in total), like a box meshed from a Brep."""
    c = _corners(lo, hi)
    m = rg.Mesh()
    m.Vertices.UseDoublePrecisionVertices = True
    if welded:
        for p in c:
            m.Vertices.Add(float(p[0]), float(p[1]), float(p[2]))
        for f in _BOX_FACES:
            m.Faces.AddFace(f[0], f[1], f[2], f[3])
    else:
        for f in _BOX_FACES:
            i0 = m.Vertices.Count
            for k in f:
                m.Vertices.Add(float(c[k][0]), float(c[k][1]), float(c[k][2]))
            m.Faces.AddFace(i0, i0 + 1, i0 + 2, i0 + 3)
    m.Normals.ComputeNormals()
    return m


def box(lo, hi):
    """Default test box: a Brep box converted with Mesh.CreateFromBrep (MeshingParameters.Coarse)."""
    return box_brep(lo, hi)


def box_brep(lo, hi, force_double=False):
    """Box meshed from a Brep with Mesh.CreateFromBrep (single-precision vertices)."""
    m = rg.Mesh()
    brep = rg.Brep.CreateFromBox(rg.BoundingBox(rg.Point3d(*lo), rg.Point3d(*hi)))
    for p in rg.Mesh.CreateFromBrep(brep, rg.MeshingParameters.Coarse):
        m.Append(p)
    m.Normals.ComputeNormals()
    m.Compact()
    if force_double:
        m.Vertices.UseDoublePrecisionVertices = True
    return m


def box_tri(lo, hi):
    m = box_brep(lo, hi)
    m.Faces.ConvertQuadsToTriangles()
    m.Normals.ComputeNormals()
    return m


def box_div(lo, hi, n):
    m = rg.Mesh.CreateFromBox(rg.BoundingBox(rg.Point3d(*lo), rg.Point3d(*hi)), n, n, n)
    m.Normals.ComputeNormals()
    m.Compact()
    return m


def xf_mesh(m, xf):
    d = m.DuplicateMesh()
    d.Transform(xf)
    return d


# ============================================================================ measurement
def edge_info(m):
    """(naked count, non-manifold count, naked length, non-manifold length)"""
    te = m.TopologyEdges
    nk, nm, nkl, nml = 0, 0, 0.0, 0.0
    for i in range(te.Count):
        n = te.GetConnectedFaces(i).Length
        if n == 1:
            nk += 1
            nkl += te.EdgeLine(i).Length
        elif n > 2:
            nm += 1
            nml += te.EdgeLine(i).Length
    return nk, nm, nkl, nml


def vol(m):
    try:
        return float(m.Volume())
    except Exception:
        return float("nan")


def is_solid(m):
    nk, nm, _, _ = edge_info(m)
    v = vol(m)
    return m.IsClosed and nk == 0 and nm == 0 and v == v and abs(v) > EPS_VOL


def near(a, b, rel=1e-4):
    return abs(a - b) <= rel * max(1.0, abs(b))


def triangles(m):
    V = m.Vertices
    out = []
    for fi in range(m.Faces.Count):
        f = m.Faces[fi]
        a, b, c = V.Point3dAt(f.A), V.Point3dAt(f.B), V.Point3dAt(f.C)
        out.append((a, b, c))
        if f.IsQuad:
            out.append((a, c, V.Point3dAt(f.D)))
    return out


def plane_areas(m):
    """{'+X@5': area, ...} for axis-aligned mesh faces (both normal directions);
    'other' = area of mesh faces that are not axis-aligned."""
    out = {}
    for a, b, c in triangles(m):
        n = rg.Vector3d.CrossProduct(b - a, c - a)
        if n.Length < 1e-12:
            continue
        comps = [n.X, n.Y, n.Z]
        ax = max(range(3), key=lambda k: abs(comps[k]))
        if abs(comps[ax]) / n.Length < 1.0 - 1e-6:
            key = "other"
        else:
            coord = round(([a.X, a.Y, a.Z][ax] + [b.X, b.Y, b.Z][ax] + [c.X, c.Y, c.Z][ax]) / 3.0, 4)
            key = "%s%s@%g" % ("+" if comps[ax] > 0 else "-", "XYZ"[ax], coord)
        out[key] = out.get(key, 0.0) + 0.5 * n.Length
    return out


def has_vertex(m, p):
    V = m.Vertices
    for i in range(V.Count):
        if V.Point3dAt(i).DistanceTo(p) < 1e-6:
            return True
    return False


def sig(ms):
    """Short, comparable description of a result."""
    parts = []
    for m in sorted(ms, key=lambda x: -abs(vol(x))):
        nk, nm, _, _ = edge_info(m)
        parts.append("%s V%d F%d nk%d nm%d vol=%.3f" % ("closed" if m.IsClosed else "open",
                                                       m.Vertices.Count, m.Faces.Count, nk, nm, vol(m)))
    return "n=%d [%s]" % (len(ms), "; ".join(parts))


# ============================================================================ booleans
PLAIN = {"Split": rg.Mesh.CreateBooleanSplit,
         "Difference": rg.Mesh.CreateBooleanDifference,
         "Intersection": rg.Mesh.CreateBooleanIntersection}
OPS = ["Split", "Difference", "Intersection"]


def _find_options_overload(name):
    for mi in clr.GetClrType(rg.Mesh).GetMethods():
        if mi.Name != name:
            continue
        ps = mi.GetParameters()
        if ps.Length == 4 and ps[2].ParameterType.Name == "MeshBooleanOptions":
            return mi
    return None


WITH_OPTS = {}
if hasattr(rg, "MeshBooleanOptions"):
    for _op, _name in (("Split", "CreateBooleanSplit"), ("Difference", "CreateBooleanDifference"),
                       ("Intersection", "CreateBooleanIntersection")):
        _mi = _find_options_overload(_name)
        if _mi is not None:
            WITH_OPTS[_op] = _mi
HAS_OPTS = len(WITH_OPTS) == 3
DEFAULT_TOL = rg.MeshBooleanOptions().Tolerance if HAS_OPTS else None


def run(op, target, cutter, tol=None):
    """Returns (meshes, error). tol=None uses the plain overload."""
    try:
        if tol is None:
            res = PLAIN[op](L([target]), L([cutter]))
        else:
            opts = rg.MeshBooleanOptions()
            opts.Tolerance = tol
            args = System.Array[System.Object]([L([target]), L([cutter]), opts, None])
            res = WITH_OPTS[op].Invoke(None, args)
        return [m for m in (res or []) if m is not None], None
    except Exception as e:
        return [], str(e)


def expected(tlo, thi, clo, chi):
    """Expected volumes for two boxes, and whether an empty Split result is acceptable
    (when nothing is cut off, Split may return nothing or the unchanged target)."""
    ov = 1.0
    for k in range(3):
        ov *= max(0.0, min(thi[k], chi[k]) - max(tlo[k], clo[k]))
    vt = (thi[0] - tlo[0]) * (thi[1] - tlo[1]) * (thi[2] - tlo[2])
    d = [vt - ov] if vt - ov > EPS_VOL else []
    i = [ov] if ov > EPS_VOL else []
    if d and i:
        return {"Split": sorted(d + i), "Difference": d, "Intersection": i}, False
    return {"Split": d or i, "Difference": d, "Intersection": i}, True


def judge(ms, exp, allow_empty):
    if allow_empty and len(ms) == 0:
        return True
    if not exp:
        return len(ms) == 0
    if len(ms) != len(exp):
        return False
    if not all(is_solid(m) for m in ms):
        return False
    return all(near(a, b) for a, b in zip(sorted(abs(vol(m)) for m in ms), sorted(exp)))


# ============================================================================ reporting
TABLE = []   # (test id, description, {op: status})
BAKED = []   # (test id, target, cutter, {op: meshes}) for tests listed in BAKE_TESTS


def header(title):
    print("")
    print("=" * 100)
    print(title)
    print("=" * 100)


def fmt_vols(vs):
    return ("[" + ", ".join("%g" % v for v in vs) + "]") if vs else "[]"


def test_case(tid, desc, target, cutter, boxes, xf=None, tol=None, detail=False):
    """Runs Split, Difference, Intersection on (target, cutter), judges each, prints and records."""
    exp, allow_empty = expected(*boxes)
    if xf is not None:
        target, cutter = xf_mesh(target, xf), xf_mesh(cutter, xf)
    print("")
    print("%s  %s" % (tid, desc))
    print("  expected volumes: Split %s  Difference %s  Intersection %s%s"
          % (fmt_vols(exp["Split"]), fmt_vols(exp["Difference"]), fmt_vols(exp["Intersection"]),
             "  (Split may also return nothing)" if allow_empty else ""))
    status, kept = {}, {}
    for op in OPS:
        ms, err = run(op, target, cutter, tol)
        kept[op] = ms
        if err is not None:
            status[op] = "ERROR"
            print("  %-12s ERROR   %s" % (op, err))
            continue
        ok = judge(ms, exp[op], allow_empty and op == "Split")
        status[op] = "CORRECT" if ok else "WRONG"
        print("  %-12s %-7s %s" % (op, status[op], sig(ms)))
        if detail:
            for k, m in enumerate(sorted(ms, key=lambda x: -abs(vol(x)))):
                pa = plane_areas(m)
                txt = "  ".join("%s=%.2f" % (key, pa[key]) for key in sorted(pa.keys()))
                print("  %-12s   piece %d: %s" % ("", k, txt))
    TABLE.append((tid, desc, status))
    if tid in BAKE_TESTS:
        BAKED.append((tid, target, cutter, kept))


def safe(fn):
    try:
        fn()
    except Exception as e:
        print("  !!! section %s raised: %s" % (fn.__name__, e))


# ============================================================================ sections
MAIN = [A_LO, A_HI, (0.0, 0.0, 0.0), (5.0, 5.0, 5.0)]


def s1_coincident_cases():
    header("1. Coincident faces, same normal orientation (A = (-5..5)^3 is the target, B the cutter)")
    test_case("T01", "B=(0..5)^3, 3 shared faces [main case]", box(A_LO, A_HI), box((0, 0, 0), (5, 5, 5)),
              MAIN, detail=True)
    for tid, hi, n in (("T02", (4.0, 4.0, 5.0), 1), ("T03", (4.0, 5.0, 5.0), 2), ("T04", (5.0, 5.0, 3.0), 2)):
        test_case(tid, "B=(0,0,0)..(%g,%g,%g), %d shared face(s)" % (hi[0], hi[1], hi[2], n), box(A_LO, A_HI),
                  box((0, 0, 0), hi), [A_LO, A_HI, (0.0, 0.0, 0.0), hi])
    test_case("T05", "B=(-2,-2,0)..(2,2,5), patch in the middle of A's +Z box face", box(A_LO, A_HI),
              box((-2, -2, 0), (2, 2, 5)), [A_LO, A_HI, (-2.0, -2.0, 0.0), (2.0, 2.0, 5.0)])
    test_case("T06", "B=(0,0,0)..(5,5,8), shared on x=5 and y=5, B passes through z=5", box(A_LO, A_HI),
              box((0, 0, 0), (5, 5, 8)), [A_LO, A_HI, (0.0, 0.0, 0.0), (5.0, 5.0, 8.0)])
    test_case("T07", "roles swapped: target B=(0..5)^3, cutter A (nothing to cut)", box((0, 0, 0), (5, 5, 5)),
              box(A_LO, A_HI), [(0.0, 0.0, 0.0), (5.0, 5.0, 5.0), A_LO, A_HI])
    test_case("T08", "opposite orientation: B=(5,0,0)..(10,5,5) touches A from outside (nothing to cut)",
              box(A_LO, A_HI), box((5, 0, 0), (10, 5, 5)), [A_LO, A_HI, (5.0, 0.0, 0.0), (10.0, 5.0, 5.0)])
    test_case("T09", "control: B=(-1..1)^3 strictly inside A, no shared faces", box(A_LO, A_HI),
              box((-1, -1, -1), (1, 1, 1)), [A_LO, A_HI, (-1.0, -1.0, -1.0), (1.0, 1.0, 1.0)])


def s2_mesh_variants():
    header("2. Main case (T01) with other mesh types and orientations")
    b0, b1 = (0, 0, 0), (5, 5, 5)
    test_case("T10", "script-built boxes, double precision, welded", box_manual(A_LO, A_HI, True),
              box_manual(b0, b1, True), MAIN)
    test_case("T11", "script-built boxes, double precision, unwelded", box_manual(A_LO, A_HI),
              box_manual(b0, b1), MAIN)
    test_case("T12", "triangulated boxes", box_tri(A_LO, A_HI), box_tri(b0, b1), MAIN)
    test_case("T13", "subdivided, vertices aligned (A 10x10x10, B 5x5x5)", box_div(A_LO, A_HI, 10),
              box_div(b0, b1, 5), MAIN)
    test_case("T14", "subdivided, vertices not aligned (A 3x3x3, B 7x7x7)", box_div(A_LO, A_HI, 3),
              box_div(b0, b1, 7), MAIN)
    test_case("T15", "rotated 90 deg (exact)", box(A_LO, A_HI), box(b0, b1), MAIN, xf=ROT90)
    test_case("T16", "script-built double-precision boxes, rotated 37 deg about (1,2,3)", box_manual(A_LO, A_HI),
              box_manual(b0, b1), MAIN, xf=ROT37)
    test_case("T17", "single precision (CreateFromBrep), rotated 37 deg", box_brep(A_LO, A_HI), box_brep(b0, b1),
              MAIN, xf=ROT37)
    test_case("T18", "CreateFromBrep + UseDoublePrecisionVertices before rotating 37 deg",
              box_brep(A_LO, A_HI, True), box_brep(b0, b1, True), MAIN, xf=ROT37)


def s3_gaps():
    header("3. Nearly coincident faces: B=(0..5+d)^3 protrudes from A by d (default options)")
    print("  Script-built double-precision boxes: single precision cannot store a gap this small.")
    for k, d in enumerate((2e-9, 3e-9, 5e-9, 1e-8, 3e-8, 7e-8, 1e-7, 1e-6, 1e-3)):
        hi = 5.0 + d
        test_case("G%02d" % (k + 1), "d = %g" % d, box_manual(A_LO, A_HI), box_manual((0, 0, 0), (hi, hi, hi)),
                  [A_LO, A_HI, (0.0, 0.0, 0.0), (hi, hi, hi)])


def s4_tolerance():
    header("4. MeshBooleanOptions.Tolerance t: Split with B=(0..5-d)^3 (OK = correct result)")
    if not HAS_OPTS:
        print("  N/A: this Rhino version has no Mesh boolean overloads with MeshBooleanOptions")
        return
    tols = [1e-12, DEFAULT_TOL, 1e-6, 1e-4, 1e-2]
    ds = [0.0, 1e-10, 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 5e-3, 2e-2]
    print("  default Tolerance = %.4g" % DEFAULT_TOL)
    print("  %-8s | %s" % ("d \\ t", " | ".join("%-8.2g" % t for t in tols)))
    for d in ds:
        hi = 5.0 - d
        exp, _ = expected(A_LO, A_HI, (0.0, 0.0, 0.0), (hi, hi, hi))
        cells = []
        for t in tols:
            ms, err = run("Split", box_manual(A_LO, A_HI), box_manual((0, 0, 0), (hi, hi, hi)), t)
            cells.append("ERROR" if err else ("OK" if judge(ms, exp["Split"], False) else "WRONG"))
        print("  %-8.0e | %s" % (d, " | ".join("%-8s" % c for c in cells)))

    print("")
    print("  Gap tests of section 3 with Tolerance = 1e-6:")
    for k, d in enumerate((2e-9, 1e-8, 3e-8, 7e-8, 1e-7)):
        hi = 5.0 + d
        test_case("GT%02d" % (k + 1), "d = %g, Tolerance = 1e-6" % d, box_manual(A_LO, A_HI),
                  box_manual((0, 0, 0), (hi, hi, hi)), [A_LO, A_HI, (0.0, 0.0, 0.0), (hi, hi, hi)], tol=1e-6)
    test_case("GT06", "single precision rotated 37 deg (as T17), Tolerance = 1e-6", box_brep(A_LO, A_HI),
              box_brep((0, 0, 0), (5, 5, 5)), MAIN, xf=ROT37, tol=1e-6)


def s5_provenance():
    header("5. Main case: where do A's and B's mesh faces of the shared x=5 box face end up?")
    print("  A marker vertex at (5, 2.5, 2.5) exists only on the subdivided mesh.")
    print("  Pieces are listed largest first. In a correct split, the marked patch is in the smaller piece")
    print("  (or, for the discarded copy, nowhere).")
    mark = rg.Point3d(5.0, 2.5, 2.5)
    for label, a, b in (("B marked", box(A_LO, A_HI), box_div((0, 0, 0), (5, 5, 5), 2)),
                        ("A marked", box_div(A_LO, A_HI, 4), box((0, 0, 0), (5, 5, 5)))):
        line = []
        for op in OPS:
            ms, err = run(op, a, b)
            if err:
                line.append("%s: ERROR" % op)
                continue
            ms = sorted(ms, key=lambda x: -abs(vol(x)))
            line.append("%s: %s" % (op, " ".join("piece%d=%s" % (k, has_vertex(m, mark)) for k, m in enumerate(ms))))
        print("  %s -> %s" % (label, " | ".join(line)))


def s6_determinism():
    header("6. Determinism: main case repeated 3 times")
    for op in OPS:
        sigs = []
        for _ in range(3):
            ms, err = run(op, box(A_LO, A_HI), box((0, 0, 0), (5, 5, 5)))
            sigs.append(("ERROR " + err) if err else sig(ms))
        print("  %-12s %s" % (op, "identical" if len(set(sigs)) == 1 else "DIFFERENT: " + " || ".join(sigs)))


def s7_native_command():
    header("7. Native command _-MeshBooleanSplit vs RhinoCommon (main case)")
    doc = Rhino.RhinoDoc.ActiveDoc
    if not RUN_NATIVE_COMMAND or doc is None:
        print("  skipped")
        return
    a, b = box(A_LO, A_HI), box((0, 0, 0), (5, 5, 5))
    ida = doc.Objects.AddMesh(a)
    idb = doc.Objects.AddMesh(b)
    created = []
    try:
        before = set(str(o.Id) for o in doc.Objects)
        doc.Objects.UnselectAll()
        doc.Objects.Select(ida)
        Rhino.RhinoApp.RunScript("_-MeshBooleanSplit _SelID %s _Enter" % idb, False)
        created = [o for o in doc.Objects if str(o.Id) not in before]
        pieces = [o.Geometry.DuplicateMesh() for o in created if isinstance(o.Geometry, rg.Mesh)]
        oa = doc.Objects.FindId(ida)
        if oa is not None and not oa.IsDeleted:
            pieces.append(oa.Geometry.DuplicateMesh())
        api, err = run("Split", a, b)
        print("  command:     %s" % sig(pieces))
        print("  RhinoCommon: %s" % (("ERROR " + err) if err else sig(api)))
        print("  same result: %s" % (sig(pieces) == sig(api)))
    finally:
        for o in created:
            doc.Objects.Delete(o.Id, True)
        for i in (ida, idb):
            o = doc.Objects.FindId(i)
            if o is not None and not o.IsDeleted:
                doc.Objects.Delete(i, True)
        doc.Objects.UnselectAll()
        doc.Views.Redraw()


INPUT_COLOR = System.Drawing.Color.FromArgb(40, 110, 220)    # blue
OUTPUT_COLOR = System.Drawing.Color.FromArgb(235, 120, 30)   # orange


TAG_KEY = "MeshBooleanSplitTest"      # user string on every object this script adds


def _attr(name, color):
    attr = Rhino.DocObjects.ObjectAttributes()
    attr.Name = name
    attr.ObjectColor = color
    attr.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject
    attr.SetUserString(TAG_KEY, SCRIPT_VERSION)
    return attr


def _add(doc, mesh, name, color, move):
    c = mesh.DuplicateMesh()
    c.Translate(move)
    return doc.Objects.AddMesh(c, _attr(name, color))


def _dot(doc, text, point, color):
    return doc.Objects.AddTextDot(rg.TextDot(text, point), _attr(text, color))


_re = __import__("re")
# exact name forms used by earlier versions of this script (objects without the tag)
_LEGACY_NAME = _re.compile(
    r"^((T|G|GT)\d\d (input (target|cutter)|(Split|Difference|Intersection)( piece)? \d+( \(.*\))?|API split piece \d+)"
    r"|A_target|B_cutter"
    r"|(Split|Difference|Intersection) piece \d+ \(closed=.*\)"
    r"|(Split outer|Split inner|Difference|Intersection)\s+\(closed=.*\)"
    r"|API split piece \d+)$")
# text of the unnamed text dots added by earlier versions
_LEGACY_DOT = _re.compile(r"^((T|G|GT)\d\d input|(Split|Difference|Intersection) \d+)$")


def cleanup_previous_runs():
    """Deletes ONLY objects added by earlier runs of this script: objects carrying the
    script's user-string tag, plus objects with the names used by earlier script versions.
    Everything else in the document is left untouched."""
    doc = Rhino.RhinoDoc.ActiveDoc
    if doc is None:
        return
    victims = []
    for o in doc.Objects:
        name = o.Attributes.Name or ""
        tagged = o.Attributes.GetUserString(TAG_KEY) is not None
        legacy_dot = isinstance(o.Geometry, rg.TextDot) and not name and _LEGACY_DOT.match(o.Geometry.Text or "")
        if tagged or _LEGACY_NAME.match(name) or legacy_dot:
            victims.append(o.Id)
    for i in victims:
        doc.Objects.Delete(i, True)
    doc.Views.Redraw()
    print("Cleanup: removed %d object(s) from earlier runs of this script." % len(victims))


def show_results(ids):
    """Perspective view: isometric-like camera from the front-left above, 50 mm lens,
    zoomed to the baked objects. Other views: zoom extents."""
    doc = Rhino.RhinoDoc.ActiveDoc
    bbox = rg.BoundingBox.Empty
    for i in ids:
        o = doc.Objects.FindId(i)
        if o is not None:
            bbox.Union(o.Geometry.GetBoundingBox(True))
    if not bbox.IsValid:
        return
    persp = None
    for v in doc.Views:
        if v.ActiveViewport.Name == "Perspective":
            persp = v
    for v in doc.Views:
        if v is not persp:
            v.ActiveViewport.ZoomBoundingBox(bbox)
    if persp is not None:
        vp = persp.ActiveViewport
        if not vp.IsPerspectiveProjection:
            vp.ChangeToPerspectiveProjection(True, 50.0)
        c = bbox.Center
        d = bbox.Diagonal.Length
        vp.SetCameraLocations(c, rg.Point3d(c.X - 0.45 * d, c.Y - 0.9 * d, c.Z + 0.6 * d))
        vp.Camera35mmLensLength = 50.0
        vp.ZoomBoundingBox(bbox)
        doc.Views.ActiveView = persp
    doc.Views.Redraw()


def bake():
    """One row per test (rows 40 apart along -Y). In each row: inputs first (blue),
    then Split 0, Split 1, Difference 0, Intersection 0 ... (orange), 15 apart along +X."""
    doc = Rhino.RhinoDoc.ActiveDoc
    if not BAKE or doc is None or not BAKED:
        return
    ids = []
    for row, (tid, target, cutter, results) in enumerate(BAKED):
        y = -40.0 * row
        center = target.GetBoundingBox(True).Center
        base = rg.Vector3d(-center.X, -center.Y, -center.Z)
        ids.append(_add(doc, target, "%s input target" % tid, INPUT_COLOR, base + rg.Vector3d(0.0, y, 0.0)))
        ids.append(_add(doc, cutter, "%s input cutter" % tid, INPUT_COLOR, base + rg.Vector3d(0.0, y, 0.0)))
        ids.append(_dot(doc, "%s input" % tid, rg.Point3d(0.0, y, 9.0), INPUT_COLOR))
        x = 15.0
        for op in OPS:
            for k, m in enumerate(sorted(results.get(op, []), key=lambda q: -abs(vol(q)))):
                nk, nm, _, _ = edge_info(m)
                name = "%s %s %d (closed=%s naked=%d non-manifold=%d)" % (tid, op, k, m.IsClosed, nk, nm)
                ids.append(_add(doc, m, name, OUTPUT_COLOR, base + rg.Vector3d(x, y, 0.0)))
                ids.append(_dot(doc, "%s %s %d" % (tid, op, k), rg.Point3d(x, y, 9.0), OUTPUT_COLOR))
                x += 15.0
    show_results(ids)
    print("")
    print("Baked %s: one row per test; inputs (blue) first, then outputs (orange) in the same row."
          % ", ".join(t[0] for t in BAKED))


# ============================================================================ main
print("Script %s | Rhino %s | Python %s | options overloads: %s%s"
      % (SCRIPT_VERSION, Rhino.RhinoApp.Version, sys.version.split()[0], HAS_OPTS,
         (" | default Tolerance %.4g" % DEFAULT_TOL) if HAS_OPTS else ""))

safe(cleanup_previous_runs)

for _fn in (s1_coincident_cases, s2_mesh_variants, s3_gaps, s4_tolerance, s5_provenance, s6_determinism,
            s7_native_command):
    safe(_fn)

header("SUMMARY (Split / Difference / Intersection)")
for tid, desc, st in TABLE:
    print("  %-5s %-8s %-8s %-8s %s" % (tid, st.get("Split", "-"), st.get("Difference", "-"),
                                          st.get("Intersection", "-"), desc))
n_all = sum(len(st) for _, _, st in TABLE)
n_ok = sum(1 for _, _, st in TABLE for v in st.values() if v == "CORRECT")
print("")
print("  %d of %d operations correct" % (n_ok, n_all))

safe(bake)
