// 对已支持的二维轮廓做几何辅助检查,不能判断完整可加工性。
//
// 检查在**展开之后**的世界坐标折线上做:块引用已经摊平、曲线已经
// 离散——检查不该关心「这条线在哪个块的哪一层引用里」。需要原始
// 实体信息的地方(句柄、半径),`Placed` 里带着源实体。
//
// 每条发现带严重程度:Info 只是说明情况、Warn 影响加工质量、
// Fatal 表示本工具要求优先修正的输入或几何问题。
//
// **这一层只报告,不修改。** 替用户改图纸(自动闭合、删重复线)
// 是另一个产品——工具把问题指出来,改不改归设计者。
//
// 整个检查是**保守**的:报出来的可以人工核对,漏报的直接切废件。
// 所以措辞宁可偏「确认一下」,不替用户下「没问题」的定论。

///|
/// 发现的严重程度。
pub(all) enum Severity {
  /// 辅助信息
  Info
  /// 需要使用者复核的问题或覆盖限制
  Warn
  /// 严重输入或几何问题
  Fatal
}

///|
/// 一条发现。
pub(all) struct Finding {
  /// 检查项名字(`退化几何` / `文字未转曲线` / …),报告按它分组
  check : String
  severity : Severity
  message : String
}

///|
/// 检查报告。
pub(all) struct Report {
  findings : Array[Finding]
}

///|
/// 统计某个严重程度的条数。
pub fn Report::count(self : Report, severity : Severity) -> Int {
  let mut n = 0
  for f in self.findings {
    let hit = match (f.severity, severity) {
      (Info, Info) | (Warn, Warn) | (Fatal, Fatal) => true
      _ => false
    }
    if hit {
      n = n + 1
    }
  }
  n
}

///|
/// 有没有致命问题。
pub fn Report::has_fatal(self : Report) -> Bool {
  self.count(Fatal) > 0
}

///|
/// 是否存在输入恢复、覆盖限制、未转轮廓文字或无效检查参数。
/// 只描述本次所选检查的完整性,不表示完整 DXF 兼容或可加工性。
/// 保留 Finding 原来的分类,避免调用方依赖解析文字来判断完整性。
pub fn Report::has_incomplete_checks(self : Report) -> Bool {
  self.findings.any(f => {
    match f.check {
      "解析" | "检查覆盖" | "文字未转曲线" | "参数" => true
      _ => false
    }
  })
}

///|
/// 一句话结论。**检查项有限,没问题不等于万事大吉**——措辞上
/// 不替用户打包票。
pub fn Report::verdict(self : Report) -> String {
  let fatal = self.count(Fatal)
  let summary = if fatal > 0 {
    "本次检查发现 \{fatal} 个严重问题,请修正并复核图纸。"
  } else {
    let warn = self.count(Warn)
    if warn > 0 {
      "本次检查发现 \{warn} 个警告,请复核图纸与加工条件。"
    } else {
      "本次已覆盖的检查项没发现问题;不构成可加工保证,请确认单位、材料、刀具和工艺。"
    }
  }
  if self.has_incomplete_checks() {
    "检查不完整:存在未解析、未转换或未能检查的内容。\{summary}"
  } else {
    summary
  }
}

///|
/// 检查参数。
pub(all) struct CheckOptions {
  /// 离散 / 重合判定的容差
  tol : Double
  /// 同图层不同实体的折线距离低于此值时提示;不测单实体窄颈。
  ///
  /// **0 表示不查这条**——净距合不合格取决于材料与工艺
  ///(1 毫米的钢和 1 毫米的纸板不是一回事,激光和铣刀也不是),
  /// 没有能通用的默认值,必须由调用方给。
  min_gap : Double
}

///|
/// 严重程度的中文标签。
pub fn severity_label(sev : Severity) -> String {
  match sev {
    Info => "提示"
    Warn => "警告"
    Fatal => "致命"
  }
}

///|
/// 报告 → 纯文本:每条发现一行 `[严重程度] [检查项] 说明`,
/// 末尾是统计和结论。CLI 和网页版都从这里取排版——两边永远不会打架。
pub fn Report::to_text(self : Report) -> String {
  let sb = StringBuilder()
  if self.findings.length() == 0 {
    sb.write_string("(没有发现)\n")
  } else {
    for f in self.findings {
      sb.write_string(
        "[\{severity_label(f.severity)}] [\{f.check}] \{f.message}\n",
      )
    }
  }
  sb.write_string(
    "致命 \{self.count(Fatal)} · 警告 \{self.count(Warn)} · 提示 \{self.count(Info)}\n",
  )
  sb.write_string(self.verdict())
  sb.to_string()
}

