# encoding: UTF-8
# ridge7 — 보강 능선을 6T 절곡 채널(hat)로 재작성 + 하판 경사(6.4452도)에 밀착
#   ① selection 확인 ② 하판 아래 잔재(이전 통짜/오배치) 제거 ③ 판 경사면 좌표계에서 절곡 단면 생성
HB = 'E:/도진팩토리/3D스캔및티칭시스템/_자동화/inbox/_보류/_r7_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
THK = 6.0
RI = 6.0
RC = RI + THK / 2.0
KF = 0.42
def vol(g); (g.volume rescue -1) * 16387.064; 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.outer_loop.vertices.map { |v| v.position.transform(tr) }
      n = e.normal.transform(tr); n.normalize!
      faces << [pts, n]
    when Sketchup::Group, Sketchup::ComponentInstance
      collect(e, tr * e.transformation, faces)
    end
  end
end
def zhit(faces, x, y, up)
  res = []
  faces.each do |pts, n|
    next if n.z.abs < 0.05
    next if up && n.z < 0.05
    next if !up && n.z > -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 fillet2(pl, r, seg)
  outp = [pl[0]]
  (1...pl.length - 1).each do |i|
    a = pl[i - 1]; b = pl[i]; c = pl[i + 1]
    v1 = [a[0] - b[0], a[1] - b[1]]; v2 = [c[0] - b[0], c[1] - b[1]]
    l1 = Math.sqrt(v1[0]**2 + v1[1]**2); l2 = Math.sqrt(v2[0]**2 + v2[1]**2)
    next if l1 < 1e-9 || l2 < 1e-9
    u1 = [v1[0] / l1, v1[1] / l1]; u2 = [v2[0] / l2, v2[1] / l2]
    ca = [[u1[0] * u2[0] + u1[1] * u2[1], 1.0].min, -1.0].max
    ang = Math.acos(ca)
    if ang > Math::PI - 0.02 || ang < 0.02
      outp << b
      next
    end
    tn = [r / Math.tan(ang / 2.0), l1 * 0.45, l2 * 0.45].min
    rr = tn * Math.tan(ang / 2.0)
    p1 = [b[0] + u1[0] * tn, b[1] + u1[1] * tn]
    p2 = [b[0] + u2[0] * tn, b[1] + u2[1] * tn]
    bis = [u1[0] + u2[0], u1[1] + u2[1]]
    bl = Math.sqrt(bis[0]**2 + bis[1]**2)
    if bl < 1e-9
      outp << b
      next
    end
    dc = rr / Math.sin(ang / 2.0)
    cx = b[0] + bis[0] / bl * dc; cy = b[1] + bis[1] / bl * dc
    a1 = Math.atan2(p1[1] - cy, p1[0] - cx)
    a2 = Math.atan2(p2[1] - cy, p2[0] - cx)
    da = a2 - a1
    da -= 2 * Math::PI while da > Math::PI
    da += 2 * Math::PI while da < -Math::PI
    (0..seg).each { |k| t = a1 + da * k / seg.to_f; outp << [cx + rr * Math.cos(t), cy + rr * Math.sin(t)] }
  end
  outp << pl[-1]
  res = [outp[0]]
  outp[1..-1].each { |p| res << p if Math.sqrt((p[0] - res[-1][0])**2 + (p[1] - res[-1][1])**2) > 0.02 }
  res
end
def off2(pl, h)
  nrm = []
  (0...pl.length - 1).each do |i|
    dx = pl[i + 1][0] - pl[i][0]; dz = pl[i + 1][1] - pl[i][1]
    l = Math.sqrt(dx * dx + dz * dz)
    nrm << [-dz / l, dx / l]
  end
  vn = pl.each_with_index.map do |p, i|
    if i == 0 then nrm[0]
    elsif i == pl.length - 1 then nrm[-1]
    else
      ax = nrm[i - 1][0] + nrm[i][0]; az = nrm[i - 1][1] + nrm[i][1]
      l = Math.sqrt(ax * ax + az * az)
      sc = (ax / l) * nrm[i][0] + (az / l) * nrm[i][1]
      sc = 0.3 if sc < 0.3
      [(ax / l) / sc, (az / l) / sc]
    end
  end
  [pl.each_with_index.map { |p, i| [p[0] + vn[i][0] * h, p[1] + vn[i][1] * h] },
   pl.each_with_index.map { |p, i| [p[0] - vn[i][0] * h, p[1] - vn[i][1] * h] }]
