# encoding: UTF-8
# mk6tc — 진짜 6.00mm 두께 절곡 판재 필러 생성.
#   그룹#2 는 Y방향 2.0472배 비균일 스케일이라 원부재 "t6" 가 모델상 12.28mm 로 부풀어 있다.
#   -> 원부재 수평단면의 중심선(절곡선 포함)을 뽑아 좌우 3mm 오프셋 = 실제 6.00mm 벽으로 재생성.
MEMBER = "SWD_02_t6_1000_jog30"
PREFIX = "SWD_02_jog30_6T"
HB = 'E:/도진팩토리/3D스캔및티칭시스템/_자동화/inbox/_보류/_mk6tc_heartbeat.txt'
def hb(s)
  begin
    File.open(HB, 'a:UTF-8') { |f| f.write("[#{Time.now.strftime('%H:%M:%S')}] #{s}\n"); f.flush }
  rescue Exception
    nil
  end
end
MM = 25.4
def r2(v); (v * 25.4).round(2); end
def ents_of(e); e.is_a?(Sketchup::ComponentInstance) ? e.definition.entities : e.entities; end
def collect(ent, tr, faces)
  ents_of(ent).each do |e|
    case e
    when Sketchup::Face
      pts = e.vertices.map { |v| v.position.transform(tr) }
      n = e.normal.transform(tr); n.normalize!
      faces << [pts, n, e.area * 645.16]
    when Sketchup::Group, Sketchup::ComponentInstance
      collect(e, tr * e.transformation, faces)
    end
  end
end
def zs_at(faces, x, y)
  res = []
  faces.each do |pts, n, _|
    next if n.z.abs < 0.05
    inside = false
    j = pts.length - 1
    (0...pts.length).each do |i|
      xi = pts[i].x; yi = pts[i].y; xj = pts[j].x; yj = pts[j].y
      inside = !inside if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
      j = i
    end
    next unless inside
    p0 = pts[0]
    d = n.x * (x - p0.x) + n.y * (y - p0.y)
    res << (p0.z - d / n.z)
  end
  res
end
def section_segs(faces, zc)
  segs = []
  faces.each do |pts, n, _|
    next if n.z.abs > 0.6
    hits = []
    j = pts.length - 1
    (0...pts.length).each do |i|
      a = pts[j]; b = pts[i]
      if (a.z - zc) * (b.z - zc) < 0
        t = (zc - a.z) / (b.z - a.z)
        hits << Geom::Point3d.new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, zc)
      end
      j = i
    end
    hits.uniq! { |p| [(p.x * 10000).round, (p.y * 10000).round] }
    segs << [hits[0], hits[1]] if hits.length >= 2
  end
  segs
end
def chain(segs, tol)
  return [[], 0] if segs.empty?
  pool = segs.dup
  s = pool.shift
  lp = [s[0], s[1]]
  changed = true
  while changed && !pool.empty?
    changed = false
    pool.each_with_index do |(a, b), i|
      if lp[-1].distance(a) < tol then lp << b; pool.delete_at(i); changed = true; break
      elsif lp[-1].distance(b) < tol then lp << a; pool.delete_at(i); changed = true; break
      elsif lp[0].distance(a) < tol then lp.unshift(b); pool.delete_at(i); changed = true; break
      elsif lp[0].distance(b) < tol then lp.unshift(a); pool.delete_at(i); changed = true; break
      end
    end
  end
  lp.pop if lp.length > 2 && lp[0].distance(lp[-1]) < tol
  [lp, pool.length]
end
# 얇고 긴 띠 다각형 -> 중심선 폴리라인
def centerline(lp)
  n = lp.length
  used = {}
  pairs = []
  (0...n).each do |i|
    next if used[i]
    # 자기 자신 이웃 제외, 가장 가까운 정점 (띠 반대편)
    best = nil; bd = nil
    (0...n).each do |j|
      next if j == i || used[j]
      next if (j - i).abs == 1 || (j - i).abs == n - 1
      d = lp[i].distance(lp[j])
      if bd.nil? || d < bd then bd = d; best = j end
    end
    next if best.nil?
    used[i] = true; used[best] = true
    pairs << [i, best, bd]
  end
  cps = pairs.map { |i, j, _| Geom::Point3d.new((lp[i].x + lp[j].x) / 2.0, (lp[i].y + lp[j].y) / 2.0, lp[i].z) }
  # i 순서대로 정렬
  order = pairs.each_with_index.sort_by { |(i, _, _), _| i }.map { |_, k| k }
  [order.map { |k| cps[k] }, pairs.map { |_, _, d| d * 25.4 }]
end
def offset_strip(cl, half)
  return nil if cl.length < 2
  nrm = []
  (0...cl.length - 1).each do |i|
    dx = cl[i + 1].x - cl[i].x; dy = cl[i + 1].y - cl[i].y
    l = Math.sqrt(dx * dx + dy * dy)
    nrm << [-dy / l, dx / l]
  end
  vn = []
  cl.each_with_index do |p, i|
    if i == 0 then vn << nrm[0]
    elsif i == cl.length - 1 then vn << nrm[-1]
    else
      ax = nrm[i - 1][0] + nrm[i][0]; ay = nrm[i - 1][1] + nrm[i][1]
      l = Math.sqrt(ax * ax + ay * ay)
      sc = (ax / l) * nrm[i][0] + (ay / l) * nrm[i][1]
      sc = 0.3 if sc < 0.3
      vn << [(ax / l) / sc, (ay / l) / sc]
    end
  end
  left = cl.each_with_index.map { |p, i| Geom::Point3d.new(p.x + vn[i][0] * half, p.y + vn[i][1] * half, p.z) }
  right = cl.each_with_index.map { |p, i| Geom::Point3d.new(p.x - vn[i][0] * half, p.y - vn[i][1] * half, p.z) }
  left + right.reverse