///|
/// 加工前检查。
///
/// `tol` 既是离散容差、也是判定尺度(重合、闭合都拿它比)。
pub fn Drawing::check(self : Drawing, opt : CheckOptions) -> Report {
  if !positive_tolerance(opt.tol) ||
    !finite_number(opt.min_gap) ||
    opt.min_gap < 0.0 {
    return self.check_expansion(opt, { placed: [], warnings: [], })
  }
  self.check_expansion(opt, self.expand(opt.tol))
}

///|
/// 按显式图纸预算检查;预算截断原因进入检查不完整报告。
pub fn Drawing::check_with_limits(
  self : Drawing,
  opt : CheckOptions,
  limits : ExpandLimits,
) -> Report {
  if !positive_tolerance(opt.tol) ||
    !finite_number(opt.min_gap) ||
    opt.min_gap < 0.0 {
    return self.check_expansion(opt, { placed: [], warnings: [], })
  }
  self.check_expansion(opt, self.expand_with_limits(opt.tol, limits))
}

///|
fn Drawing::check_expansion(
  self : Drawing,
  opt : CheckOptions,
  exp : Expansion,
) -> Report {
  let tol = opt.tol
  let findings : Array[Finding] = []
  if !positive_tolerance(tol) ||
    !finite_number(opt.min_gap) ||
    opt.min_gap < 0.0 {
    return {
      findings: [
        {
          check: "参数",
          severity: Fatal,
          message: "容差必须是正的有限数字,最小净距必须是非负有限数字",
        },
      ],
    }
  }
  // 展开过程中的问题(缺块、循环引用)本身就是检查结论
  for w in exp.warnings {
    findings.push({ check: "解析", severity: Warn, message: w, })
  }
  // 已解码不等于已检查几何。不能让未支持实体或空图纸得到无问题结论。
  let unsupported : Array[(String, Int)] = []
  let unconverted_ocs : Array[(String, Int)] = []
  let mut outlines = 0
  for p in exp.placed {
    match p.flat {
      Outline(points~, ..) =>
        if points.length() >= 2 {
          outlines = outlines + 1
        }
      _ => ()
    }
    match p.entity.geom {
      Face(..) =>
        if !((p.entity.kind() == "SOLID" || p.entity.kind() == "TRACE") &&
          p.flat is Outline(..)) {
          bump(unsupported, p.entity.kind())
        }
      Hatch(..) | Dimension(..) | Unknown => bump(unsupported, p.entity.kind())
      _ => ()
    }
    let record = p.entity.common.record
    if record.get_double(210, 0.0) != 0.0 ||
      record.get_double(220, 0.0) != 0.0 ||
      record.get_double(230, 1.0) != 1.0 {
      bump(unconverted_ocs, p.entity.kind())
    }
  }
  for item in unconverted_ocs {
    findings.push({
      check: "检查覆盖",
      severity: Warn,
      message: "\{item.1} 个 \{item.0} 实体带非默认挤出方向;本工具未转换 OCS,几何结果需另行复核",
    })
  }
  for item in unsupported {
    findings.push({
      check: "检查覆盖",
      severity: Warn,
      message: "\{item.1} 个 \{item.0} 实体的轮廓未参与检查;保留记录不表示已验证几何",
    })
  }
  if outlines == 0 {
    findings.push({
      check: "检查覆盖",
      severity: Warn,
      message: "没有可检查的二维轮廓;空文件、文字或未支持实体不能作为检查通过的依据",
    })
  }
  // 退化几何:逐条看,每条都要能定位(图层 + 句柄)
  for p in exp.placed {
    check_degenerate(p, findings)
  }
  // 文字和图层按图层汇总——不然一份图纸几百条文字会刷屏
  check_texts(exp.placed, findings)
  let tables = match self.section("TABLES") {
    Some(t) => Some(parse_tables(t))
    None => None
  }
  check_layers(tables, exp.placed, findings)
  check_closure(exp.placed, tables, tol, findings)
  check_crossings(exp.placed, tables, tol, findings)
  check_min_gap(exp.placed, tables, opt, findings)
  { findings, }
}

