=begin
TIG (c) 2013 - 2015  
###  
All rights reserved.  
###  
TIG-Fracture.rb  
###  
Installation:  
Place this RB file in the Plugins folder and restart Sketchup.  
Or even easier, install from the RBZ.  
###  
Usage: Plugins > TIG-Fracture  
or with a suitable selection use the Context-menu > TIG-Fracture  
For Pro >=v.8 only...  
Preselect a 'solid' object [group or component_instance].  
Activate the tool.  
In the dialog:  

Enter the number of "Iterations" - 1 to 10.  
Default = 4 [~16 cuts, ~32 parts]  
It must be at least 1 [~2 cuts, ~4 parts], 8 is slow [~128 cuts, ~256 parts] etc!  
It can't be more than 10 - otherwise SketchUp will die under thousands of bits!  

Enter the "Size" - 0.01 to 0.5.  
Default = 0.25  
This is the minimum size of the smaller piece of each cut part 25:75 -   
at 0.5 all parts have ~equal overall dims, smaller % gives more variety of sizes.  

Enter the "X Variance" - 0.0 to 1.0, which constrains splitting around X axis.  
Default = 1.0  

Enter the "Y Variance" - 0.0 to 1.0, which constrains splitting around Y axis.  
Default = 1.0  

Enter the "Z Variance" - 0.0 to 1.0, which constrains splitting around Z axis.  
Default = 1.0  

If all three variances are set to 1.0 the 3d-splitting is entirely random.  
If variances nearer 0.0, the 'flatter' the split-plane is in that axis.  
Setting all variances to 0.0 produces all cuts parallel to XYZ.  

Enter "Jitter" - 0.0 to 1.0  
Default = 0.5  

This sets how much the cuts might be skewed from the XYZ axes.  
Values nearer 0.0 give less skewing.  
Using 1.0 makes it entirely random.  
If the XYZ Variances are all 0.0 then "Jitter" is ignored.  

Choose the "Form" - Stacked|Exploded|Scaled  
Stacked  == Parts stay stacked in place. [Default]  
Exploded == Parts are exploded away from the center.  
Scaled   == Parts are scaled to ~90% to leave gaps between them.  

The original solid is 'fractured' into randmomly 'split' pieces.  
Each is named 'GroupName-FracN-00C', where N is the number of iterations, and   
00C is the 'counter' of the split part.  
 
The 'fracturing' is one step undo-able,  
with an initial separate undo for the explode/scale [if done].  

It is faster/safer if the Outliner window is rolled-up [closed], the Shadows   
are off, no Textures, the Style is simple etc.  
###  
Version:  
1.0 20130213 First issue.  
2.0 20150426 Full issue, including Size % and Explode options.    
3.0 20150429 Undo limited to whole fracture and any Exploded/Scaled, 
			 which are now incorporated in dialog.  
			 Constraints on XYZ axes included.  
4.0 20150430 Now recast as XYZ 'Variance', at 0 now makes orthogonal cuts.  
			 Skewing 'Jitter' option added.  'Size' now in range 0.0 to 1.0.  
