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<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")