# encoding: UTF-8
# fold_r — 절곡재 1장(단일 솔리드): 판 한 장을 jog 단차로 접되 모든 절곡부에 굽힘 R 적용.
#   중립선 R(Rc)=9.0 -> 내R 6.0(=1.0t), 외R 12.0(=Rc+3). 벽 두께 6.00mm 균일.
#   상단=주황판 하면, 하단=하판 경사면, 길이=사이드막음판 안쪽.
MEMBER = "@@MEMBER@@"
NEWNAME = "@@NEWNAME@@"
HB = 'E:/도진팩토리/3D스캔및티칭시스템/_자동화/inbox/_보류/_foldr_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
RC = 9.0      # 중립선(판 중심) 굽힘 반경
THK = 6.0     # 판두께
KF = 0.42     # K-factor
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_down(faces, x, y)
  res = []
  faces.each do |pts, n, _|
    next if 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
# 폴리라인 모서리를 반경 r 로 라운딩 (평면 XY, z=0)
def fillet(pl, r, seg)
  return pl if pl.length < 3
  outp = [pl[0]]
  (1...pl.length - 1).each do |i|
    a = pl[i - 1]; b = pl[i]; c = pl[i + 1]
    v1 = [a.x - b.x, a.y - b.y]; v2 = [c.x - b.x, c.y - b.y]
    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]
    cosang = [[u1[0] * u2[0] + u1[1] * u2[1], 1.0].min, -1.0].max
    ang = Math.acos(cosang)                 # 두 변 사이 각
    if ang > Math::PI - 0.02 || ang < 0.02
      outp << b
      next
    end
    tan = r / Math.tan(ang / 2.0)           # 접점까지 거리
    tan = [tan, l1 * 0.45, l2 * 0.45].min
    rr = tan * Math.tan(ang / 2.0)
    p1 = Geom::Point3d.new(b.x + u1[0] * tan, b.y + u1[1] * tan, 0)
    p2 = Geom::Point3d.new(b.x + u2[0] * tan, b.y + u2[1] * tan, 0)
    # 원호 중심
    bis = [u1[0] + u2[0], u1[1] + u2[1]]
    bl = Math.sqrt(bis[0]**2 + bis[1]**2)
    next (outp << b) if bl < 1e-9
    dc = rr / Math.sin(ang / 2.0)
    cx = b.x + bis[0] / bl * dc; cy = b.y + bis[1] / bl * dc
    a1 = Math.atan2(p1.y - cy, p1.x - cx)
    a2 = Math.atan2(p2.y - cy, p2.x - cx)
    da = a2 - a1
    da -= 2 * Math::PI while da > Math::PI
    da += 2 * Math::PI while da < -Math::PI
    (0..seg).each do |k|
      t = a1 + da * k / seg.to_f
      outp << Geom::Point3d.new(cx + rr * Math.cos(t), cy + rr * Math.sin(t), 0)
    end
  end
  outp << pl[-1]
  res = [outp[0]]
  outp[1..-1].each { |p| res << p if res[-1].distance(p) > 0.02 / 25.4 }
  res