end
def boxgrp(ents, x0, x1, y0, y1, z0, z1)
  g = ents.add_group
  f = g.entities.add_face([Geom::Point3d.new(x0 / 25.4, y0 / 25.4, z0 / 25.4),
                           Geom::Point3d.new(x1 / 25.4, y0 / 25.4, z0 / 25.4),
                           Geom::Point3d.new(x1 / 25.4, y1 / 25.4, z0 / 25.4),
                           Geom::Point3d.new(x0 / 25.4, y1 / 25.4, z0 / 25.4)])
  f.reverse! if f.normal.z < 0
  f.pushpull((z1 - z0) / 25.4)
  g
end

m = Sketchup.active_model
out = []
hb("START")
sel = m.selection.to_a
out << "[selection] #{sel.length}개" + (sel.empty? ? " -> 보강능선 대상으로 간주" : ": #{sel.map { |e| e.respond_to?(:name) ? e.name : e.class.to_s }.inspect}")
plate = m.entities.grep(Sketchup::Group).find { |g| g.name == "조립부원판_20260919" }
if plate.nil?
  out << "하판 없음 -> 중단"
else
  fw = []; collect(plate, plate.transformation, fw)
  ys = 11375.0
  za = zhit(fw, 8850.0 / MM, ys / MM, true).max * 25.4
  zb2 = zhit(fw, 9200.0 / MM, ys / MM, true).max * 25.4
  slope = (zb2 - za) / 350.0
  ang = Math.atan(slope)
  ztop = lambda { |x| za + slope * (x - 8850.0) }
  zbot = lambda { |x| ztop.call(x) - 12.0 / Math.cos(ang) }
  out << "하판 상면 경사 #{( -ang * 180 / Math::PI).round(4)}도, 두께 12T -> 능선은 CEO 지시대로 6T 절곡 채널, 하판 하면에 밀착"
  ry0 = 10980.0; ry1 = 11830.0
  xs_c = [8950.0, 9130.0]
  wtop = 100.0; wbot = 60.0; dep = 25.0
  begin
    base = File.basename(m.path, '.skp'); dir = File.dirname(m.path)
    m.save_copy(File.join(dir, "#{base}.bak_#{Time.now.strftime('%Y%m%d_%H%M%S')}_r7직전.skp"))
    hb("BACKUP OK")
    m.start_operation("6T 절곡 보강능선 재작성", true)
    body = plate
    # 기존 잔재 제거: 하판 하면 아래 전부
    xs_c.each_with_index do |xc, i|
      zc = zbot.call(xc + 70.0) - 0.3
      cb = boxgrp(m.entities, xc - 75.0, xc + 75.0, 10050.0, 11950.0, -250.0, zc)
      r = cb.subtract(body)
      body = r if r
      hb("CLEAN#{i} #{r.class}")
    end
    body.name = "조립부원판_20260919"
    out << "하판 잔재 제거 후: 체적 #{vol(body).round(0)}mm3 y #{(body.bounds.min.y*25.4).round(1)}..#{(body.bounds.max.y*25.4).round(1)} z #{(body.bounds.min.z*25.4).round(1)}..#{(body.bounds.max.z*25.4).round(1)} solid=#{body.manifold?}"
    # 6T 절곡 채널 2줄 (판 경사 좌표계)
    made = []
    xs_c.each_with_index do |xc, idx|
      ca = Math.cos(ang); sa = Math.sin(ang)
      zb0 = zbot.call(xc)
      # (u,w) -> world(x,z) : x = xc + u*ca - w*sa ,  z = zb0 + u*sa + w*ca
      f2w = lambda { |u, w| [xc + u * ca - w * sa, zb0 + u * sa + w * ca] }
      hw = THK / 2.0
      pathuw = [[-70.0, -hw], [-50.0, -hw], [-30.0, -dep - hw], [30.0, -dep - hw], [50.0, -hw], [70.0, -hw]]
      bends = []
      (1...pathuw.length - 1).each do |i|
        v1 = [pathuw[i][0] - pathuw[i - 1][0], pathuw[i][1] - pathuw[i - 1][1]]
        v2 = [pathuw[i + 1][0] - pathuw[i][0], pathuw[i + 1][1] - pathuw[i][1]]
        l1 = Math.sqrt(v1[0]**2 + v1[1]**2); l2 = Math.sqrt(v2[0]**2 + v2[1]**2)
        cc = [[(v1[0] * v2[0] + v1[1] * v2[1]) / (l1 * l2), 1.0].min, -1.0].max
        bends << Math.acos(cc) * 180 / Math::PI
      end
      fp = fillet2(pathuw, RC, 6)
      up, dn = off2(fp, THK / 2.0)
      poly = (up + dn.reverse).map do |p|
        xw, zw = f2w.call(p[0], p[1])
        Geom::Point3d.new(xw / MM, ry0 / MM, zw / MM)
      end
      g = m.entities.add_group
      f = g.entities.add_face(poly)
      f.pushpull((ry1 - ry0) / MM)
      bb = g.bounds
      if (bb.min.y * 25.4) < ry0 - 1.0
        g.transform!(Geom::Transformation.translation(Geom::Vector3d.new(0, (ry1 - ry0) / MM, 0)))
        bb = g.bounds
      end
      g.name = "보강능선_6T_#{idx == 0 ? 'A' : 'B'}_20260919"
      hb("RIDGE#{idx} solid=#{g.manifold?} y #{(bb.min.y*25.4).round(1)}..#{(bb.max.y*25.4).round(1)}")
      flat = 0.0
      (0...fp.length - 1).each { |i| flat += Math.sqrt((fp[i + 1][0] - fp[i][0])**2 + (fp[i + 1][1] - fp[i][1])**2) }
      ba = bends.map { |a| (Math::PI / 180.0) * a * (RI + KF * THK) }
      made << [g, xc, bends, flat, ba, bb]
    end
    m.commit_operation
    hb("COMMIT")
    made.each_with_index do |(g, xc, bends, flat, ba, bb), i|
      out << "#{g.name}: solid=#{g.manifold?} 두께 #{THK}T 일정, 절곡 #{bends.length}개 각도 #{bends.map { |a| a.round(1) }.inspect}도, 내R #{RI}/중립R #{RC}/외R #{(RI + THK).round(1)}, K=#{KF}"
      out << "   상부폭 #{wtop}/하부폭 #{wbot}/깊이 #{dep}, 길이 #{((bb.max.y - bb.min.y) * 25.4).round(1)}mm (y #{(bb.min.y*25.4).round(1)}~#{(bb.max.y*25.4).round(1)}), 전개 블랭크 폭 #{(flat + ba.inject(0.0) { |s, v| s + v } - bends.length * 0).round(1)}mm"
      out << "   x #{(bb.min.x*25.4).round(1)}..#{(bb.max.x*25.4).round(1)} z #{(bb.min.z*25.4).round(1)}..#{(bb.max.z*25.4).round(1)} 체적 #{vol(g).round(0)}mm3"
    end
    # 밀착 검증: 능선 상단 플랜지 z vs 하판 하면 z
    fw2 = []; collect(body, body.transformation, fw2)
    made.each do |(g, xc, _, _, _, _)|
      gaps = []
      [-65.0, -55.0, 55.0, 65.0].each do |u|
        xw = xc + u * Math.cos(ang)
        zr = zhit([[g.entities.grep(Sketchup::Face).map { |f| f.outer_loop.vertices.map { |v| v.position.transform(g.transformation) } }, nil]], 0, 0, true) rescue []
        zp = zhit(fw2, xw / MM, 11400.0 / MM, false)
        gt = g.bounds
        gaps << (zp.empty? ? nil : (zp.min * 25.4))
      end
      out << "#{g.name} 하판 하면 z(해당 x) = #{gaps.compact.map { |v| v.round(2) }.inspect}, 능선 상단 z = #{(g.bounds.max.z * 25.4).round(2)} -> 틈 #{gaps.compact.empty? ? '-' : (gaps.compact.max - g.bounds.max.z * 25.4).abs.round(3)}mm"
    end
  rescue Exception => e
    m.commit_operation rescue nil
    out << "ERR #{e.class}: #{e.message}"
    hb("ERR #{e.message}")
  end
end
v = m.active_view
v.camera = Sketchup::Camera.new(Geom::Point3d.new(8450 / 25.4, 10350 / 25.4, -260 / 25.4), Geom::Point3d.new(9040 / 25.4, 11380 / 25.4, 95 / 25.4), Geom::Vector3d.new(0, 0, 1))
out << "capture=#{v.write_image('E:/도진팩토리/3D스캔및티칭시스템/_자동화/inbox/_보류/_capR7.png', 1400, 900, true)}"
puts out.join("\n")
hb("END")
"RIDGE7"
