=begin (c) TIG 2010/2011 Permission to use, copy, modify, and distribute this software for any purpose and without fee is hereby granted, provided the above copyright notice appear in all copies. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ___________________________________________________________________________ Script: #HolePunchTool.rb place it in the ../Plugins/ folder... From v1.6 it also requires 'deBabelizer.rb' in the ../Plugins/ folder and any required locale lingvo files in the ../Plugins/TIGtools sub-folder... e.g. #HolePunchToolEN-US.lingvo, #HolePunchToolFR.lingvo etc... ___________________________________________________________________________ Overview: This script adds a new tool for glued/cutting component-instances. It allows them to punch linked 3D holes through the faces of double-skinned walls/roofs etc. It is accessed through the right-click context-menu 'Hole Punching...' sub-menu, which offers various items depending on the current Selection. ___________________________________________________________________________ Usage: 1. TO PUNCH A HOLE: Select a component-instance** that has already been placed on the face of a wall or roof etc - e.g. a newly inserted/imported or created window component. The right-click context-menu 'Hole Punching...' sub-menu should then include 'Punch' as an option - pick it. Only selected component-instances of a definition with a gluing/cutting capability will be processed. In addition to the externally 'cut' 2D hole, the instance will now punch a hole through to any inner-face of that 'wall' - using the inner-face's material/layer etc for the reveals, or if there is no inner-face it will make a maximum depth reveal using the outer-faces' material/layer etc for the reveals [Note that there is a 'Set Depth' option - see below]. The component-instance and the punched-hole's reveals' geometry are given 'HolePunching' attributes that link them together so they will then 'transform' as one. **Several component-instances can be selected and processed in one go - however, if any of the selected component-instances are already 'punched' [and therefore have 'HolePunching' attributes] then the 'Punch' option will not be available - only 'Undo Punch' will be shown - see below... 2. TO UNDO A PUNCHED HOLE: Select a component-instance** that has been placed on a face that is 'punched' [i.e. it has 'HolePunching' attributes]. The right-click context-menu 'Hole Punching...' sub-menu should then include 'Undo Punch' as an option - pick it. The selected component-instance will no longer punch a 3D hole through the whole 'wall' - it will only make the 2D 'cut' hole in its front 'parent' face as any normal 'cutting' component. The punched-hole's reveals' geometry will be completely removed and the inner face healed. That component-instance will also have its 'HolePunching' attributes removed. **Several 'punching' component-instances can be selected and 'undone' in one go. 3. TO UNLINK A COMPONENT-INSTANCE AND ITS PUNCHED HOLE: If you Select a 'Hole Punching' component-instance then its punched-hole's reveals will auto-select too, so that they will always 'transform' together - e.g. they will Move as one item. If you Select any geometry forming a punched-hole all of its geometry will auto-select, but the linked component-instance will not be auto-selected. You can 'unlink' a component-instance and its punched-hole. Select a component-instance** that has been placed on a face that is 'punched' [i.e. it has 'HolePunching' attributes]. The right-click context-menu 'Hole Punching...' sub-menu should then include 'Unlink Punch' as an option - pick it. The component-instance and its punched-hole's geometry are now 'unlinked' and their 'HolePunching' attributes are removed. This 'unlinking' is permanent - so use it with care. If later on you should want to 'Punch' the component-instance again you can, BUT the earlier punched-hole's geometry should perhaps be deleted or relocated, OR the component-instance relocated on the face to suit, to avoid clashes. So for example the now 'unlinked' component-instance can be Moved, but the now 'unlinked' punched-hole will stay as where is; or you can Select+Erase the component-instance and the punched-hole will remain unselected and therefore unchanged. Also the punched-hole geometry is no longer linked together and individual parts of it can now be edited as with any other 'hole' formed manually in faces. **Several 'punching' component-instances can be selected and 'unlinked' in one go. 4. TO ERASE A PUNCHED HOLE: If you Select the punching component-instance its punched-hole's reveals will also be auto-selected. If you were then to press 'delete' the punching component-instance and all of its punched-hole's reveals will be removed. Alternatively, if you Select any part of a punched-hole's reveals then all of the reveals' geometry is selected. If you were then to press 'delete' all of that reveals' geometry will be removed; however, the punching component-instance is kept - in this case the punching component-instance automatically looses its 'HolePunching' attributes as it has no 'hole' remaining. Therefore the right-click context-menu 'Hole Punching...' sub-menu should then include 'Punch' as its option. If you use the Erase tool to remove a punching component-instance then the punched-hole and its reveals is NOT removed. If you use the Erase tool [perhaps by accident] to remove only some of the punched-hole's reveals' geometry then the punching component-instance will still have its 'HolePunching' attributes linked to the punched-hole's remaining geometry, and in that case the right-click context-menu 'Hole Punching...' sub-menu will still include 'Undo Punch' as an option. You can pick that to 'tidy up' and remove the punched-hole's remaining geometry. You can always 'Punch' the component-instance afterwards to 'redo' the punched-hole properly, as by then the component-instance will not have 'HolePunching' attributes. Note that you can also 'Unlink' the component-instance and its punched-hole - see above... 5. TO SWAP A PUNCHING COMPONENT-INSTANCE'S COMPONENT-DEFINITION FOR A DIFFERENT COMPONENT-DEFINITION: You can swap a hole-punching component-instance's component-definition for a different component-definition using the tools built into the Component-Browser ["Replace Selected" etc]. This replacement component-definition might not cut a hole that is the same shape or size as the component-definition of the original component-instance. Because the component-instance keeps its links to the reveals' geometry you can correctly this quite easily - use the 'Redo Punch' tool [or alternatively use 'Undo Punch' to remove the incorrectly formed holes/reveals, then immediately use 'Punch'], to re-punch the correct sized holes/reveals to suit the replacement component-definition's form. 6. TO SET THE MAXIMUM PUNCH DEPTH: By default the 'Punch' will only operate on any inner-face found behind the wall's outer-face, and that in parallel it, AND that faces in the opposite direction, AND that is within 20" [500mm] of it - which is a reasonable maximum wall thickness to expect in most buildings. However, you can adjust this maximum reveal depth at anytime - and its value is remembered with the SKP model. To change this use the right-click context-menu 'Hole Punching...' sub-menu 'Set Depth'. This item might be shown on its own if there are no suitable component-instances selected OR be displayed after 'Punch' or 'Undo Punch/Unlink Punch' depending on what is selected. A simple dialog opens showing the current depth. Enter a new maximum depth for the punched-hole's reveals, in the model's current units. Click OK to save the currently entered value, or Cancel to leave it as it was originally shown. The depth must be more that zero [0] and it should be carefully considered... because making it too large could result in the inner hole/reveals being formed with a face across the room !!! However, if there is a suitable inner-face within the current maximum depth then the punched-hole will be made to suit the actual distance between the two faces. Otherwise if there is no suitable inner-face within the maximum depth [e.g. it's a single skin wall, or the wall is very thick] then the punched-hole will be made using the maximum depth. 7. TRANSFORMATIONS: The punched-holes/reveals geometry is linked to the punching component-instance with 'HolePunching' attributes and an 'observer'; therefore they will always 'Select' together. Therefore the punched-hole/reveals geometry will Move, Rotate, Scale etc with the punching component-instance as if they were one item. The punched-holes' perimeter Edges are also linked to the two faces by virtue of them being coplanar: so if either of the wall's two faces are moved relative to each other and thereby the wall's 'thickness' changes then the punched-hole's reveals will be automatically adjusted to suit, always filling the distance between the two faces. 8. COPYING: With the newer version of this Tool you can now 'Copy' a punching component-instance [and its auto-selected punched-hole's geometry] using Move+Ctrl or Rotate+Ctrl [with xN or /N to 'Array' them if desired]. Each instance 'copy' will ultimately be 'made-unique' with its own linked punched-hole's geometry. Note that while you continue using the Move Tool or Rotate Tool these copies will remain 'unbaked', so you must change Tool to another Tool - say the Select Tool - to complete these punched copies to re-glue/un-punch/re-punch them appropriately... To undo this copying you will need to do two 'Undoes' - the first to undo the 'fix-duplicates' and the second to undo the Copy/Array itself that was done using the external tool. Do not leave the 'half-baked' copies after one undo, as the hole-punching will be 'cross-threaded' between instances and the new copies will not be glued to the face etc. Note that this method of copying does mean that any 'tweaking' that might have been done to the original's reveals - e.g. splaying the sides - will be lost because ALL instances including the original one will need to be re-glued/un-punched/re-punched to resolve any copying conflicts and make them all unique. If you are making many copies by 'Array' then it will be quicker to do this using a non-punched original component, then afterwards select it and all arrayed copies, and punch them en mass; rather that selecting a punched instance, arraying it etc, since all of them will then need re-gluing/un-punching/re-punching - which is over three times as many operations - so there will be a noticeable lag before the lot get made-unique... Remember that you can always quickly un-punch the original before copying and re-punch it with the rest after arraying... If you do have customized reveals that you wish to retain then you need to place new instances, punch them and then 'tweak' each of their reveals as desired. Also note that any punching-components within groups etc that are themselves copied and exploded together are also caught and will be 'made-unique'. Note that using Edit>Copy>Paste with 'punching-components' might sometimes give unreliable results [depending on their geometry/origin], so be warned! It's recommended that you use Move+Ctrl for copying 'punching-components'... 9. REDO PUNCH: The new version includes a 'Redo Punch' context-menu option. It is useful if you have edited a component's shape so that the linked punched-hole's geometry is no longer the same shape as the punching-instance itself, or you have swapped an instance's definition for one with a different form. This tool allows you to Select affected punching-instances and 'redo' their hole punching to match their current form. It is an alternative to using 'Undo Punch' and then 'Punch' on such instances. 10. REGLUE: The new version includes a 'Reglue' context-menu option. If you have erased a face that contained standard 'hole-cutting' instances you might have noticed that they are NOT automatically reglued to any new face you might try to reform to replace the original erased one. This 'ungluing' will also occur with a 'hole-punching' instance made with these tools so that a full hole will no longer be punched through both wall faces. This 'Reglue' tool can be used to reglue any such wayward instances. Note that it works for both instances of standard 'hole-cutting' components AND this tool's own 'hole-punching' instances. You simply Select the affected instances [you don't need to be too careful selecting as 'non-gluing' objects are ignored anyway] and then run the 'Reglue' tool from the context-menu - they will each glue to the face that they are currently placed on and 'cut' the appropriate hole in that face: with any hole-punching ones the full depth punched hole will also reappear as before... 11. UNDO: All Hole-Punching operations are 'one step' undoable. However, note that when copying punching component-instances the first 'undo' will undo the tool's 'fix-duplicates' operation but a second 'undo' is needed to undo the actual copying itself. 12. ACROSS-SESSION ATTRIBUTES: The Hole Punching 'maximum depth' is remembered across sessions, saved within each model. The connectivity between each punching component-instance and its punched-hole's geometry is remembered across sessions, using their linking 'HolePunching' attributes and 'observers'. Provided that this script loads at startup, setting the 'observers', then they will continue to auto-select as one and therefore transform as one etc. ___________________________________________________________________________ Donations: by PayPal to info @ revitrev.org ___________________________________________________________________________ Version: 1.0 20100914 First Release. 1.1 20100914 Components exactly matching wall-depth glitch fixed. Zero depth in dialog trapped. 1.2 20100915 Finding correct inner-face methods improved. 'Unlink' option added. 1.3 20101002 New AppObserver added to keep hole_punch observers firing when model is 'from menu' as 'open' or 'new'. 1.4 20101229 Copied/Arrayed punching-instances are now 'made-unique'. 1.5 20101230 'Reglue' and 'Redo Punch' options added. 1.6 20101231 DeBabelizer/lingvo-file language translation options added for EN-US, DE, FR, ES etc. 'Reglue' only appears if selection contains potential gluing instances. ___________________________________________________________________________ =end ######################## require 'sketchup.rb' require 'deBabelizer.rb' ######################## class HolePunchTool ########################################################### def db(string) dir=File.dirname(__FILE__)+"/TIGtools" toolname="#HolePunchTool" locale=Sketchup.get_locale.upcase path=dir+"/"+toolname+locale+".lingvo" if not File.exist?(path) return string else deBabelizer(string,path) end end#def def HolePunchTool::db(string) dir=File.dirname(__FILE__)+"/TIGtools" toolname="#HolePunchTool" locale=Sketchup.get_locale.upcase path=dir+"/"+toolname+locale+".lingvo" if not File.exist?(path) return string else deBabelizer(string,path) end end#def def initialize(inst=nil) @inst=inst @model=Sketchup.active_model @selection=@model.selection punching_components=[] if not @inst ################################# if @selection.empty? UI.messagebox(db("No Selection. Select Punching Component(s).")+"\n"+db("Exiting.")) return nil end#if ############ @selection.to_a.each{|e| if e.valid? and e.class==Sketchup::ComponentInstance and e.definition.behavior.is2d? and e.definition.behavior.cuts_opening? if not face=e.glued_to face=HolePunchTool.reglue(e) end#if next if not face edge_loop=self.get_outline(e) punching_components << [e, face, edge_loop] if edge_loop end#if } @selection.clear if not punching_components[0] UI.messagebox(db("No Suitable Punching Component(s) Selected.")+"\n"+db("Exiting.")) return nil end ### elsif @inst and @inst.valid? and @inst.class==Sketchup::ComponentInstance and @inst.definition.behavior.is2d? and @inst.definition.behavior.cuts_opening? and face=@inst.glued_to and face.valid? edge_loop=self.get_outline(@inst) punching_components << [@inst, face, edge_loop] if edge_loop return nil if not punching_components[0] end#if ### set/set attrib @depth=@model.get_attribute("HolePunching","depth",false) ### get units if @model.options["UnitsOptions"]["LengthUnit"]<=1 ### it's '/" @depth=20.inch if not @depth else ### it's metric @depth=500.mm if not @depth end#if ### @model.set_attribute("HolePunching","depth",@depth) ### @model.start_operation(db("Hole Punching")) if not @inst punching_components.each{|pc|self.punch_component(pc)} @model.commit_operation if not @inst ################# return nil ################# end#end 'new' def get_outline(punching_component) the_array=[] components_edges=[] punching_component.definition.entities.each{|e| components_edges << e if e.class==Sketchup::Edge and e.start.position.z==0 and e.end.position.z==0 } if components_edges[0] start_edges=Array::new(components_edges.length,-1) end_edges=Array::new(components_edges.length,-1) components_edges.length.times{|i| start_vertex=components_edges[i].start end_vertex_point=components_edges[i].end components_edges.length.times{|j| if i != j start_edges[i]=j if components_edges[j].used_by?(start_vertex) end_edges[i]=j if components_edges[j].used_by?(end_vertex_point) end#if } } edges=[] components_edges.length.times{|i|edges << components_edges[i] if start_edges[i] != -1 and end_edges[i] != -1} vertex_points=[] edges.length.times{|i|vertex_points << [edges[i].start.position, edges[i].end.position]} vertex_points_loops=[] current_vertex_point=vertex_points[0] end_vertex_point=current_vertex_point[1] vertex_point=[current_vertex_point[0]] vertex_points.length.times{|i| vert_points=[] vertex_points.each{|e| vert_points << e if e[0]==end_vertex_point or e[1]==end_vertex_point } vertex_points.delete(current_vertex_point) if vert_points.member?(current_vertex_point) if vert_points.length < 2 vertex_points_loops << vertex_point if vertex_points[0] current_vertex_point=vertex_points[0] end_vertex_point=current_vertex_point[1] vertex_point=[current_vertex_point[0]] end#if else vertex_point << end_vertex_point vert_points.delete(current_vertex_point) if vert_points.member?(current_vertex_point) current_vertex_point=vert_points[0] current_vertex_point.each{|e| if e.distance(end_vertex_point) != 0 end_vertex_point=e break end#if } end#if } # true == point on border is inside if vertex_points_loops[1] vertex_points_loops.length.times{|i| vertex_points_loop=vertex_points_loops[i] currently_inside=false vertex_points_loops.length.times{|j| if i != j test_loop=vertex_points_loops[j] inside=true vertex_points_loop.each{|vp|inside=false if not Geom.point_in_polygon_2D(vp, test_loop, true)} currently_inside=inside if inside end#if } the_array << vertex_points_loop if not currently_inside } else the_array=vertex_points_loops.clone end#if end#if component_edges[0] return the_array end#end get_outline def punch_component(carray) compi=carray[0] iface=carray[1] loops=carray[2] return nil if not compi or not compi.valid? return nil if not iface or not iface.valid? return nil if not loops[0] return nil if not compi.parent == iface.parent ifacenormal=iface.normal ifacematerial=iface.material ifaceback_material=iface.back_material ifacelayer=iface.layer ifaceplane=iface.plane pents=iface.parent.entities ### loops.each{|pvs| tr=compi.transformation pvs.length.times{|i|pvs[i].transform!(tr)} new_face=pents.add_face(pvs) ### trapped for compo same deth as wall etc ### inner-face must overlap compi's center point cent=compi.definition.bounds.center cent.z=0.0 cent.transform!(tr) ### if new_face new_face.reverse! if new_face.normal != ifacenormal new_face_edges=new_face.edges vec=ifacenormal.reverse inner_face=nil ### get all faces with vec normal faces=[] pents.to_a.each{|e|faces << e if e.class==Sketchup::Face and e.normal==vec} ### remove face 'level' OR if too far away OR on wrong side of outer-face. ### AND point is NOT on face cfaces=[]+faces faces.each{|face| pt=Geom.intersect_line_plane([cent,vec],face.plane) if not pt or face.plane==ifaceplane cfaces.delete(face) elsif pt fcp=face.classify_point(pt) if fcp==Sketchup::Face::PointOutside or fcp==Sketchup::Face::PointUnknown or fcp==Sketchup::Face::PointNotOnPlane cfaces.delete(face) end#if end#if if pt and cent.distance(pt) > @depth cfaces.delete(face) end#if } faces=cfaces ### now take nearest face in remaining faces depth=@depth faces.each{|face| pt=Geom.intersect_line_plane([cent,vec],face.plane) dist=cent.distance(pt) if dist <= depth inner_face=face depth=dist end#if } ### if not inner_face depth=@depth end#if ### entsa=pents.to_a ############################################## new_face.pushpull(-depth, false) if depth != 0 ############################################## entsaNew=pents.to_a - entsa new_faces=[] entsaNew.each{|e|new_faces << e if e.valid? and e.class==Sketchup::Face} if inner_face and inner_face.valid? new_faces.each{|e| next if not e.valid? e.material=inner_face.material e.back_material=inner_face.back_material e.layer=inner_face.layer }### match inner face else### match outer face new_faces.each{|e| next if not e.valid? e.erase! if e.normal==ifacenormal and e != iface next if not e.valid? e.material=ifacematerial e.back_material=ifaceback_material e.layer=ifacelayer } ### remove blank 'end' end#if ### now add attributes id=Time.now.to_i.to_s+rand.to_s ### 1 to component-instance [compi] @selection.add(compi) if not @inst self.add_attribute(compi,"inst",true)if compi and compi.valid? self.add_attribute(compi,"id",id)if compi and compi.valid? ### 2 to geometry reveal_geom=[]+new_face_edges entsaNew.each{|e|reveal_geom << e if e.valid? and not reveal_geom.include?(e)} reveal_geom.each{|e| next if not e.valid? self.add_attribute(e,"geom",true) self.add_attribute(e,"id",id) @selection.add(e) if not @inst } end#if new_face }#loops return nil end#end punch_component def add_attribute(ent,k,v) ent.set_attribute("HolePunching",k,v) end#end add_attribute ######################### def HolePunchTool::status status=0 model=Sketchup.active_model model.selection.to_a.each{|e| if e.class==Sketchup::ComponentInstance and e.definition.behavior.is2d? and e.definition.behavior.cuts_opening? and not e.get_attribute("HolePunching","inst",false) status=1 ### has suitable compoi to punch end#if if e.class==Sketchup::ComponentInstance and e.definition.behavior.is2d? and e.definition.behavior.cuts_opening? and e.get_attribute("HolePunching","inst",false) status=2 ### has pre-punched compoi - so >>> UNDO or UNLINK etc end#if } ### we have a suitable compo... return status end#status def HolePunchTool::undo_punch(inst=nil) model=Sketchup.active_model selection=model.selection if not inst model.start_operation(HolePunchTool.db("Undo Hole Punching")) sela=selection.to_a selection.clear sela.each{|e| if e.class==Sketchup::ComponentInstance and id=e.get_attribute("HolePunching","id",false) e.set_attribute("HolePunching","inst",false) e.set_attribute("HolePunching","id",false) model.active_entities.to_a.each{|ee| next if not ee.valid? next if not ee.class==Sketchup::Face and not ee.class==Sketchup::Edge ee.erase! if ee.valid? if id==ee.get_attribute("HolePunching","id",false) } end#if } sela.each{|e|selection.add(e)if e.valid?} model.commit_operation ####################### elsif inst and inst.valid? and inst.class==Sketchup::ComponentInstance and id=inst.get_attribute("HolePunching","id",false) return nil if not inst or not inst.valid? or not parent=inst.parent inst.set_attribute("HolePunching","inst",false) inst.set_attribute("HolePunching","id",false) parent.entities.to_a.each{|ee| next if not ee.valid? next if not ee.class==Sketchup::Face and not ee.class==Sketchup::Edge ee.erase! if ee.valid? if id==ee.get_attribute("HolePunching","id",false) } end#if ### end#undo_punch def HolePunchTool::unlink_punch(inst=nil) model=Sketchup.active_model selection=model.selection if not inst model.start_operation(HolePunchTool.db("Unlink Hole Punching")) sela=selection.to_a selection.clear sela.each{|e| next if not e.valid? if e.class==Sketchup::ComponentInstance and e.get_attribute("HolePunching","inst",false) and id=e.get_attribute("HolePunching","id",false) e.set_attribute("HolePunching","inst",false) e.set_attribute("HolePunching","id",false) model.active_entities.to_a.each{|ee| next if not ee.valid? next if not ee.class==Sketchup::Face and not ee.class==Sketchup::Edge if id==ee.get_attribute("HolePunching","id",false) ee.set_attribute("HolePunching","id",false) ee.set_attribute("HolePunching","geom",false) end#if } end#if } sela.each{|e|selection.add(e)if e.valid?} model.commit_operation else if inst.valid? and inst.class==Sketchup::ComponentInstance and inst.get_attribute("HolePunching","inst",false) and id=inst.get_attribute("HolePunching","id",false) return nil if not inst or not inst.valid? or not parent=inst.parent inst.set_attribute("HolePunching","inst",false) inst.set_attribute("HolePunching","id",false) parent.entities.to_a.each{|ee| next if not ee.valid? next if not ee.class==Sketchup::Face and not ee.class==Sketchup::Edge if id==ee.get_attribute("HolePunching","id",false) ee.set_attribute("HolePunching","id",false) ee.set_attribute("HolePunching","geom",false) end#if } end#if end#if end#undo_punch def HolePunchTool::set_depth() model=Sketchup.active_model depth=model.get_attribute("HolePunching","depth",false) ### get units if model.options["UnitsOptions"]["LengthUnit"]<=1 ### it's '/" depth=20.inch if not depth else ### it's metric depth=500.mm if not depth end#if ### prompt=HolePunchTool.db("Max.Depth: ") title=HolePunchTool.db("Hole Punching") result=UI.inputbox([prompt], [depth.to_l], title) return nil if not result if result[0] <= 0 UI.messagebox(HolePunchTool.db("Depth must be > 0")+"\n"+HolePunchTool.db("Try Again...")) HolePunchTool::set_depth() return nil end#if depth=result[0] model.set_attribute("HolePunching","depth",depth) end#set_depth def HolePunchTool::fix_duplicates(inst) return nil if not inst or not inst.valid? or not inst.definition model=Sketchup.active_model selection=model.selection id=inst.get_attribute("HolePunching","id",false) return nil if not id return nil if not inst return nil if not inst.definition selection.remove_observer($holePunchingSelectionObserver)############## dups=[] ### process ALL instances inst.definition.instances.each{|e| idd=e.get_attribute("HolePunching","id",false) next if not idd ### skip non-punched instances dups << e if idd == id ### add matches next if not dups[1] ### i.e. NO duplicates! dups.each{|dup| ### we redo ALL dups! HolePunchTool.undo_punch(dup) HolePunchTool.reglue(dup) HolePunchTool.new(dup) } } selection.clear selection.add_observer($holePunchingSelectionObserver)################# end#fix_duplicates def HolePunchTool::reglue(instance=nil) model=Sketchup.active_model selection=model.selection if not instance ### we do selection sela=selection.to_a return nil if not sela model.start_operation(HolePunchTool.db("Reglue")) sela.each{|instance| next if not instance.valid? if instance.class==Sketchup::ComponentInstance and instance.definition.behavior.is2d? and instance.definition.behavior.cuts_opening? next if face=instance.glued_to and face.valid? ### it's already glued tr=instance.transformation co=ORIGIN.clone.transform!(tr) ### switch to our coordinates cv=Z_AXIS.clone.transform!(tr) ### switch to our coordinates return nil if not instance or not instance.valid? or not parent=instance.parent parent.entities.to_a.each{|face| next if not face.class==Sketchup::Face if face.classify_point(co)==Sketchup::Face::PointInside and face.normal.parallel?(cv) instance.glued_to=face break end#if } end#if } model.commit_operation else ### do instance return nil if not instance.valid? tr=instance.transformation co=ORIGIN.clone.transform!(tr) ### switch to our coordinates cv=Z_AXIS.clone.transform!(tr) ### switch to our coordinates status=nil return nil if not instance or not instance.valid? or not parent=instance.parent parent.entities.to_a.each{|face| next if not face.class==Sketchup::Face if face.classify_point(co)==Sketchup::Face::PointInside and face.normal.parallel?(cv) status=instance.glued_to=face break end#if } return status end#if end#def reglue def HolePunchTool::redo_punch() model=Sketchup.active_model selection=model.selection sela=selection.to_a return nil if not sela model.start_operation(HolePunchTool.db("Redo Hole Punching")) selection.remove_observer($holePunchingSelectionObserver)############## insts=[] sela.each{|instance| next if not instance.valid? if instance.class==Sketchup::ComponentInstance and instance.get_attribute("HolePunching","inst",false) and instance.get_attribute("HolePunching","id",false) insts << instance HolePunchTool.undo_punch(instance) HolePunchTool.new(instance) end#if } insts.each{|e|selection.remove(e) if e.valid?} selection.add_observer($holePunchingSelectionObserver)################# model.commit_operation end#def redo ########################################################### end#end Class HolePunchTool ############################################### ########################################################## ### SelectionObserver to select and move reveals with compi etc class HolePunchingSelectionObserver < Sketchup::SelectionObserver def onSelectionBulkChange(selection) model=Sketchup.active_model entsa=model.active_entities.to_a selection.to_a.each{|s| id = -1 next if not s.valid? ### do instances if s.class==Sketchup::ComponentInstance and s.get_attribute("HolePunching","inst",true) and id=s.get_attribute("HolePunching","id",false) next if not id reveals=false entsa.each{|e| ### find matching reveals next if not e.valid? if e.get_attribute("HolePunching","geom",false) and id==e.get_attribute("HolePunching","id",false) selection.add(e) reveals=true end#if } ### insts=s.definition.instances ### if not reveals ### tidy of all of reveals already erased! insts.each{|ins| if id==ins.get_attribute("HolePunching","id",false) ins.set_attribute("HolePunching","inst",false) ins.set_attribute("HolePunching","id",false) end#if } next ###############<<<<<<<<<<<<<< end#if #HolePunchTool.reglue(s) ### do rest as it is geometry elsif not s.class==Sketchup::ComponentInstance and s.get_attribute("HolePunching","geom",true) and id=s.get_attribute("HolePunching","id",false) next if not id entsa.each{|e| if e.valid? and e.get_attribute("HolePunching","geom",false) and id==e.get_attribute("HolePunching","id",false) selection.add(e) end#if } end#if } ### end#def end###class ########################################################### def addHolePunchingSelectionObserver() $holePunchingSelectionObserver=HolePunchingSelectionObserver.new if not $holePunchingSelectionObserver Sketchup.active_model.selection.add_observer($holePunchingSelectionObserver) end#def addHolePunchingSelectionObserver() ########################################################## ### add definition observer for any punch instances definitions. class HolePunchingDefinitionObserver < Sketchup::DefinitionObserver def onComponentInstanceAdded(definition, instance) return nil if not instance or not instance.valid? return nil if not id=instance.get_attribute("HolePunching","id",false) ### check if a duplicate as new instances will normally not have 'id'. $holePunchCopiesArray=[] if not $holePunchCopiesArray $holePunchCopiesArray << instance end#def ### def onComponentInstanceRemoved(definition, instance) return nil if not instance or not instance.valid? return nil if not id=instance.get_attribute("HolePunching","id",false) $holePunchCopiesArray=[] if not $holePunchCopiesArray $holePunchCopiesArray << instance end#def end#class ### Make defn observer at kick off def addHolePunchingDefinitionObserver() $holePunchingDefinitionObserver=HolePunchingDefinitionObserver.new if not $holePunchingDefinitionObserver Sketchup.active_model.definitions.each{|d| d.add_observer($holePunchingDefinitionObserver) } end#def addHolePunchingDefinitionObserver() ### ### watch for new defns added class HolePunchingDefinitionsObserver < Sketchup::DefinitionsObserver def onComponentAdded(definitions, definition) return nil if definition.group? or definition.image? definition.add_observer($holePunchingDefinitionObserver) end def classonComponentTypeChanged(definitions, definition) return nil if definition.group? or definition.image? definition.add_observer($holePunchingDefinitionObserver) end end# ### def addHolePunchingDefinitionsObserver() $holePunchingDefinitionsObserver=HolePunchingDefinitionsObserver.new if not $holePunchingDefinitionsObserver Sketchup.active_model.definitions.add_observer($holePunchingDefinitionsObserver) end#def addHolePunchingDefinitionsObserver() ########################################################## ### Tools observer class HolePunchingToolsObserver < Sketchup::ToolsObserver def onActiveToolChanged(tools, tool_name, tool_id) #p tool_name; p tool_id return nil if tool_id == 21013 #==PasteTool,otherwise it is disabled! return nil if not $holePunchCopiesArray model=Sketchup.active_model model.start_operation(HolePunchTool.db("Hole Punching")+" - "+HolePunchTool.db("Fix Duplicates")) $holePunchCopiesArray.each{|inst|HolePunchTool.fix_duplicates(inst)} $holePunchCopiesArray=[] model.commit_operation end end# def addHolePunchingToolsObserver() $holePunchingToolsObserver=HolePunchingToolsObserver.new if not $holePunchingToolsObserver Sketchup.active_model.tools.add_observer($holePunchingToolsObserver) end#def addHolePunchingToolsObserver() ########################################################## ### AppObserver class HolePunchingAppObserver < Sketchup::AppObserver def onNewModel(model) ### from menu or app addHolePunchingSelectionObserver() addHolePunchingDefinitionsObserver() addHolePunchingDefinitionObserver() addHolePunchingToolsObserver() addHolePunchingAppObserver() end def onOpenModel(model) ### from menu addHolePunchingSelectionObserver() addHolePunchingDefinitionsObserver() addHolePunchingDefinitionObserver() addHolePunchingToolsObserver() addHolePunchingAppObserver() end def onQuit() model=Sketchup.active_model model.selection.remove_observer($holePunchingSelectionObserver) model.definitions.remove_observer($holePunchingDefinitionsObserver) model.definitions.each{|d| d.remove_observer($holePunchingDefinitionObserver) } model.tools.remove_observer($holePunchingToolsObserver) Sketchup.remove_observer($holePunchingAppObserver) end end # Attach the App observer at kick-off def addHolePunchingAppObserver() $holePunchingAppObserver=HolePunchingAppObserver.new if not $holePunchingAppObserver Sketchup.add_observer($holePunchingAppObserver) end#def addHolePunchingAppObserver() ########################################################## ########################################################## ### shortcut to tool def holepunch(inst=nil) Sketchup.active_model.select_tool(HolePunchTool.new(inst)) end#if ########################################################## ########################################################## ### menu if not file_loaded?(File.basename(__FILE__)) ### add to context-menu only UI.add_context_menu_handler{|menu| if HolePunchTool.status()==0 menu.add_separator hpmenu=menu.add_submenu(HolePunchTool.db("Hole Punching")+"...") hpmenu.add_item(HolePunchTool.db("Set Depth")){HolePunchTool.set_depth()} #hpmenu.add_item("Reglue"){HolePunchTool.reglue()} elsif HolePunchTool.status()==1 menu.add_separator hpmenu=menu.add_submenu(HolePunchTool.db("Hole Punching")+"...") hpmenu.add_item(HolePunchTool.db("Punch")){HolePunchTool.new()} hpmenu.add_item(HolePunchTool.db("Set Depth")){HolePunchTool.set_depth()} hpmenu.add_item(HolePunchTool.db("Reglue")){HolePunchTool.reglue()} elsif HolePunchTool.status()==2 menu.add_separator hpmenu=menu.add_submenu(HolePunchTool.db("Hole Punching")+"...") hpmenu.add_item(HolePunchTool.db("Undo Punch")){HolePunchTool.undo_punch()} hpmenu.add_item(HolePunchTool.db("Unlink Punch")){HolePunchTool.unlink_punch()} hpmenu.add_item(HolePunchTool.db("Redo Punch")){HolePunchTool.redo_punch()} hpmenu.add_item(HolePunchTool.db("Set Depth")){HolePunchTool.set_depth()} hpmenu.add_item(HolePunchTool.db("Reglue")){HolePunchTool.reglue()} end#if }#end cmenu end file_loaded(File.basename(__FILE__)) ##########################################################