///|
/// 退化几何:半径非正的圆 / 弧、单点或空轮廓、零长度线段。
fn check_degenerate(p : Placed, findings : Array[Finding]) -> Unit {
  // 半径只有展开前的实体看得到(离散之后就只剩点列了)
  match p.entity.geom {
    Circle(radius~, ..) => check_radius(p, "圆", radius, findings)
    Arc(radius~, ..) => check_radius(p, "圆弧", radius, findings)
    _ => ()
  }
  match p.flat {
    Outline(points~, closed~) => {
      let n = points.length()
      if n == 0 {
        findings.push({
          check: "退化几何",
          severity: Warn,
          message: "\{where_of(p)}有一条空轮廓",
        })
      } else if n == 1 {
        // 圆 / 弧的单点轮廓已经在半径检查里报过,别重复
        let kind = p.entity.kind()
        if !(kind == "CIRCLE" || kind == "ARC") {
          findings.push({
            check: "退化几何",
            severity: Warn,
            message: "\{where_of(p)}有只有一个点的轮廓(\{kind})",
          })
        }
      } else {
        let segs = if closed { n } else { n - 1 }
        let mut zeros = 0
        let mut i = 0
        while i < segs {
          if points[i].distance(points[(i + 1) % n]) == 0.0 {
            zeros = zeros + 1
          }
          i = i + 1
        }
        if zeros > 0 {
          findings.push({
            check: "退化几何",
            severity: Warn,
            message: "\{where_of(p)}的轮廓里有 \{zeros} 段零长度线段",
          })
        }
      }
    }
    _ => ()
  }
}

///|
fn check_radius(
  p : Placed,
  what : String,
  radius : Double,
  findings : Array[Finding],
) -> Unit {
  if radius < 0.0 {
    findings.push({
      check: "退化几何",
      severity: Fatal,
      message: "\{where_of(p)}的\{what}半径是负数(\{radius}),需要修正几何定义",
    })
  } else if radius == 0.0 {
    findings.push({
      check: "退化几何",
      severity: Warn,
      message: "\{where_of(p)}的\{what}半径是 0——退化成一个点",
    })
  }
}

///|
/// 文字未转轮廓:不检查字形几何,下游软件的处理方式由使用者确认。
fn check_texts(placed : Array[Placed], findings : Array[Finding]) -> Unit {
  let counts : Array[(String, Int)] = []
  for p in placed {
    let is_text = match p.entity.kind() {
      "TEXT" | "MTEXT" | "ATTRIB" | "ATTDEF" => true
      _ => false
    }
    if is_text {
      bump(counts, p.layer)
    }
  }
  for c in counts {
    findings.push({
      check: "文字未转曲线",
      severity: Warn,
      message: "图层「\{c.0}」上有 \{c.1} 处文字:文字尚未转换为轮廓,本工具不检查字形几何,请确认下游软件的文字处理方式。",
    })
  }
}

///|
/// 图层相关的提醒:关闭 / 冻结的图层上有东西、引用了图层表里没有的图层。
fn check_layers(
  tables : Tables?,
  placed : Array[Placed],
  findings : Array[Finding],
) -> Unit {
  let tables = match tables {
    Some(t) => t
    None => return
  }
  for l in tables.layers {
    let mut status = ""
    if l.is_off() {
      status = "关闭"
    } else if l.frozen {
      status = "冻结"
    }
    if status == "" {
      continue
    }
    let mut n = 0
    for p in placed {
      if p.layer == l.name {
        n = n + 1
      }
    }
    if n > 0 {
      findings.push({
        check: "图层",
        severity: Info,
        message: "图层「\{l.name}」是\{status}的,上面有 \{n} 条实体:本工具在部分几何检查中跳过该关闭/冻结图层,请确认下游软件的实际处理方式。",
      })
    }
  }
  // 引用了图层表里没有的图层:引用完整性问题
  let unknown : Array[(String, Int)] = []
  for p in placed {
    if p.layer != "" && tables.layer(p.layer) is None {
      bump(unknown, p.layer)
    }
  }
  for u in unknown {
    findings.push({
      check: "图层",
      severity: Warn,
      message: "图层「\{u.0}」不在图层表里,但有 \{u.1} 条实体引用它",
    })
  }
}