end
def offset_poly(cl, half)
  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.map do |p, i|
    if i == 0 then nrm[0]
    elsif i == cl.length - 1 then 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
      [(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]
end

m = Sketchup.active_model
out = []
hb("START #{MEMBER}")
g2 = m.entities.grep(Sketchup::Group).find { |g| g.definition.name == "그룹#2" }
src = g2.entities.grep(Sketchup::Group).find { |g| g.name == MEMBER }
s04 = g2.entities.grep(Sketchup::Group).find { |g| g.name == "SWD_04_t9_240x1046" }
won = m.entities.grep(Sketchup::Group).find { |g| g.name == "조립부원판_20260919" }
if [src, s04, won].any?(&:nil?)
  out << "대상 없음 -> 중단"
  hb("ABORT")
else
  f4 = []; collect(s04, g2.transformation * s04.transformation, f4)
  fs = []; collect(src, g2.transformation * src.transformation, fs)
  walls = fs.select { |_, n, _| n.z.abs < 0.3 }.sort_by { |_, _, a| -a }
  ref = walls.first[1]
  side = walls.select { |_, n, _| n.x * ref.x + n.y * ref.y > 0.5 }
  segs = side.map do |pts, n, a|
    p1 = nil; p2 = nil; best = -1
    pts.each_with_index do |p, i|
      pts.each_with_index do |q, j|
        next if j <= i
        d = (p.x - q.x)**2 + (p.y - q.y)**2
        if d > best
          best = d; p1 = p; p2 = q
        end
      end
    end
    [Geom::Point3d.new(p1.x, p1.y, 0), Geom::Point3d.new(p2.x, p2.y, 0), Math.sqrt(best) * 25.4]
  end.select { |_, _, l| l > 20.0 }
  segs = segs.sort_by { |a, b, _| [a.y, b.y].min }
  pl = []
  segs.each do |a, b, _|
    s2 = (a.y <= b.y) ? [a, b] : [b, a]
    if pl.empty?
      pl << s2[0] << s2[1]
    else
      pl << s2[0] if pl[-1].distance(s2[0]) > 0.5 / 25.4
      pl << s2[1]
    end
  end
  base_pl = [pl[0]]
  pl[1..-1].each { |p| base_pl << p if base_pl[-1].distance(p) > 0.5 / 25.4 }
  # 기준 판면 -> 중립선(판 중심): 법선 반대쪽으로 3mm
  nl, nr = offset_poly(base_pl, THK / 2.0 / MM)
  cand = [nl, nr]
  # 부재 내부 쪽(기준면 법선 반대) 선택
  test = lambda { |arr| (arr[0].x - base_pl[0].x) * ref.x + (arr[0].y - base_pl[0].y) * ref.y }
  center = test.call(nl) < 0 ? nl : nr
  # 절곡부 R 적용
  straight = []
  (0...center.length - 1).each { |i| straight << center[i].distance(center[i + 1]) * 25.4 }
  bends = []
  (1...center.length - 1).each do |i|
    v1 = [center[i].x - center[i - 1].x, center[i].y - center[i - 1].y]
    v2 = [center[i + 1].x - center[i].x, center[i + 1].y - center[i].y]
    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
    cosang = [[(v1[0] * v2[0] + v1[1] * v2[1]) / (l1 * l2), 1.0].min, -1.0].max
    ang = Math.acos(cosang) * 180 / Math::PI
    next if ang < 1.0
    cross = v1[0] * v2[1] - v1[1] * v2[0]
    bends << [(center[i].y * 25.4).round(1), ang.round(2), cross > 0 ? "좌(CCW)" : "우(CW)"]
  end
  cfil = fillet(center, RC / MM, 8)
  lft, rgt = offset_poly(cfil, THK / 2.0 / MM)
  poly = lft + rgt.reverse
  ylo = 10879.34; yhi = 11872.77
  poly = poly.map do |p|
    yy = p.y * 25.4
    yy = ylo - 5.0 if yy <= 10920.0
    yy = yhi + 5.0 if yy >= 11830.0
    Geom::Point3d.new(p.x, yy / 25.4, p.z)
  end
  # 전개(블랭크) 폭: 직선부 합 + 굽힘보정(BA)
  ri = RC - THK / 2.0
  ba = bends.map { |_, a, _| (Math::PI / 180.0) * a * (ri + KF * THK) }
  flat_sum = 0.0
  (0...center.length - 1).each { |i| flat_sum += center[i].distance(center[i + 1]) * 25.4 }
  setback = bends.map { |_, a, _| 2.0 * (RC) * Math.tan((a * Math::PI / 180.0) / 2.0) }
  blank_w = flat_sum - setback.inject(0.0) { |s, v| s + v } + ba.inject(0.0) { |s, v| s + v }
  out << "절곡 #{bends.length}회, 내R #{ri.round(1)}mm / 중립선R #{RC} / 외R #{(RC + THK / 2.0).round(1)}mm, K=#{KF}, 두께 #{THK}mm"
  bends.each_with_index { |(y, a, d), i| out << "   절곡#{i + 1}: y=#{y} 각도 #{a}도 #{d} BA=#{ba[i].round(2)}mm" }
  out << "전개 블랭크 폭(단면 펼침) #{blank_w.round(1)}mm (직선합 #{flat_sum.round(1)} - setback #{setback.inject(0.0){|s,v| s+v}.round(1)} + BA #{ba.inject(0.0){|s,v| s+v}.round(1)})"
  xs = poly.map { |p| p.x * 25.4 }
  prof = []
  yy = ylo
  while yy <= yhi + 0.001
    zz = []
    9.times do |k|
      xq = xs.min + (xs.max - xs.min) * k / 8.0
      r = zs_down(f4, xq / MM, yy / MM)
      zz << r.min unless r.empty?
    end
    prof << [yy, zz.empty? ? nil : zz.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 }
  last = nil
  (prof.length - 1).downto(0) { |i| if prof[i][1].nil? then prof[i][1] = last else last = prof[i][1] end }
  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')}_foldR직전.skp"))
    hb("BACKUP OK")
    m.start_operation("#{NEWNAME} R절곡 단일판", true)
    zb = 40.0
    g = m.entities.add_group
    f = g.entities.add_face(poly.map { |p| Geom::Point3d.new(p.x, p.y, zb / MM) })
    f.reverse! if f.normal.z < 0
    f.pushpull((300.0 - zb) / MM)
    hb("PRISM solid=#{g.manifold?}")
    gb = m.entities.add_group
    xlo = xs.min - 60.0; xhi = xs.max + 60.0
    plg = [Geom::Point3d.new(xlo / MM, ylo / MM, zb / MM), Geom::Point3d.new(xlo / MM, yhi / MM, zb / MM)]
    prof.reverse.each { |y, z| plg << Geom::Point3d.new(xlo / MM, y / MM, z / MM) }
    gf = gb.entities.add_face(plg)
    gf.pushpull(gf.normal.x > 0 ? (xhi - xlo) / MM : -(xhi - xlo) / MM)
    body = g.intersect(gb)
    hb("TOP #{body.class}")
    res = body
    if body
      wc = m.entities.add_group
      wc.entities.add_instance(won.definition, won.transformation).explode
      r2 = wc.manifold? ? wc.subtract(body) : nil
      res = r2 if r2
      oc = m.entities.add_group
      oc.entities.add_instance(s04.definition, g2.transformation * s04.transformation).explode
      r3 = oc.manifold? ? oc.subtract(res) : nil
      res = r3 if r3
      res.name = NEWNAME
      b = res.bounds
      out << "생성 #{NEWNAME}: 단일솔리드=#{res.manifold?} 면수 #{res.entities.grep(Sketchup::Face).length} 그룹내 하위그룹 #{res.entities.grep(Sketchup::Group).length}개"
      out << "  bbox (#{(b.min.x*25.4).round(2)},#{(b.min.y*25.4).round(2)},#{(b.min.z*25.4).round(2)})..(#{(b.max.x*25.4).round(2)},#{(b.max.y*25.4).round(2)},#{(b.max.z*25.4).round(2)}) 길이 #{((b.max.y-b.min.y)*25.4).round(1)}mm"
      out << "  전개 블랭크: #{blank_w.round(1)}mm × #{((b.max.y-b.min.y)*25.4).round(1)}mm × t6.00"
    else
      out << "intersect 실패"
    end
    m.commit_operation
    hb("COMMIT")
  rescue Exception => e
    m.commit_operation rescue nil
    out << "ERR #{e.class}: #{e.message}"
    hb("ERR #{e.message}")
  end
end
puts out.join("\n")
hb("END")
"FOLDR"