end

m = Sketchup.active_model
out = []
hb("START #{MEMBER}")
g2 = m.entities.grep(Sketchup::Group).find { |g| g.definition.name == "그룹#2" }
src = g2 ? g2.entities.grep(Sketchup::Group).find { |g| g.name == MEMBER } : nil
s04 = g2 ? g2.entities.grep(Sketchup::Group).find { |g| g.name == "SWD_04_t9_240x1046" } : nil
if src.nil? || s04.nil?
  out << "대상 없음 -> 중단"
else
  f4 = []
  collect(s04, g2.transformation * s04.transformation, f4)
  subs = src.definition.entities.grep(Sketchup::Group)
  foots = []
  subs.each_with_index do |c, i|
    fs = []
    collect(c, g2.transformation * src.transformation * c.transformation, fs)
    zs = fs.flat_map { |pts, _, _| pts.map { |p| p.z } }
    zc = (zs.min + zs.max) / 2.0
    lp, left = chain(section_segs(fs, zc), 0.05 / MM)
    if lp.length < 3
      out << "조각#{i + 1}: 단면 실패"; foots << nil; next
    end
    cl, widths = centerline(lp)
    strip = offset_strip(cl, 3.0 / MM)
    clen = 0.0
    (0...cl.length - 1).each { |k| clen += cl[k].distance(cl[k + 1]) * 25.4 }
    out << "조각#{i + 1}: 단면#{lp.length}점 -> 중심선 #{cl.length}점, 전개길이 #{clen.round(1)}mm, 원두께 #{widths.map { |w| w.round(2) }.inspect} -> 신규 6.00mm"
    foots << strip
  end
  y0 = 11050.0; y1 = 11850.0; zb = 106.5
  allp = foots.compact.flatten
  xmid = allp.map { |p| p.x }.inject(:+) / allp.length
  prof = []
  yy = y0
  while yy <= y1 + 0.001
    z = zs_at(f4, xmid, yy / MM)
    prof << [yy, z.empty? ? nil : z.min * 25.4]
    yy += 2.0
  end
  last = nil
  prof.each_with_index { |(y, z), i| if z.nil? then prof[i][1] = last else last = z end }
  if prof.any? { |_, z| z.nil? } || foots.compact.empty?
    out << "프로파일 결측 -> 중단"
  else
    begin
      base = File.basename(m.path, '.skp'); dir = File.dirname(m.path)
      bak = File.join(dir, "#{base}.bak_#{Time.now.strftime('%Y%m%d_%H%M%S')}_#{PREFIX}직전.skp")
      m.save_copy(bak)
      out << "사전 백업 OK -> #{bak}"
      hb("BACKUP OK")
      pents = g2.entities
      made = []
      m.start_operation("#{PREFIX} 6T 절곡 필러", true)
      foots.each_with_index do |strip, i|
        next if strip.nil?
        xs = strip.map { |p| p.x * 25.4 }
        prism = pents.add_group
        pf = prism.entities.add_face(strip.map { |p| Geom::Point3d.new(p.x, p.y, zb / MM) })
        pf.reverse! if pf.normal.z < 0
        pf.pushpull((260.0 - zb) / MM)
        hb("PRISM#{i} solid=#{prism.manifold?}")
        gb = pents.add_group
        xlo = xs.min - 30.0; xhi = xs.max + 30.0
        pl = [Geom::Point3d.new(xlo / MM, y0 / MM, zb / MM), Geom::Point3d.new(xlo / MM, y1 / MM, zb / MM)]
        prof.reverse.each { |y, z| pl << Geom::Point3d.new(xlo / MM, y / MM, z / MM) }
        gf = gb.entities.add_face(pl)
        gf.pushpull(gf.normal.x > 0 ? (xhi - xlo) / MM : -(xhi - xlo) / MM)
        if !prism.manifold? || !gb.manifold?
          prism.erase!; gb.erase!
          out << "조각#{i + 1}: 솔리드 실패"; next
        end
        res = prism.intersect(gb)
        hb("INT#{i} #{res.class}")
        if res
          res.name = "#{PREFIX}_#{i + 1}_20260919"
          made << res
        end
      end
      src.hidden = true
      m.commit_operation
      hb("COMMIT made=#{made.length}")
      out << "생성 #{made.length}개"
      made.each do |gp|
        b = gp.bounds
        out << "  #{gp.name} bbox (#{r2(b.min.x)},#{r2(b.min.y)},#{r2(b.min.z)})..(#{r2(b.max.x)},#{r2(b.max.y)},#{r2(b.max.z)}) solid=#{gp.manifold?}"
      end
    rescue Exception => e
      m.commit_operation rescue nil
      out << "ERR #{e.class}: #{e.message}"
      hb("ERR #{e.message}")
    end
  end
end
puts out.join("\n")
hb("END")
"MK6TC"