///|
/// 轮廓闭合:同一个图层里的开放折线按端点容差串成链条,
/// 串不动之后头尾还对不上的,就是没闭合的轮廓。
///
/// 为什么不标致命:**判断不了意图**——轮廓件缺一段确实会散架,
/// 但刻线、折弯线和有意留的开口本来就是开口。报出来让人确认,
/// 比替用户下结论诚实。
///
/// 本工具的闭合检查跳过关闭 / 冻结图层;这不推断下游软件或机床行为。
///
/// 串链是朴素的贪心——每层边的数量是图纸实体级的(几百上千),
/// O(n²) 在单张图上跑得动;真遇到几万条再上空间索引。
fn check_closure(
  placed : Array[Placed],
  tables : Tables?,
  tol : Double,
  findings : Array[Finding],
) -> Unit {
  // 这层有没有按本工具图层筛选约定参与检查的开放折线
  let names : Array[String] = []
  for p in placed {
    let open = match p.flat {
      Outline(closed~, ..) => !closed
      _ => false
    }
    if !open || is_uncut_layer(tables, p.layer) {
      continue
    }
    let mut seen = false
    for n in names {
      if n == p.layer {
        seen = true
      }
    }
    if !seen {
      names.push(p.layer)
    }
  }
  for layer in names {
    // 这一层的开放折线,每条只留两个端点(中间的点不影响串链)
    let edges : Array[(Vec2, Vec2)] = []
    for p in placed {
      if p.layer != layer {
        continue
      }
      match p.flat {
        Outline(points~, closed~) =>
          if !closed && points.length() >= 2 {
            edges.push((points[0], points[points.length() - 1]))
          }
        _ => ()
      }
    }
    let (open_count, worst_gap, at) = open_chains(edges, tol)
    if open_count > 0 {
      findings.push({
        check: "轮廓闭合",
        severity: Warn,
        message: "图层「\{layer}」上有 \{open_count} 处轮廓没闭合——最宽的缝 \{worst_gap}((\{at.x}, \{at.y}) 附近);若用于封闭区域切割,请确认端点连接;开放刀路需按工艺判断",
      })
    }
  }
}

///|
/// 把一组开放段按端点(容差内)串成链,数出没闭合的条数。
/// 返回(没闭合的条数、最宽的缝、缝的位置)。
fn open_chains(
  edges : Array[(Vec2, Vec2)],
  tol : Double,
) -> (Int, Double, Vec2) {
  let used : Array[Bool] = []
  for _ in edges {
    used.push(false)
  }
  let mut open = 0
  let mut worst = 0.0
  let mut at = Vec2::zero()
  let mut i = 0
  while i < edges.length() {
    if used[i] {
      i = i + 1
      continue
    }
    used[i] = true
    let mut head = edges[i].0
    let mut tail = edges[i].1
    // 两头都往外接,接到接不动为止
    let mut grew = true
    while grew {
      grew = false
      let mut j = 0
      while j < edges.length() {
        if used[j] {
          j = j + 1
          continue
        }
        let (a, b) = edges[j]
        if tail.distance(a) <= tol {
          tail = b
          used[j] = true
          grew = true
        } else if tail.distance(b) <= tol {
          tail = a
          used[j] = true
          grew = true
        } else if head.distance(a) <= tol {
          head = b
          used[j] = true
          grew = true
        } else if head.distance(b) <= tol {
          head = a
          used[j] = true
          grew = true
        }
        j = j + 1
      }
    }
    let gap = head.distance(tail)
    if gap > tol {
      open = open + 1
      if gap > worst {
        worst = gap
        at = head
      }
    }
    i = i + 1
  }
  (open, worst, at)
}

// ── 自相交与重复切割 ────────────────────────────────────────────────────
//
// 两条检查都要「线段和线段比」,朴素两两配对是 O(n²)——图纸上千条
// 线段就顶不住了。做法是按 min_x 排序后扫掠:每段只需要和 min_x
// 落在自己 x 区间内的段比(再加 y 方向的包围盒粗筛),
// 典型图纸上候选对是线性的。

///|
/// 一条待检线段:两个端点 + 包围盒 + 身份(哪条折线的第几段)。
priv struct CutSeg {
  a : Vec2
  b : Vec2
  min_x : Double
  max_x : Double
  min_y : Double
  max_y : Double
  /// 属于第几条展开几何(数组下标)
  owner : Int
  /// 折线里的第几段
  seg : Int
  /// 折线总段数(判断环绕相邻用)
  count : Int
  closed : Bool
}

