# encoding: UTF-8 # 하판 볼트홀 3+5 비대칭 -> 4+4 대칭 재배치 (판을 새로 생성해 교체, 기존판은 hidden 보존) m = Sketchup.active_model cur = m.entities.grep(Sketchup::Group).find { |e| e.name == '조립부원판_20260919' } raise "하판 없음" if cur.nil? tr = cur.transformation best=nil; ba=0.0 cur.entities.grep(Sketchup::Face).each { |f| n=f.normal.transform(tr); next unless n.z<-0.05; if f.area>ba then ba=f.area; best=f end } pa,pb,pc,pd = Geom.fit_plane_to_points(best.outer_loop.vertices.map{|v| v.position.transform(tr)}) zf = lambda { |x,y| -(pa*x + pb*y + pd)/pc } tv = Geom::Vector3d.new(1.0, 0.0, -pa/pc); tv.normalize! nv = Geom::Vector3d.new(-pa, -pb, -pc); nv.normalize!; nv.reverse! if nv.z > 0 wv = tv.cross(nv); wv.normalize! # 기존 홀 실측 olds = best.loops.reject{|l| l.outer?}.map do |l| b=Geom::BoundingBox.new; l.vertices.each{|v| b.add(v.position.transform(tr))} [((b.min.x.to_mm+b.max.x.to_mm)/2.0).round(2), ((b.min.y.to_mm+b.max.y.to_mm)/2.0).round(2)] end rows = olds.group_by{|c| c[1]} puts "기존 홀 #{olds.size}개 / 열별: " + rows.map{|y,v| "y=#{y} -> #{v.size}개 x=#{v.map{|c| c[0]}.sort.inspect}"}.join(' | ') YS = rows.keys.sort # [10753.63, 12002.72] X0 = 8830.45; X1 = 9210.45 PITCH = (X1 - X0) / 3.0 XS = (0..3).map { |i| (X0 + PITCH*i).round(3) } R = 9.0 puts "신규 배치: x=#{XS.inspect} 피치=%.3fmm / y열=#{YS.inspect} / Ø%.1f" % [PITCH, R*2] outer = best.outer_loop.vertices.map { |v| v.position.transform(tr) } circle = lambda do |cx, cy| c = Geom::Point3d.new(cx.mm, cy.mm, zf.call(cx.mm, cy.mm)) (0...24).map { |i| a = 2*Math::PI*i/24.0; c.offset(tv, (R*Math.cos(a)).mm).offset(wv, (R*Math.sin(a)).mm) } end m.start_operation('볼트홀 4+4 대칭 재배치', true) g3 = m.entities.add_group fo = g3.entities.add_face(outer) raise "외곽면 실패" if fo.nil? made = 0 YS.each { |yy| XS.each { |xx| hf = g3.entities.add_face(circle.call(xx, yy)); (hf.erase!; made += 1) if hf && hf.valid? } } fo = g3.entities.grep(Sketchup::Face).max_by { |x| x.area } nloops = fo.loops.size - 1 fo.reverse! if fo.normal.z > 0 fo.pushpull(-12.mm) g3.name = '조립부원판_20260919' cur.name = '조립부원판_구2_홀비대칭_20260919' cur.hidden = true m.commit_operation puts "뚫은 홀=#{made} / 신규 하면 내부루프=#{nloops}" b = g3.bounds puts "신규하판 solid=#{g3.manifold?} vol=%.0fmm3 bbox x %.2f..%.2f y %.2f..%.2f z %.2f..%.2f" % [g3.volume.to_f*(25.4**3), b.min.x.to_mm,b.max.x.to_mm,b.min.y.to_mm,b.max.y.to_mm,b.min.z.to_mm,b.max.z.to_mm] # 검증: 실제 생성된 홀 실측 + 좌우/전후 대칭 t3 = g3.transformation bf=nil; ba3=0.0 g3.entities.grep(Sketchup::Face).each{|f| n=f.normal.transform(t3); next unless n.z<-0.05; if f.area>ba3 then ba3=f.area; bf=f end} cs = bf.loops.reject{|l| l.outer?}.map do |l| bb=Geom::BoundingBox.new; l.vertices.each{|v| bb.add(v.position.transform(t3))} [((bb.min.x.to_mm+bb.max.x.to_mm)/2.0).round(3), ((bb.min.y.to_mm+bb.max.y.to_mm)/2.0).round(3), (bb.max.y.to_mm-bb.min.y.to_mm).round(2)] end cs.sort_by!{|c| [c[1], c[0]]} puts "검증 홀 #{cs.size}개:" cs.group_by{|c| c[1]}.each{|y,v| puts " y=#{y} : #{v.size}개 x=#{v.map{|c| c[0]}.inspect} Ø(y방향)=#{v.map{|c| c[2]}.uniq.inspect}"} xc = (b.min.x.to_mm + b.max.x.to_mm)/2.0 mism = cs.map{|c| d = cs.map{|o| ((o[0]-(2*xc-c[0])).abs + (o[1]-c[1]).abs)}.min; d}.max yc = (b.min.y.to_mm + b.max.y.to_mm)/2.0 mismy = cs.map{|c| cs.map{|o| ((o[0]-c[0]).abs + (o[1]-(2*yc-c[1])).abs)}.min}.max puts "좌우(x) 미러 최대불일치=%.4fmm / 전후(y) 미러 최대불일치=%.4fmm" % [mism, mismy] puts "연단거리: x좌 %.2f / x우 %.2f / y하 %.2f / y상 %.2f (권장 36.0)" % [XS.min - b.min.x.to_mm, b.max.x.to_mm - XS.max, YS.min - b.min.y.to_mm, b.max.y.to_mm - YS.max] m.save puts "SAVED #{File.mtime(m.path)}" nil