require 'sketchup.rb' ########################################################### # # Copyright (C) 2008 Uli Tessel (utessel@gmx.de) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # ########################################################### # # Outliner # # Outliner creates (groups of) Paths for faces: # The paths will have a configurable distance to the contour (or # outline) of the face. # One group is generated for each face, containing all "loops": So # all holes of that face are found in # the same group # # One purpose of this script is to create a milling # path for existing faces: # A (very simple) kind of CAM (Computer Aided Manufacturing) # ########################################################### # # Options: # # - Distance: The distance where the path is created. # 0 is possible (should be) # For milling: use half of the diameter of your # milling tool # # - Material: Allows to filter the faces: Only create # the paths for faces with the selected Material, # or use "all" to get all faces # # - Sort outside: # "no" will create the outline path just around # its face. # "yes" will rotate and move the result, sorts them # by width and place them one after another starting # at the origin: So you will get your paths placed # flat outside of your model # # - Wrap sort at: # only used when sort is yes: The results will be # placed into several columns. A column is full at # "Wrap sort at". 0 can be used for infinite, so # only one column is generated. # # - Noses: # For concave edges this will create a short additional # edge, required for milling so the result will fit # ########################################################### class Outliner #-------------------------------------------------------------------------- def initialize( distance, material, sort, wrapAtY, noses ) @distance = distance @material = material @sort = sort @noses = noses @wrapAtY = wrapAtY @posX = 0.0 @posY = 0.0 @maxx = 0.0 @maxy = 0.0 end #-------------------------------------------------------------------------- def MakePlanar( group, n ) # rotate group, so that its normal vector is upwards: # this is simple: # target is the vector I want: target = Geom::Vector3d.new( 0, 0, 1 ) # angle is the rotation of the current normal: angle = n.angle_between target # orth is an orthogonal vector to both (cross product) orth = target * n # which is the perfect rotation axis if (angle!=0) and (orth.length!=0) # the point does not matter as the result is moved later point = Geom::Point3d.new( 0,0,0) t = Geom::Transformation.rotation( point, orth, -angle ) group.transform! t end group.explode end #-------------------------------------------------------------------------- def PlaceIt( group ) bb = group.bounds point = bb.min dest = Geom::Point3d.new( @posX, @posY, 0 ) move = dest - point group.transform! Geom::Transformation.translation( move ) # next pos: @posY = @posY + bb.max.y - bb.min.y # keep track of max width in this column maxx = @posX + bb.max.x - bb.min.x if (maxx>@maxx) @maxx = maxx end # wrap: start new column if (@wrapAtY>0) and (@posY>@wrapAtY) @posY = 0; @posX = @maxx end end #-------------------------------------------------------------------------- def SortGroup() list = [] @newGroup.entities.each { |g| list << g } # sort groups by their height list.sort! { |a,b| b.bounds.width<=>a.bounds.width } # then Place them one after another list.each { |g| PlaceIt(g) } end #-------------------------------------------------------------------------- def combineTransformation( t1, t2 ) if (t1) if (t2) return t1 * t2 else return t1 end else return t2 end end #-------------------------------------------------------------------------- def IntersectLight( line ) l1 = [ @lastLine[0], @lastLine[1] ] l2 = [ line[0], line[1] ] p = Geom.intersect_line_line( l1, l2 ) @lastLine = line if (p) if (@prevPoint) @resultLoopLength += (p-@prevPoint).length end @prevPoint = p; end end #-------------------------------------------------------------------------- def Intersect( line, n ) l1 = [ @lastLine[0], @lastLine[1] ] l2 = [ line[0], line[1] ] p = Geom.intersect_line_line( l1, l2 ) if p then if (line[3]) # original is curve if (not @lastLine[3]) if @pts.length > 1 # prev wasn't a curve: so add the pts as edges @faceGroup.entities.add_edges( @pts ) end # and start a curve at last point: last = @pts[ @pts.length-1 ] @pts = [last]; end # original was part of a curve: always use the intersection @pts << p else if (@distance==0) firstPoint = p secondPoint = nil thirdPoint = nil else # maybe that intersection is to far: # Then use another line to connect the two # create that line: # vectors for the two original lines: vl1 = l1[0] - l1[1] vl2 = l2[1] - l2[0] # vector from original point to intersection v1 = p - line[2] # orthogonal to that: will be the direction of our connect-line v2 = v1 * n # p1 is a point on our connect-line in @distance from original point v1.length = @distance p1 = line[2] + v1 # with the orthognal we get the connect-line: l3 = [ p1, v2 ] # that line has two intersections with our original lines p3 = Geom.intersect_line_line( l1, l3 ) p4 = Geom.intersect_line_line( l2, l3 ) # now what is shorter? an edge to the connect-line or to # the intersection? test1 = p - @lastLine[0] test2 = p3 - @lastLine[0] if (test1.length < test2.length) firstPoint = p if @noses secondPoint = line[2] + v1 thirdPoint = p else secondPoint = nil thirdPoint = nil end else firstPoint = p3 secondPoint = p4 thirdPoint = nil end end if (@lastLine[3]) # prev was part of a curve, new point is not: # so close that curve @pts << firstPoint if @pts.length>1 @faceGroup.entities.add_curve( @pts ) end @pts = [] end @pts << firstPoint @pts << secondPoint if secondPoint @pts << thirdPoint if thirdPoint end end @lastLine = line if (@firstPoint == nil) @firstPoint = @pts[0] end end #-------------------------------------------------------------------------- def DoEdge( vertex1, vertex2, n ) p1 = vertex1.position p2 = vertex2.position if (@currentTransformation) p1.transform! @currentTransformation p2.transform! @currentTransformation end # vector in direction of line linev = p2-p1 @originalLoopLength += linev.length linev.normalize! # cross product of both: orthogonal to line movev = linev * n # with the required distance movev.length = @distance # defines two points of a moved line np1 = p1 + movev np2 = p2 + movev # also store the "original" point and if that was part of a curve return [ np1, np2, p1, vertex1.curve_interior? ] end #-------------------------------------------------------------------------- def DoVertex( vertex, n ) @lines << DoEdge( @lastVertex, vertex, n ) @lastVertex = vertex end #-------------------------------------------------------------------------- def DoLoop( loop, n ) tries = 0 outer = loop.outer? while (tries<2) @originalLoopLength = 0.0 @resultLoopLength = 0.0 vertices = loop.vertices @lines = Array.new @lastVertex = vertices.last vertices.each { |vertex| DoVertex( vertex, n ) } @lastLine = @lines.last @lastPoint = nil @prevPoint = nil @lines.each {|line| IntersectLight( line ) } IntersectLight( @lines.first ) # puts @resultLoopLength.to_mm.to_s+" "+@originalLoopLength.to_mm.to_s if (outer) break if @resultLoopLength>@originalLoopLength else break if @resultLoopLength<@originalLoopLength end # puts "reversing" n.reverse! tries += 1 end @pts = Array.new @lastLine = @lines.last @firstPoint = nil @lines.each {|line| Intersect( line, n ) } if (@firstPoint) @pts << @firstPoint end if (@pts.length>1) if (@lastLine[3]) @faceGroup.entities.add_curve( @pts ) else @faceGroup.entities.add_edges( @pts ) end end end #-------------------------------------------------------------------------- #-------------------------------------------------------------------------- def OutlineFace( face, trans ) if (@material) return if (@material != face.material) and (@material != face.back_material) end @currentTransformation = trans n = face.normal if (@currentTransformation) n.transform! @currentTransformation end n.normalize! if (@sort) outergroup = @newGroup.entities.add_group outergroup.name = @currentName+"_"+face.entityID.to_s @faceGroup = outergroup.entities.add_group else @faceGroup = @newGroup.entities.add_group @faceGroup.name = @currentName+"_"+face.entityID.to_s end face.loops.each { |loop| DoLoop( loop, n ) } if (@sort) MakePlanar( @faceGroup, n ) end end #-------------------------------------------------------------------------- def DoOutline( entity, trans ) return if !entity.visible? return if entity == @newGroup #----------- FACE ----------------- if entity.is_a? Sketchup::Face then OutlineFace( entity, trans ) #----------- GROUP ----------------- elsif entity.is_a? Sketchup::Group subtrans = combineTransformation( trans, entity.transformation ) DoOutlines( entity.entities, subtrans ) #----------- COMPONENT ----------------- elsif entity.is_a? Sketchup::ComponentInstance was = @currentName @currentName = @currentName+"_"+entity.definition.name subtrans = combineTransformation( trans, entity.transformation ) DoOutlines( entity.definition.entities, subtrans ) @currentName = was end end #-------------------------------------------------------------------------- def DoOutlines( entities, trans ) entities.each{ |entity| DoOutline( entity, trans ) } end #-------------------------------------------------------------------------- def Work(what) model = Sketchup.active_model model.start_operation("Outliner") @newGroup = model.entities.add_group begin @currentName = "Outline" DoOutlines( what, nil ) if (@sort) SortGroup() end model.selection.clear model.selection.add @newGroup model.commit_operation rescue => bang model.abort_operation UI.messagebox "Error: " + bang end end end #-------------------------------------------------------------------------- #----------------------------------------------------------------------------- def DoOutliner( ) what = Sketchup.active_model.selection if (what.count==0) then UI.messagebox "Nothing selected" return; end model = Sketchup.active_model materials = model.materials names = materials.collect {|m| m.name} displaynames = materials.collect {|m| m.display_name} displaynames << "all" prompts = ["Distance", "Material", "Sort outside", "Wrap sort at", "Noses" ] if $outlinerLastTimeValues values = $outlinerLastTimeValues else values = [10.mm, displaynames[0], "no", 10000.mm, "no" ] end enums = [nil, displaynames.join("|"), "yes|no", nil, "yes|no" ] results = UI.inputbox prompts, values, enums, "Outline creation" return if not results $outlinerLastTimeValues = results distance = results[0] if (results[1]=="all") material = nil else index = displaynames.index(results[1]) materialname = index ? names[index] : nil material = materialname ? materials[materialname] : nil if (material==nil) UI.messagebox( "Material "+matname+" unknown" ) return end end sort = results[2]=="yes" wrapAtY = results[3] noses = results[4]=="yes" outliner = Outliner.new(distance, material, sort, wrapAtY, noses) outliner.Work(what) end #-------------------------------------------------------------------------- #$outlinerLastTimeValues = nil #-------------------------------------------------------------------------- # Register within Sketchup if(file_loaded("outliner.rb")) menu = UI.menu("Plugins"); menu.add_item("Outliner") { DoOutliner() } end #-------------------------------------------------------------------------- file_loaded("outliner.rb")