///|
/// 自相交(轮廓自己和自己交叉)与重复切割(线段重叠)。
///
/// - **自相交**只算「真正穿过」:交点严格在两段内部。端点相接不算
///   (可能是合法的连接点),共线重叠也不在这里报(归重复切割)。
/// - **重复切割**算共线重叠:同一刀会切两遍,激光会烧焦、铣刀多切一刀。
/// - 相邻段不参与(正常折线的相邻段本来就首尾相接)。
/// - 只在会被切到的图层上查,和闭合检查一个口径。
fn check_crossings(
  placed : Array[Placed],
  tables : Tables?,
  tol : Double,
  findings : Array[Finding],
) -> Unit {
  let layers : Array[String] = []
  for p in placed {
    let is_outline = match p.flat {
      Outline(..) => true
      _ => false
    }
    if is_outline && !is_uncut_layer(tables, p.layer) {
      push_unique(layers, p.layer)
    }
  }
  for layer in layers {
    let segs = layer_segments(placed, layer)
    if segs.length() < 2 {
      continue
    }
    // 按 min_x 排序后扫掠
    segs.sort_by(fn(s1, s2) {
      if s1.min_x < s2.min_x {
        -1
      } else if s1.min_x > s2.min_x {
        1
      } else {
        0
      }
    })
    let self_crossed : Array[Int] = []
    let mut dup_pairs = 0
    let mut dup_len = 0.0
    let mut dup_at = Vec2::zero()
    let mut dup_has = false
    let mut i = 0
    while i < segs.length() {
      let s = segs[i]
      let mut j = i + 1
      while j < segs.length() && segs[j].min_x <= s.max_x + tol {
        let t = segs[j]
        // y 方向粗筛
        if !(t.min_y > s.max_y + tol || s.min_y > t.max_y + tol) {
          if !(s.owner == t.owner && adjacent_segs(s, t)) {
            if s.owner == t.owner && proper_cross(s.a, s.b, t.a, t.b) {
              push_unique_int(self_crossed, s.owner)
            }
            let over = overlap_length(s.a, s.b, t.a, t.b, tol)
            if over > tol {
              dup_pairs = dup_pairs + 1
              dup_len = dup_len + over
              if !dup_has {
                dup_at = s.a
                dup_has = true
              }
            }
          }
        }
        j = j + 1
      }
      i = i + 1
    }
    if self_crossed.length() > 0 {
      findings.push({
        check: "自相交",
        severity: Warn,
        message: "图层「\{layer}」有 \{self_crossed.length()} 条轮廓自相交,请确认是否符合设计",
      })
    }
    if dup_pairs > 0 {
      findings.push({
        check: "重复切割",
        severity: Warn,
        message: "图层「\{layer}」有 \{dup_pairs} 对线段重叠(共 \{dup_len} 长度,(\{dup_at.x}, \{dup_at.y}) 附近),请确认是否存在重复路径",
      })
    }
  }
}

///|
/// 收集一个图层里所有可切轮廓的线段。
fn layer_segments(placed : Array[Placed], layer : String) -> Array[CutSeg] {
  let segs : Array[CutSeg] = []
  let mut pi = 0
  while pi < placed.length() {
    let p = placed[pi]
    if p.layer == layer {
      match p.flat {
        Outline(points~, closed~) => {
          let n = points.length()
          if n >= 2 {
            let count = if closed { n } else { n - 1 }
            let mut i = 0
            while i < count {
              let a = points[i]
              let b = points[(i + 1) % n]
              segs.push({
                a,
                b,
                min_x: a.x.min(b.x),
                max_x: a.x.max(b.x),
                min_y: a.y.min(b.y),
                max_y: a.y.max(b.y),
                owner: pi,
                seg: i,
                count,
                closed,
              })
              i = i + 1
            }
          }
        }
        _ => ()
      }
    }
    pi = pi + 1
  }
  segs
}

///|
/// 同一折线里的相邻段(闭合折线首尾相接的那对也算)。
fn adjacent_segs(s : CutSeg, t : CutSeg) -> Bool {
  let d = if s.seg > t.seg { s.seg - t.seg } else { t.seg - s.seg }
  if d == 1 {
    return true
  }
  s.closed && d == s.count - 1
}