=end
###
require('sketchup.rb')
###
module TIG
	module Fracture
		### menu
		unless file_loaded?(__FILE__)
			UI.menu("Plugins").add_item("TIG-Fracture"){ self.new() }
			UI.add_context_menu_handler{|menu|
				ss = Sketchup.active_model.selection
				if ss.length == 1 && ( ss[0].is_a?(Sketchup::Group) || ss[0].is_a?(Sketchup::ComponentInstance) ) && ss[0].manifold?
					menu.add_item("TIG-Fracture"){ self.new() }
				end
			}
			file_loaded(__FILE__)
		end#if loaded
		###
		@@its = 4
		@@per = 0.25
		@@xan = 1.0
		@@yan = 1.0
		@@zan = 1.0
		@@jit = 0.5
		@@for = "Stacked"
		###
		def self.new()
			unless Sketchup.is_pro? && Sketchup.version.to_i >= 8
				UI.messagebox("TIG-Fracture.rb\n\nOnly works with SketchUp Pro, >= v8")
				Sketchup.send_action("selectSelectionTool:")
				return nil
			end
			@model = Sketchup.active_model
			@ents = @model.active_entities
			@view = @model.active_view
			@ss = @model.selection
			unless selection_solid?()
				UI.messagebox("TIG-Fracture:\n\nOnly works with Selection of ONE Solid Group/Component-Instance.")
				Sketchup.send_action("selectSelectionTool:")
				return nil
			end
			Sketchup::set_status_text("TIG-Fracture: Number of Iterations", SB_PROMPT)
			return nil unless dialog()
			@msg="TIG-Fracture: Fracturing"
			Sketchup::set_status_text(@msg, SB_PROMPT)
			###
			@bb = @gp.bounds
			###
			@lok = @gp.locked?
			@hid = @gp.hidden?
			@shc = @gp.casts_shadows?
			@shr = @gp.receives_shadows?
			if @gp.is_a?(Sketchup::ComponentInstance)
				@nam = @gp.definition.name
				@nam = @nam+"["+@gp.name+"]" unless @gp.name.empty?
			else ### it's a Group
				@nam = @gp.name
				@nam = "Group" if @nam.empty? ### might be "" !
			end
			@mat = @gp.material
			@lay = @gp.layer
			@ads = @gp.attribute_dictionaries
			###
			@model.start_operation("TIG::Fracture", true)
			if @gp.is_a?(Sketchup::ComponentInstance)
				tgroup = @ents.add_group(@gp)
				@gp.explode
				tgroup.locked = @lok
				tgroup.hidden = @hid
				tgroup.casts_shadows = @shc
				tgroup.receives_shadows = @shr
				tgroup.name = @nam
				tgroup.material = @mat
				tgroup.layer = @lay
				@ads.each{|ad|
					na = ad.name
					ad.each_pair{|k,v|
						tgroup.set_attribute(na, k, v)
					}
				} if @ads
				@gp = tgroup
			end
			@tgroup = @ents.add_group(@gp)
			@tents = @tgroup.entities
			###
			@@its.times{|i|
				@gps = @tents.grep(Sketchup::Group)
				self.fracture(i+1)
			}
			###
			@xents = @tents.grep(Sketchup::Group)
			@tgroup.explode
			###
			@model.commit_operation
			###
			case @@for
				when "Exploded"
					@model.start_operation("TIG::Fracture Exploded", true)
					self.exploder()
					@model.commit_operation
				when "Scaled"
					@model.start_operation("TIG::Fracture Scaled", true)
					self.scaler()
					@model.commit_operation
				else
					###
			end#case
			###
			Sketchup::set_status_text("TIG-Fracture: Done.")
			UI.messagebox("TIG-Fracture: Done.")
			Sketchup.send_action("selectSelectionTool:")
			return nil
			###
		end
		###
		def self.exploder()
			ct = @bb.center
			@xents.grep(Sketchup::Group).each{|gp|
				ve = ct.vector_to(gp.bounds.center)
				tr = Geom::Transformation.translation(ve)
				gp.transform!(tr)
			}
		end
		###
		def self.scaler()
			@xents.grep(Sketchup::Group).each{|gp|
				ct = gp.bounds.center
				tr = Geom::Transformation.scaling(ct, 0.9, 0.9, 0.9)
				gp.transform!(tr)
			}
		end
		###
		def self.dialog()
			results = inputbox(["Iterations: ", "Size: ", "X Variance: ", "Y Variance: ", "Z Variance: ", "Jitter: ", "Form ? "], [@@its, @@per, @@xan, @@yan, @@zan, @@jit, @@for], ["", "", "", "", "", "", "Stacked|Exploded|Scaled"], "TIG-Fracture")
			return nil unless results
			@@its, @@per, @@xan, @@yan, @@zan, @@jit, @@for = results
			if @@its < 1
				@@its = 1
				UI.messagebox("TIG-Fracture:\n\nMust have at least ONE Iteration.")
			elsif @@its > 10
				@@its = 10
				UI.messagebox("TIG-Fracture:\n\nShould be no more than TEN Iterations.")
			end
			if @@per < 0.01
				@@per = 0.01
				UI.messagebox("TIG-Fracture:\n\nMust be >= 0.01")
			elsif @@per > 0.5
				@@per = 0.5
				UI.messagebox("TIG-Fracture:\n\nMust be <= 0.5")
			end
			if @@xan < 0.0
				@@xan = 0.0
				UI.messagebox("TIG-Fracture:\n\nX Variance must be >= 0.0")
			elsif @@xan > 1.0
				@@xan = 1.0
				UI.messagebox("TIG-Fracture:\n\nX Variance must be <= 1.0")
			end
			if @@yan < 0.0
				@@yan = 0.0
				UI.messagebox("TIG-Fracture:\n\nY Variance must be >= 0.0")
			elsif @@yan > 1.0
				@@yan = 1.0
				UI.messagebox("TIG-Fracture:\n\nY Variance must be <= 1.0")
			end
			if @@zan < 0.0
				@@zan = 0.0
				UI.messagebox("TIG-Fracture:\n\nZ Variance must be >= 0.0")
			elsif @@zan > 1.0
				@@zan = 1.0
				UI.messagebox("TIG-Fracture:\n\nZ Variance must be <= 1.0")
			end
			if @@jit < 0.0
				@@jit = 0.0
				UI.messagebox("TIG-Fracture:\n\nJitter must be >= 0.0")
			elsif @@jit > 1.0
				@@jit = 1.0
				UI.messagebox("TIG-Fracture:\n\nJitter must be <= 1.0")
			end
			return true
		end
		###
		def self.selection_solid?()
			return nil unless @ss && @ss[0]
			return nil if @ss[1]
			@gp = @ss[0]
			return nil unless @gp.is_a?(Sketchup::Group) || @gp.is_a?(Sketchup::ComponentInstance)
			return nil unless @gp.manifold?
			return true
		end
		###
		def self.manifolder(gp)
			gp.entities.grep(Sketchup::Edge).select{|e| e.faces.length != 2 }.each{|e|
				unless e.faces[0]
					e.erase!
					next
				end
				e.find_faces unless e.faces[1]
			}
			gp = nil unless gp.valid?
			gp.entities.grep(Sketchup::Edge).select{|e| e.faces.length == 1 }.each{|e|
				e.erase!
			} if gp
			gp = nil unless gp.valid?
			return gp
		end
		###
		def self.fracture(its=0)
			###
			ctr = "001"
			###
			@gps.dup.each{|gp|
				next unless gp && gp.valid?
				gp = self.manifolder(gp) unless gp.manifold?
				next unless gp
				begin
					Sketchup::set_status_text("#{@msg} #{its} of #{@@its} - Step #{ctr}", SB_PROMPT)
					defn = gp.entities.parent
					tr = gp.transformation
					gp1 = @tents.add_instance(defn, tr)
					gp1.name = gp.name
					gp1.make_unique
					### generate random center
					bb = gp.bounds
					di = bb.diagonal
					ct = bb.center
					mi = bb.min
					ma = bb.max
					ve = mi.vector_to(ma)
					pn = mi.offset(ve, @@per*di/100)
					pc = ct.clone
					###
					pp = rand(1)
					ppp = 1-pp
					pt = Geom::linear_combination(pp, pn, ppp, pc)
					###
					tgp = @tents.add_group()
					tents = tgp.entities
					es = tents.add_circle(pt, X_AXIS, di, 6)
					fa = tents.add_face(es)
					fa.reverse! if fa.bounds.center.z == 0 && fa.normal.parallel?(Z_AXIS)
					###
					fa.pushpull(di)
					###
					tgp1 = @tents.add_group()
					tents1 = tgp1.entities
					es1 = tents1.add_circle(pt, X_AXIS.reverse, di, 6)
					fa1 = tents1.add_face(es1)
					fa1.reverse! if fa1.bounds.center.z == 0 && fa1.normal.parallel?(Z_AXIS)
					###
					fa1.pushpull(di)
					###
					if @@xan == 0.0 &&  @@yan == 0.0 &&  @@zan == 0.0
						seed = rand(3) ### <1 <2 <3
						if seed < 1 ### in Z
							a = 90.0
							t = Geom::Transformation.rotation(pt, Y_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
						elsif seed < 2 ### in Y
							a = 90.0
							t = Geom::Transformation.rotation(pt, Z_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
						else
							### < 3 - we leave it where it is, in X
						end
					else ### random
						seed = rand(3) ### <1/<2/<3
						if seed < 1 ### in Z
							a = 90.0
							t = Geom::Transformation.rotation(pt, Y_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
							a = rand(90)*@@zan
							t = Geom::Transformation.rotation(pt, Y_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
							a = rand(90)*@@jit
							t = Geom::Transformation.rotation(pt, X_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
						elsif seed < 2 ### in Y
							a = 90.0
							t = Geom::Transformation.rotation(pt, Z_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
							a = rand(90)*@@yan
							t = Geom::Transformation.rotation(pt, X_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
							a = rand(90)*@@jit
							t = Geom::Transformation.rotation(pt, Z_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)						
						else # already in X
							a = rand(90)*@@xan
							t = Geom::Transformation.rotation(pt, Z_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
							a = rand(90)*@@jit
							t = Geom::Transformation.rotation(pt, Y_AXIS, a.degrees)
							tgp.transform!(t)
							tgp1.transform!(t)
						end
					end
					### cut forwards
					ob  = tgp.subtract(gp)
					if ob && ob.valid?
						ob.locked = @lok
						ob.hidden = @hid
						ob.casts_shadows = @shc
						ob.receives_shadows = @shr
						ob.name = "#{@nam}-Frac#{its}-#{ctr}"
						ob.material = @mat
						ob.layer = @lay
						@ads.each{|d|
							name = d.name
							d.each_pair{|k, v| ob.set_attribute(name, k, v) }
						} if @ads
					end
					ctr.next!
					### cut backwards
					ob1 = tgp1.subtract(gp1)
					if ob1 && ob.valid?
						ob1.locked = @lok
						ob1.hidden = @hid
						ob1.casts_shadows = @shc
						ob1.receives_shadows = @shr
						ob1.name = "#{@nam}-Frac#{its}-#{ctr}"
						ob1.material = @mat
						ob1.layer = @lay
						@ads.each{|d|
							name = d.name
							d.each_pair{|k, v| ob1.set_attribute(name, k, v) }
						} if @ads
					end
					
					ctr.next!
					###
					tgp.erase! if tgp && tgp.valid?
					tgp1.erase! if tgp1 && tgp1.valid?
					gp.erase! if gp && gp.valid?
					gp1.erase! if gp1 && gp1.valid?
					###
				rescue
					p 666
					tgp.erase! if tgp && tgp.valid?
					tgp1.erase! if tgp1 && tgp1.valid?
					gp.erase! if gp && gp.valid?
					gp1.erase! if gp1 && gp1.valid?
					###
				end
				@view.refresh
			}
			###
		end
		###
	end#Fracture
end#TIG
