=begin
PipeAlongPath
TIG (c) 2005 - 2014
Description : Creates circular faced 'followme' pipe extrusions along a path - 
Use it to make pipes, ducts etc, starting from a selection of joined edges
Usage : 
Select joined lines, arcs, circles, curves, etc.
Select "Pipe Along Path" from the Plugins menu. 
In the dialog choose:-
Outside diameter:   in current units or use suffix for other, default 110mm/4"
Inside diameter:    in current units or use suffix for other, default 110mm/4"
Number of segments: default 24
Cpoints?:    [Yes/No, default=true to add cppoints at the pipe's vertices]
Cline layer: [layer for the path, default="XCLINE", make blank for 'none']:  
In group?:   [Yes/No to move selected path into pipe's group, default=true]
OK.  
Last used setting are remembered across sessions.
The pipe-extrusion is made*.
The pipe-extrusion is grouped so it does not interact with adjacent surfaces - 
afterwards just explode it if appropriate.
Edit it to intesect with model and tidy up to make tees etc...
The pipe also has construction points added at vertices if you set Cpoints=true.  
These can be used for snapping, if not wanted 'erase construction geometry' 
will remove them globally or just within a group that you are editing.
One Undo to remove the construction points, a second to Undo the Pipe itself.
The default for extrusion face segments is 24, the minimum is 3.
The alignment is always 'Centroid' - along the pipe's centre line.
The diameter is always measured square to the vector of the 
first path's line.  A diameter that is less than the length of pieces of path 
edge might give correct but unexpected extrusions, which might need manually 
tidying...
Closed loop paths are fully extruded in a loop.
Multiple arcs and other complex 3D paths might give unexpected results.  
Note that SketchUp can't handle very small faces in its FollowMe mode - so any 
Arc bends of 8" radius or less that have the same radius for the pipe (o/d) 
applied will almost certainly cause a crash / "bug splat" and are trapped out 
BUT note that similar radii in 'welded' Curves are NOT easily trappable and so 
they may always cause a crash with a "bug-splat", so avoid using these type of 
path with small radii bends/pipes - or you can keep it but use 'scale' as
explained below...
IF you must have this matching small diameter pipe and arc or welded elbow 
bends then to get it to work you can make the path a 
group, scale it by a factor so it's larger (say x10), then 
edit it and use this tool within it applying the diameters x10, 
after it's all made scale the group back down by x0.1 and 
explode it and it'll all be OK.
#
Both diameters cannot be zero and will return an error.
If one of the diamaters is zero then you get a 'tube' rather 
than a pipe.
If the diamaters entered are equal the inside one is taken as 
zero and you will then get a 'tube' rather than a pipe.
If the inside's diameter is greater than the outside's then 
they are swapped.
A branching path returns an error as 'FollowMe' can't decide 
which path to take.
Selected edges that are not joined (i.e. they don't all have 
common ends-starts) will not be extruded - only the first edge 
or joined group of edges will extrude.

Version :
1.0  18/9/5	first release.
1.1  18/9/5	visual segmenting of arced sections etc addressed.
1.2  9/2/6  centrepoints added at vertices of non-looped paths,
			occasional reversed faces on single 'up' line fixed.
1.3  16/2/6 undo of cpoints fixed, diam <8" with matching arc 
			elbow bend radius bug splat trapped (but not curves).
1.4  2/5/6	group name -> OD=xx ID=nn with " or mm as units.
1.5  3/5/6	Transposed ID/OD fixed (sorry!).
1.6 12/5/7  @error=0 ### fix
1.7 20121019 Rehashed to modern standards, dialog based options to add cpoints 
			 at nodes, put selected path onto a specified layer and move the 
			 path inside the pipe-group.
1.8 20130115 Fixed typo glitch when path was lone vertical downward line.
1.9 20140303 Lockup weirdness trapped.  Any units format now allowed.
			 Last used units remebered across sessions and globally.
=end
###
require 'sketchup.rb'
###

module TIG

###
### menu bits ###
unless file_loaded?(__FILE__)###
	UI.menu("Plugins").add_item("Pipe Along Path"){ self.pipe_along_path() }###
end
file_loaded(__FILE__)###
###

def self.pipe_along_path()

model = Sketchup.active_model
entities = model.active_entities
ss = model.selection
layers=model.layers
###
### show VCB and status info
Sketchup::set_status_text("Pipe Along Path... PARAMETERS...", SB_PROMPT)
Sketchup::set_status_text(" ", SB_VCB_LABEL)
Sketchup::set_status_text(" ", SB_VCB_VALUE)
###
if ss.grep(Sketchup::Edge).length<1
  Sketchup::set_status_text("Pipe Along Path... NO SELECTED EDGES ! ", SB_PROMPT)
  UI.messagebox("No Edges Selected to Extrude Pipe.")
  return nil
end
### trap for small radius arcs
aflag=false
arads=[]
for s in ss
  if s.is_a?(Sketchup::Edge)
    if s.curve
      if s.curve.is_a?(Sketchup::ArcCurve)
        if s.curve.radius < 200.mm
          aflag=true
          arads << s.curve.radius
        end
      else
        for e in s.curve.edges
          if e.curve.is_a?(Sketchup::ArcCurve)
            if e.curve.radius < 200.mm
              aflag=true
              arads << e.curve.radius
            end
          end
        end#for
      end
    end
  end
end#for
### ####################################################################
### remembered
@diam_outer = nil
@diam_inner = nil
@segments   = nil
@cpoints    = nil
@cline_layer= nil
@in_group   = nil
###
diam_outer = Sketchup.read_default('PipeAlongPath', 'diam_outer')
diam_inner = Sketchup.read_default('PipeAlongPath', 'diam_inner')
segments   = Sketchup.read_default('PipeAlongPath', 'segments')
cpoints    = Sketchup.read_default('PipeAlongPath', 'cpoints')
cline_layer= Sketchup.read_default('PipeAlongPath', 'cline_layer')
in_group   = Sketchup.read_default('PipeAlongPath', 'in_group')
###
@diam_outer = diam_outer.to_f.inch unless diam_outer==nil
@diam_inner = diam_inner.to_f.inch unless diam_inner==nil
@segments   = segments.to_i unless segments==nil
@cpoints    = cpoints.to_s unless cpoints==nil
@cline_layer= cline_layer.to_s unless cline_layer==nil
@in_group   = in_group.to_s unless in_group==nil
###
### dialog #############################################################
pops = ["","","","Yes|No","","Yes|No"]
###
if model.options["UnitsOptions"]["LengthUnit"]<=1
	@diam_outer = 4.0.inch unless @diam_outer
	@diam_inner = 3.5.inch unless @diam_inner
else #metric
	@diam_outer = 110.0.mm unless @diam_outer
	@diam_inner = 100.0.mm unless @diam_inner
end
###
@segments    = 24 unless @segments
@cpoints     = "Yes" if @cpoints==nil
@cline_layer = "XCLINE" if @cline_layer==nil
@in_group    = "Yes" if @in_group==nil
###
values = [@diam_outer, @diam_inner, @segments, @cpoints, @cline_layer, @in_group]
prompts = ["Outside Diameter: ", "Inside Diameter: ", "Number of Segments: ", "Cpoints at Nodes? ", "Cline Layer Name: ", "Move Path into Pipe Group? "]
###
results = inputbox(prompts, values, pops, "Pipe Parameters")
###
return nil unless results ### i.e. the user cancelled the operation
###
diam_outer, diam_inner, segments, cpoints, cline_layer, in_group = results
###
if diam_outer == 0 && diam_inner == 0 ### can't BOTH be 0 ###
   UI.messagebox("Two Zero Diameters NOT allowed ! ")
   return nil
end
if diam_outer == diam_inner ### can't be same so Inner=0 ###
   diam_inner = 0.0.mm
   UI.messagebox("Equal Diameters NOT allowed !\nInner Diameter is reset to Zero. ")
end

if diam_inner > diam_outer ### Inner can't be bigger ###
   diam_inner_temp = diam_inner
   diam_inner = diam_outer
   diam_outer = diam_inner_temp
   UI.messagebox("Inner Diameter NOT allowed to be bigger than Outer!\nInner is reset to be the smaller value. ")
end
###
if segments < 3
   segments = 24
   UI.messagebox("Fewer than 3 Faces NOT allowed !\nDefaulting to 24. ")
end
###
@diam_outer, @diam_inner, @segments, @cpoints, @cline_layer, @in_group = diam_outer, diam_inner, segments, cpoints, cline_layer, in_group
###
### remember
Sketchup.write_default('PipeAlongPath', 'diam_outer', "#{@diam_outer.to_f.to_s}")
Sketchup.write_default('PipeAlongPath', 'diam_inner', "#{@diam_inner.to_f.to_s}")
Sketchup.write_default('PipeAlongPath', 'segments', "#{@segments.to_s}")
Sketchup.write_default('PipeAlongPath', 'cpoints', @cpoints)
Sketchup.write_default('PipeAlongPath', 'cline_layer', @cline_layer)
Sketchup.write_default('PipeAlongPath', 'in_group', @in_group)
###
###
radius_outer = @diam_outer / 2.0
radius_inner = @diam_inner / 2.0
###
text_outer=@diam_outer.to_s
text_inner=@diam_inner.to_s
###
Sketchup::set_status_text("Pipe Along Path... MAKING PIPE...", SB_PROMPT)
begin
	model.start_operation("Pipe Along Path", true)
rescue
	model.start_operation("Pipe Along Path")
end
@sel=ss.to_a
if @in_group=="Yes"
	group=entities.add_group(@sel)
	@sel=group.entities.to_a
else
	group=entities.add_group()
end
###
def self.get_vertices()
### this next bit is mainly thanks to Rick Wilson's weld.rb ###
	@error=0 ### 20070512
	model=Sketchup.active_model
	ents=model.active_entities
	sl=@sel.length
	verts=[]
	edges=[]
	@new_pts=[]
	startEdge=startVert=nil
#DELETE NON-EDGES, GET THE VERTICES
	edges = @sel.grep(Sketchup::Edge)
	edges.each{|e| verts << e.vertices }
	verts.flatten!
#FIND AN END VERTEX
	vertsShort=[]
	vertsLong=[]
	vertsEnds=[] ### to ensure array is only ends ###
	verts.each{|v|
		if vertsLong.include?(v)
			vertsShort << v
		else
			vertsLong << v
		end
	}
	vertsLong.each{|v| ### to ensure array is only ends ###
		unless vertsShort.include?(v)
			vertsEnds << v
		end
	} ### 17/8/5 ### to ensure array is only ends ###
	if vertsEnds.length==0 ### i.e. it's looped ###
 		### path start or end ?
		if @theEnd==0
		  startVert=vertsLong.first
		  @startVert=@endVert=nil ###1.2
		  startEdge=startVert.edges.first
		else
		  startVert=vertsLong.last
		  @startVert=@endVert=nil ###1.2
		  startEdge=startVert.edges.first
		end
		###
		closed=true
	else
		if vertsEnds.length != 2
			Sketchup::set_status_text("Pipe Along Path... PATH ERROR ! ", SB_PROMPT)
			UI.messagebox("The selected Path either branches or is not continuous ! \nCannot make a Pipe Extrusion ! \nRe-select a single continuous path... ")
			@error=1
			model.abort_operation
			return nil
		else
			### path start or end ?
			if @theEnd==0
			  startVert=vertsEnds.first
		      @startVert=startVert ###1.2
		      @endVert=vertsLong.last ###1.2
			else
			  startVert=vertsEnds.last
  		      @startVert=startVert ###1.2
			  @endVert=vertsLong.first ###1.2
			end
			###
			closed=false
			startEdge=startVert.edges.first
		end
	end
	model.selection.clear
#SORT VERTICES, LIMITING TO THOSE IN THE SELECTION SET
	if startVert==startEdge.start
		@new_pts=[startVert]
		counter=0
		while @new_pts.length < verts.length
			edges.each{|edge|
				if edge.end==@new_pts.last
					@new_pts << edge.start
				elsif edge.start==@new_pts.last
					@new_pts << edge.end
				end
			}
			counter+=1
			if counter > verts.length
				Sketchup::set_status_text("Pipe Along Path... ERROR ! ", SB_PROMPT)
				if UI.messagebox("There seems to be a problem. Try again?", MB_YESNO)!=6
					model.abort_operation
					return nil
				end
				@new_pts.reverse!
				reversed=true
			end
		end
	else
		@new_pts=[startVert]
		counter=0
		while @new_pts.length < verts.length
			edges.each{|edge|
				if edge.end==@new_pts.last
					@new_pts << edge.start
				elsif edge.start==@new_pts.last
					@new_pts << edge.end
				end
			}
			counter+=1
			if counter > verts.length
				Sketchup::set_status_text("Pipe Along Path... ERROR ! ", SB_PROMPT)
				if UI.messagebox("There seems to be a problem. Try again?", MB_YESNO)!=6
					model.abort_operation
					return nil
				end
				@new_pts.reverse!
				reversed=true
			end
		end
	end
	@new_pts.reverse! if reversed
#CONVERT VERTICES TO POINT3Ds >> ARRAY
	@new_pts.collect!{|x| x.position.to_a }
	@new_pts.uniq!
	@new_pts << @new_pts[0] if closed
	@new_pts.reverse! if closed
	###
	@closed = closed
	@edges = edges
	### now have an array of vertices in order with NO forced closed loop ...
end ### get_vertices
	###
	@theEnd=0
	###
	self.get_vertices()
	###
	if @error==1
		model.abort_operation
		return nil
	end
	### NOW - do main stuff 1 ###
	### check for small arcs and == diameter bug-splatters 1.3
	dflag=false
	for r in arads
	  dflag=true if r == radius_outer
	end
	if aflag && dflag ###1.3
	  Sketchup::set_status_text("Pipe Along Path... RADIUS ? ! ", SB_PROMPT)
	  UI.messagebox("The Radius of at least one Arc in the Path is < 200mm(8\") \nand it also matches the Pipe's Outer Radius (#{radius_outer}) ! \nThis might cause SketchUp to Bug-Splat and Crash !! \nHowever it CAN be done this way - \nScale up the Path and Diameters by x10, \nuse this Tool and then Scale down the Path and Pipe by x0.1...\n\nExiting...")
	  model.abort_operation
	  return nil
	end
	###
#=begin	### trap for slightly skewed paths...
	skewed=false
	if @new_pts[2] ### more that one edge
		0.upto(@new_pts.length-2){|i|
			pt1=@new_pts[i]
			pt2=@new_pts[i+1]
			pt2z = pt2.clone
			if pt1.x==pt2.x && pt1.y==pt2.y ### vertical ?
				#pt2z.z = pt1.z
			else
				pt2z.z = pt1.z
			end
			vec  = pt2.vector_to(pt1)
			vecz = pt2z.vector_to(pt1)
			ang = vec.angle_between(vecz)
			#p ang.radians
			#@new_pts[i+1]=pt2z if ang < 0.38.degrees
			if ang < 0.38.degrees
				skewed=true
				break
			end
		}
	end
	if skewed
		Sketchup::set_status_text("Pipe Along Path... SLIGHT SKEW ? ! ", SB_PROMPT)
		UI.beep
		yn=UI.messagebox("Parts of the selected path have a very slight 'skew'.\nThere is a remote chance that it might cause SketchUp to hang when it is doing the FollowMe.\n\nFix the issue?\n\nYes \t=\t Try to readjust the path\nNo \t=\t Use path anyway!\nCancel \t=\t Abort!!\n\nIf it hangs and you haven't just saved, then you will loose work!", MB_YESNOCANCEL )
		if yn==6 #Yes
			0.upto(@new_pts.length-2){|i|
				pt1=@new_pts[i]
				pt2=@new_pts[i+1]
				pt2z = pt2.clone
				if pt1.x==pt2.x && pt1.y==pt2.y ### vertical ?
					#pt2z.z = pt1.z
				else
					pt2z.z = pt1.z
				end
				vec  = pt2.vector_to(pt1)
				vecz = pt2z.vector_to(pt1)
				ang = vec.angle_between(vecz)
				#p ang.radians
				@new_pts[i+1]=pt2z if ang < 0.38.degrees
			}
		elsif yn==7 #No
			### continue !
		else ### 2 #Cancel
			model.abort_operation
			return nil
		end
	end
#=end	###
	pt1 = @new_pts[0]
	pt2 = @new_pts[1]
	vec = pt2.vector_to(pt1)
	###
	ents=group.entities
	unless radius_inner == 0 ###1.1
	  circle_inner = ents.add_circle(pt1, vec, radius_inner, segments)
	  face_inner   = ents.add_face(circle_inner)
	end
	circle_outer = ents.add_circle(pt1, vec, radius_outer, segments)
	the_face     = ents.add_face(circle_outer)
	unless radius_inner == 0 ###1.1
	   face_inner.erase!
	end
	###
	### stop inside-out extrusion
	the_face.reverse! if pt1.z==0 && vec.normalize==Z_AXIS
	###
	ants = model.active_entities ###1.1
	the_edges = []
	0.upto(@new_pts.length-2){|i|
		if @in_group=="Yes"
			the_edges[i] = ents.add_line(@new_pts[i], @new_pts[i+1])
		else
			the_edges[i] = ants.add_line(@new_pts[i], @new_pts[i+1])
		end
	}

	### follow me along ordered edges
	extrusion = the_face.followme(the_edges) ###
	###
	### add name
	group.name="OD=#{text_outer} ID=#{text_inner}"
	unless @cline_layer.empty?
		xlayer=layers.add(@cline_layer)
		@edges.each{|e| next unless e.valid?; e.layer=xlayer }
	end
	###
	model.selection.clear
	if @in_group=="No"
		model.selection.add(@edges)
	end
	###
	model.commit_operation
	###
	if @cpoints=="Yes"
		begin
			model.start_operation("Pipe's Cpoints", true)
		rescue
			model.start_operation("Pipe's Cpoints")
		end
		@new_pts.each{|pt| ents.add_cpoint(pt) }
		model.commit_operation ###1.3
	end#if
	###
end ### end def ###

end#module