///|
/// 两段**真正穿过**(交点严格在两段的内部)才算交叉。
/// 端点相接、共线重叠都不算——前者可能是合法的连接,
/// 后者归「重复切割」。
fn proper_cross(a1 : Vec2, a2 : Vec2, b1 : Vec2, b2 : Vec2) -> Bool {
  let d1 = orient(a1, a2, b1)
  let d2 = orient(a1, a2, b2)
  let d3 = orient(b1, b2, a1)
  let d4 = orient(b1, b2, a2)
  ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0)) &&
  ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
}

///|
/// 叉积的符号判断点在直线的哪一侧。
fn orient(a : Vec2, b : Vec2, c : Vec2) -> Double {
  b.sub(a).cross(c.sub(a))
}

///|
/// 两段共线时重叠部分的长度;不共线(或离得太远)返回 0。
///
/// 判定:b 的两个端点到 a 所在直线的垂直距离都在容差内
///(直线段是线性的,端点贴线整段就贴线),
/// 再把两段投影到 a 的方向上取区间交。
fn overlap_length(
  a1 : Vec2,
  a2 : Vec2,
  b1 : Vec2,
  b2 : Vec2,
  tol : Double,
) -> Double {
  let dir = a2.sub(a1)
  let len = dir.length()
  if len <= tol {
    // a 自身就是零长度段——退化几何那条会报它
    return 0.0
  }
  let unit = dir.scale(1.0 / len)
  let normal = unit.perp()
  if b1.sub(a1).dot(normal).abs() > tol || b2.sub(a1).dot(normal).abs() > tol {
    return 0.0
  }
  let t1 = b1.sub(a1).dot(unit)
  let t2 = b2.sub(a1).dot(unit)
  let over = t1.max(t2).min(len) - t1.min(t2).max(0.0)
  if over < 0.0 {
    0.0
  } else {
    over
  }
}

// ── 最小间隙 ────────────────────────────────────────────────────────────
//
// 两条轮廓之间留的料太窄:激光会烧糊、铣刀会崩。对应 stlkit 里的
// 「壁厚」——那边是三维里量的,这边是在平面里量的净距。
//
// 判定上有两个分寸:
//
// - **只比不同实体之间**。一条折线自己相邻的段永远贴在一起
//  (离散之后更是如此),比它们没有意义;同一个实体的远亲段
//  (折回来自已贴近自己)这版不查——那是单条折线自身的形状问题,
//   报出来噪音大过价值。
// - **端点相接的不算**。一条轮廓常常由多条线首尾串成,
//   邻居之间「净距 0」是串接,不是缺陷。

///|
/// 最小间隙检查。`min_gap` 为 0 时不查(见 `CheckOptions`)。
fn check_min_gap(
  placed : Array[Placed],
  tables : Tables?,
  opt : CheckOptions,
  findings : Array[Finding],
) -> Unit {
  if opt.min_gap <= 0.0 {
    return
  }
  let tol = opt.tol
  let layers : Array[String] = []
  for p in placed {
    let is_outline = match p.flat {
      Outline(..) => true
      _ => false
    }
    if is_outline && !is_uncut_layer(tables, p.layer) {
      push_unique(layers, p.layer)
    }
  }
  for layer in layers {
    let segs = layer_segments(placed, layer)
    if segs.length() < 2 {
      continue
    }
    segs.sort_by(fn(s1, s2) {
      if s1.min_x < s2.min_x {
        -1
      } else if s1.min_x > s2.min_x {
        1
      } else {
        0
      }
    })
    let near : Array[(Int, Int)] = []
    let mut worst = opt.min_gap
    let mut at = Vec2::zero()
    let mut i = 0
    while i < segs.length() {
      let s = segs[i]
      let mut j = i + 1
      while j < segs.length() && segs[j].min_x <= s.max_x + opt.min_gap {
        let t = segs[j]
        if s.owner != t.owner &&
          !(t.min_y > s.max_y + opt.min_gap || s.min_y > t.max_y + opt.min_gap) {
          // 端点相接的(串成一条轮廓的邻居)不算净距问题
          let touching = s.a.distance(t.a) <= tol ||
            s.a.distance(t.b) <= tol ||
            s.b.distance(t.a) <= tol ||
            s.b.distance(t.b) <= tol
          if !touching {
            let gap = segment_gap(s.a, s.b, t.a, t.b)
            if gap < opt.min_gap {
              push_unique_pair(near, s.owner, t.owner)
              if gap < worst {
                worst = gap
                at = s.a
              }
            }
          }
        }
        j = j + 1
      }
      i = i + 1
    }
    if near.length() > 0 {
      findings.push({
        check: "最小间隙",
        severity: Warn,
        message: "图层「\{layer}」有 \{near.length()} 对实体轮廓距离低于阈值——折线最小距离 \{worst}((\{at.x}, \{at.y}) 附近);请结合单位、离散容差与工艺复核",
      })
    }
  }
}

///|
/// 两段之间的最小距离。相交(含端点接触)算 0——
/// 四个「端点到对方段」的距离对相交的段是给不出 0 的,得先判相交。
fn segment_gap(a1 : Vec2, a2 : Vec2, b1 : Vec2, b2 : Vec2) -> Double {
  if segments_intersect(a1, a2, b1, b2) {
    return 0.0
  }
  let d1 = dist_to_segment(a1, b1, b2)
  let d2 = dist_to_segment(a2, b1, b2)
  let d3 = dist_to_segment(b1, a1, a2)
  let d4 = dist_to_segment(b2, a1, a2)
  d1.min(d2).min(d3).min(d4)
}

///|
/// 两条线段是否相交(含端点接触、含共线重叠)——标准的方向判定。
fn segments_intersect(a1 : Vec2, a2 : Vec2, b1 : Vec2, b2 : Vec2) -> Bool {
  let d1 = orient(b1, b2, a1)
  let d2 = orient(b1, b2, a2)
  let d3 = orient(a1, a2, b1)
  let d4 = orient(a1, a2, b2)
  if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0)) &&
    ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0)) {
    return true
  }
  // 共线情形:某个端点在另一条段上
  if d1 == 0.0 && on_segment(b1, b2, a1) {
    return true
  }
  if d2 == 0.0 && on_segment(b1, b2, a2) {
    return true
  }
  if d3 == 0.0 && on_segment(a1, a2, b1) {
    return true
  }
  if d4 == 0.0 && on_segment(a1, a2, b2) {
    return true
  }
  false
}

///|
/// 已知 p 在直线 ab 上,判断它在不在**线段** ab 的范围内(包围盒判定)。
fn on_segment(a : Vec2, b : Vec2, p : Vec2) -> Bool {
  p.x >= a.x.min(b.x) &&
  p.x <= a.x.max(b.x) &&
  p.y >= a.y.min(b.y) &&
  p.y <= a.y.max(b.y)
}

///|
/// 往「实体对」表里加一对(小下标记在前),去重。
fn push_unique_pair(pairs : Array[(Int, Int)], a : Int, b : Int) -> Unit {
  let x = if a < b { a } else { b }
  let y = if a < b { b } else { a }
  for p in pairs {
    if p.0 == x && p.1 == y {
      return
    }
  }
  pairs.push((x, y))
}

///|
fn push_unique(names : Array[String], v : String) -> Unit {
  for n in names {
    if n == v {
      return
    }
  }
  names.push(v)
}

///|
fn push_unique_int(ids : Array[Int], v : Int) -> Unit {
  for n in ids {
    if n == v {
      return
    }
  }
  ids.push(v)
}

///|
/// 是否按本工具约定跳过该图层。图层表中没有的图层继续参与检查。
/// 不据此判断下游软件或机床是否处理该图层。
fn is_uncut_layer(tables : Tables?, name : String) -> Bool {
  match tables {
    Some(t) =>
      match t.layer(name) {
        Some(l) => !l.is_enabled_for_checks()
        None => false
      }
    None => false
  }
}

///|
/// 往 (名字, 计数) 表里加一。
fn bump(counts : Array[(String, Int)], key : String) -> Unit {
  let mut i = 0
  while i < counts.length() {
    if counts[i].0 == key {
      counts[i] = (key, counts[i].1 + 1)
      return
    }
    i = i + 1
  }
  counts.push((key, 1))
}

///|
/// 发现的位置前缀:「图层「轮廓」句柄 2D 」(带尾空格,后面接「的…」)。
/// 没句柄就不提句柄。
fn where_of(p : Placed) -> String {
  let h = p.entity.handle()
  if h == "" {
    "图层「\{p.layer}」"
  } else {
    "图层「\{p.layer}」句柄 \{h} "
  }
}