sketchucation logo sketchucation
    • Login
    πŸ€‘ SketchPlus 1.3 | 44 Tools for $15 until June 20th Buy Now

    Can't draw subclasses of Sketchup::Edge

    Scheduled Pinned Locked Moved Developers' Forum
    25 Posts 4 Posters 672 Views 4 Watching
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • G Offline
      Grays42
      last edited by

      Short version: If I define a class "GcodeEdge < Sketchup::Edge", is there any way to actually draw it?

      Long version: I'm having difficulty writing functions that link and manage Edge objects that act like "gcode", i.e., contain information that allow the edges to be transformed into toolpaths for a CNC machine. In order to function properly, they'd need to link together discretely, and I'm choosing to implement it similar to a doubly linked list, storing coupling information in instance variables.

      Up to this point, I've been simply extending the Edge class itself within a module, but that created a new problem when, upon onEraseEntity(), the entity no longer has access to the methods and variables that allow it to decouple itself from linked GCode edges. The two linked edges have no idea that their shared edge was erased, and I have no way of calling a decouple method on them without reference to the deleted Edge.

      The ideal solution would be to simply make a subclass of Sketchup::Edge that contains all of the extension I need (by overriding .erase!), but a new problem has developed: I can't draw it! The only way to draw edges is with Entities.add_edges, and there's no apparent way to directly draw subclasses. My edges will be perfectly valid as Edges in all respects except for some added functionality, but there's no way to get them into the model.

      The other solution I tried prior to this was to add a class variable to store references to all of the gcode edges and simply have every single gcode edge validate itself and decouple individually in the event that any one gcode edge is deleted. That gets annoyingly complicated, and a subclass would be a much more durable solution.

      I'm stumped; any suggestions?

      1 Reply Last reply Reply Quote 0
      • G Offline
        Grays42
        last edited by

        The code I'm talking about:

        In the class itself:

        module Gcode
        	class Sketchup;;Edge
        
        		def gcode?
        			@@all_gcode_edges = [] if @@all_gcode_edges.nil?
        			return @@all_gcode_edges.include?(self)
        		end
        		
        		def to_gcode!
        			@@all_gcode_edges = [] if @@all_gcode_edges.nil?
        			@@all_gcode_edges << self unless @@all_gcode_edges.include?(self)
        			@observer = self.add_observer(GcodeEdgeObserver.new)
        			
        			# Other stuff
        		end
        	end
        end
        
        

        In the observer:

        module Gcode
        	class GcodeEdgeObserver < Sketchup;;EntityObserver
        		def onEraseEntity(entity)
        			return unless entity.gcode? # <----this method works on the entity until this function runs on deletion, then this function returns false. I can't figure out why.
        			entity.vertices.each do |v|
        				c = entity.coupled_edge[v]
        				c.decouple_vertex(v)
        			end
        		end
        	end
        end
        
        1 Reply Last reply Reply Quote 0
        • A Offline
          Aerilius
          last edited by

          The problem is that the SketchUp API does not mention a Sketchup::Edge.new constructor method. It is Ruby that has one (with 0 arguments) but it is not relevant/intended to be used for SketchUp. Initializing a Ruby object does not make a counterpart in SketchUp's C code, but calling SketchUp's C code functions (via add_edges) will create wrapper objects in Ruby. I even wonder how your subclass edges can have a position and length?

          If I understand right, you want to store data with entitities. You can either do that by keeping an external data structure (a hash with edges as keys), or by using entity attributes.

          If you need additional functionality, you can extend a Ruby object with a module:

          module MyGCodeEdgeMixin
            def do_something
              puts self.length
            end
          end # module
          
          edge.extend(MyGCodeEdgeMixin)
          edge.do_something
          
          1 Reply Last reply Reply Quote 0
          • G Offline
            Grays42
            last edited by

            @aerilius said:

            The problem is that the SketchUp API does not mention a Sketchup::Edge.new constructor method. It is Ruby that has one (with 0 arguments) but it is not relevant/intended to be used for SketchUp. Initializing a Ruby object does not make a counterpart in SketchUp's C code, but calling SketchUp's C code functions (via add_edges) will create wrapper objects in Ruby.

            Yes, that's the wall I've hit. I'm wondering if there's any way around it.

            @aerilius said:

            I even wonder how your subclass edges can have a position and length?

            It would hypothetically, if I could draw it, as it would contain all of the parent class's methods. But since I can't draw it, I'll never know.

            @unknownuser said:

            If I understand right, you want to store data with entitities. You can either do that by keeping an external data structure (a hash with edges as keys), or by using entity attributes.

            If you need additional functionality, you can extend a Ruby object with a module:
            edge.extend(MyGCodeEdgeMixin)

            I think that's the solution I'm after. I'll try it out and respond back if it doesn't do what I need to. Before I get into that, will I be able to properly access and execute cleanup methods if the entity is deleted by the user? (Example in practice: If someone wants to delete an edge and redraw it, the two coupled edges need to decouple when the one in the middle is deleted. I've already developed the ability for the extended edges to automatically re-couple.)

            Also, it is apparent to me that I have misused "module". I was treating it as a scope definition rather than using the .extend function. I've never had to work with modules before.

            In any case, thanks for the info, and I'll give that a shot. Please ignore my code examples that will be posted shortly prior to this post; the moderator has to approve them, so they're in limbo as I type this.

            1 Reply Last reply Reply Quote 0
            • thomthomT Offline
              thomthom
              last edited by

              @grays42 said:

              Also, it is apparent to me that I have misused "module". I was treating it as a scope definition rather than using the .extend function. I've never had to work with modules before.

              No, that is not a mis-use of modules. It's an excellent use!
              On the topic, check out this article: http://www.thomthom.net/thoughts/2012/01/golden-rules-of-sketchup-plugin-development/

              Thomas Thomassen β€” SketchUp Monkey & Coding addict
              List of my plugins and link to the CookieWare fund

              1 Reply Last reply Reply Quote 0
              • G Offline
                Grays42
                last edited by

                Okay, then...I tried using .extend, but my overall problem that prompted my restructuring of the class/subclass/module, etc., is the misbehaving of onEraseEntity() in the observer.

                Here's what my class looks like now (note that my "all_gcode_edges" workaround is still there, will change it to a class variable if I can fix my onEraseEntity problem):

                module Gcode
                	@@all_gcode_edges = [] 
                	
                	def gcode?
                		return @@all_gcode_edges.include?(self)
                	end
                
                	def to_gcode!
                		@@all_gcode_edges << self unless @@all_gcode_edges.include?(self)
                		@observer = self.add_observer(GcodeEdgeObserver.new)
                		
                		#Other stuff
                		
                	end
                
                	# lots of other methods
                
                end
                

                And currently, this is how my unit tests are building edges, although I'll find a way to make this a single line later:

                	p1 = Geom;;Point3d.new(0,0,0)
                	p2 = Geom;;Point3d.new(0,1,0)
                	e1 = Sketchup.active_model.entities.add_edges(p1,p2)[0]
                	e1.extend(Gcode)
                	e1.to_gcode!
                

                Say I have 3 gcode edges in a row, coupled together in doubly linked list form (each edge has a @previous_edge and a @next_edge class variable that stores a reference to the edge preceding or following it). The middle one, which is being observed, is erased by the user.

                In onEraseEntity(), I need to tell the other two, non-deleted edges "The edge that you were paired with is gone, decouple from it". But, I can't; when the entity is in onEraseEntity, it has lost access its module class functions.

                module Gcode
                	class GcodeEdgeObserver < Sketchup;;EntityObserver
                		def onEraseEntity(entity)
                			entity.vertices.each do |v|
                				c = entity.coupled_edge[v]
                				c.decouple_vertex(v)
                			end
                		end
                	end
                end
                

                On deletion, this throws:

                
                Error; #<NoMethodError; undefined method `coupled_edge' for #<Sketchup;;Edge;0xa16f364>>
                C;/Users/.../Plugins/Toolpath_Builder/gcode_edge_observer.rb;5;in `onEraseEntity'
                
                

                The coupled_edge method normally returns the edge coupled at a given vertex, and it works perfectly right up to the point of erasure, then when erasure occurs, the erased Gcode edge no longer has any Gcode functions; it has lost the Gcode mixin at some point prior to being passed into onEraseEntity(). Is there a way for me to trigger a cleanup method as it's being erased?

                If not, my only real option is to trigger a brute force validation for ALL gcode edges by storing references to all of them in a class variable Array. One of them gets deleted, all of them check and validate their coupled edges. Needless to say, this isn't exactly optimal, especially later, when I do things like break down b-splines into low-error arcs that could number in the hundreds or thousands.

                (For clarity, the reason I'm using linked list style references rather than just using the vertex is because multiple toolpaths might converge on the same vertex; A cnc bit will descend to a certain Z-plane, cut around the part to the same point, then descend again. The toolpath must have an unambiguous path and can't get confused if it gets to a four-way intersection.)

                1 Reply Last reply Reply Quote 0
                • Dan RathbunD Offline
                  Dan Rathbun
                  last edited by

                  Your toplevel module name needs to be an "author" or "company" name.

                  Using "Gcode" is not unique enough, and very likely to clash. (There are several other authors working on Gcode projects at this time.)

                  Example:

                  module Grays
                  
                    module GcodeLib
                  
                      class Gcode
                  
                      end # class Gcode
                  
                    end # module GcodeLib
                  
                  end # module Grays
                  

                  I'm not here much anymore.

                  1 Reply Last reply Reply Quote 0
                  • G Offline
                    Grays42
                    last edited by

                    I appreciate that, but I don't have any plans at present to release it to the public, nor would I under that naming convention. I'm just trying to fix my onEraseEntity problem.

                    1 Reply Last reply Reply Quote 0
                    • Dan RathbunD Offline
                      Dan Rathbun
                      last edited by

                      No you cannot use custom subclasses of any of the API's Sketchup::Drawingelement subclasses (ie, edge, curve, etc.)

                      Main reason (besides the fact that the app engine would not know how to draw them,) is that the API methods that ADD drawingelement objects to the model, are coded to only accept the specific API class, and not any subclasses.

                      Look at the "add" methods in the Entities class.

                      There are methods to ONLY add standard API class objects.

                      I'm not here much anymore.

                      1 Reply Last reply Reply Quote 0
                      • Dan RathbunD Offline
                        Dan Rathbun
                        last edited by

                        Well we cannot see all your code so...

                        ... all I see is that you are extending the Array that is holding references to edge objects.
                        But the observer callback is wanting to call extended methods on edge objects themselves.

                        I'm not here much anymore.

                        1 Reply Last reply Reply Quote 0
                        • G Offline
                          Grays42
                          last edited by

                          @dan rathbun said:

                          Well we cannot see all your code so...

                          ... all I see is that you are extending the Array that is holding references to edge objects.
                          But the observer callback is wanting to call extended methods on edge objects themselves.

                          coupled_edge is an instance method for an Edge object within the module. When the Edge object is passed to onEraseEntity, it is stripped of all Gcode mixin instance functions and variables, making a custom cleanup routine impossible, as far as I can tell. The class variable array is how I'm attempting a workaround; it is not actually necessary, and in fact causes a big problem in the event that a very large number of Gcode Edge objects are present in the model.

                          I'm looking for a workaround to implement my own cleanup routine in onEraseEntity (or, in general, when the Gcode Edge entity is erased) that has access to functions native to the module. That isn't possible unless the functions and/or variables in the mixin are present on the entity object that is passed to onEraseEntity. Any ideas?

                          1 Reply Last reply Reply Quote 0
                          • thomthomT Offline
                            thomthom
                            last edited by

                            @aerilius said:

                            If you need additional functionality, you can extend a Ruby object with a module:

                            module MyGCodeEdgeMixin
                            >   def do_something
                            >     puts self.length
                            >   end
                            > end # module
                            > 
                            > edge.extend(MyGCodeEdgeMixin)
                            > edge.do_something
                            

                            Beware of this though - if say plugin A iterates all edges an extend them, adding method foo - then plugin B comes along and iterate each edge afterwards method foo is accessible to plugin B as well. The object instance for entities are persistent through the model session.
                            So if plugin C also extends the edge with method foo then you have a conflict. And off course this means that modifying a base method would break things for everyone.

                            You are in effect adding methods into a shared namespace and risk collision. I don't really recommend this. Not for entities. It works for Point3d objects - because they are not persistent. When you ask for Vertex.position you get a new object every time.

                            I really wish one could get a local copy though to extend. But alas.

                            Thomas Thomassen β€” SketchUp Monkey & Coding addict
                            List of my plugins and link to the CookieWare fund

                            1 Reply Last reply Reply Quote 0
                            • thomthomT Offline
                              thomthom
                              last edited by

                              In fact, by using extend it's harder to detect a clash - as one cannot do a search for files that modify Sketchup::Edge...

                              Thomas Thomassen β€” SketchUp Monkey & Coding addict
                              List of my plugins and link to the CookieWare fund

                              1 Reply Last reply Reply Quote 0
                              • thomthomT Offline
                                thomthom
                                last edited by

                                Yes, it's a known problem - observer events that report erased entities report then after the object has been erased - not before. I think it's a completely different reference passed.

                                I've been trying to map out the issues and limitations of observers here:
                                http://www.thomthom.net/software/sketchup/observers/
                                (And some extra notes here: https://bitbucket.org/thomthom/sketchup-observers/issues?status=new&status=open )

                                Not sure how to work around it. It's a very troublesome limitation. Maybe you can try to index the edges you want to keep track of - and when onErase is triggered you iterate your own cache and query each of them to check if they are valid or deleted. ?

                                (regardless I still strongly recommend you don't extend or subclass the base classes - if you create a conflict it'll very hard to debug, not only for you, but also the other developers affected.)

                                Also beware that modifying the model in observer events is very risky. You might break native of third party operations, clutter the undo stack and there's a good possibility of causing a BugSplat. Observer events are best for just collecting data which you then react to at later "safe" points in your code. (Just and FYI as this is a common issue that trips people when working with observers.)

                                Thomas Thomassen β€” SketchUp Monkey & Coding addict
                                List of my plugins and link to the CookieWare fund

                                1 Reply Last reply Reply Quote 0
                                • G Offline
                                  Grays42
                                  last edited by

                                  Okay, that helps, it's good to know that it isn't just me.

                                  My workaround that I was working on was to store all mixin-enabled edges in a class array (not instance array) and trigger validation on all of them when any one is deleted...but that becomes incredibly expensive once the number of edges becomes high. Later on, I'll be breaking down b-splines into very small arcs with minimum error (as the cnc controller can't handle splines), and those will have on the order of hundreds of mixin-enabled arcs. If all arcs in a very large object are validated every time one of the hundreds of arcs is deleted when a single spline is deleted, it will have processing time on the order of n^2, very bad.

                                  I'll figure out another workaround and I'm glad it isn't just something I'm missing. I'll toy with adding functionality to the adjoining vertices and calling that. A mixin on the vertices of the deleted entity should still exist even if the mixin on the entity no longer does, right?

                                  And I'm only extending the base class within the namespace of my module, as we previously discussed. Does that cause conflicts? If so, what is your recommendation for adding the functionality I'm after to the base class without conflicting with other plugins?

                                  Also, a mostly unrelated question: is there a constructor for .extend or a preferred method for constructor-like functionality that automatically triggers once you call .extend on the base class of an instance? The .extended method in the documentation seems to only apply if the entire class is extended, not just an instance. Nevermind, defining .extend inside the mixin itself acted as a constructor when the mixin is triggered on an base class instance with .extend.

                                  1 Reply Last reply Reply Quote 0
                                  • G Offline
                                    Grays42
                                    last edited by

                                    thomthom, I appreciate that, but I'm already there--I'm doing that. I'm trying to explain my issue in detail, and I'm miscommunicating.

                                    My difficulty is that the entity is no longer extended with the mixin when the entity is passed into onEraseEntity by an observer, and using .extend(Gcode) again inside onEraseEntity adds the methods, but not any of the data.

                                    coupled_edge is a mixin class method that functions correctly until the user deletes the edge. The deletion triggers the observer that I've attached to the edge, then it's supposed to execute this code. Note that all of the involved edges in this example have been extended as Gcode; the entity and the two adjoining edges.

                                    module Gcode
                                       class GcodeEdgeObserver < Sketchup;;EntityObserver
                                          def onEraseEntity(entity)
                                             entity.vertices.each do |v|
                                                adjoining_edge = entity.coupled_edge(v) #throws undefined method
                                                adjoining_edge.decouple_at_vertex(v)
                                                #other cleanup methods here
                                             end
                                          end
                                       end
                                    end
                                    

                                    At this point, the entity passed into onEraseEntity no longer contains any mixin methods, and is just a core class with no other functions. I have tried a number of calls to the mixin methods and variables on the entity that is passed into onEraseEntity and none of them work. It is simply no longer extended at that point. Calling .extend(Gcode) on the entity within onEraseEntity gives it Gcode methods again, but none of the data.

                                    That's why I posted about making a subclass in the first place; everything in this thread has been an attempt to work around the onEraseEntity limitations. As it stands, I have absolutely no way to run a cleanup routine, as the necessary functions don't exist in the scope of onEraseEntity. Any suggestions for how to work around it?

                                    1 Reply Last reply Reply Quote 0
                                    • thomthomT Offline
                                      thomthom
                                      last edited by

                                      @grays42 said:

                                      And I'm only extending the base class within the namespace of my module, as we previously discussed. Does that cause conflicts? If so, what is your recommendation for adding the functionality I'm after to the base class without conflicting with other plugins?

                                      Yes, any module you extend will be there for everyone else.

                                      If you have defined your mixin module Bar within your namespace Foo adding method #hello then any other script that iterate the entities will have access to method #hello and the entity will return true to entity.is_a?(Foo::Bar). The effect of using #extend is global. Hence the chance of conflicts.

                                      Thomas Thomassen β€” SketchUp Monkey & Coding addict
                                      List of my plugins and link to the CookieWare fund

                                      1 Reply Last reply Reply Quote 0
                                      • G Offline
                                        Grays42
                                        last edited by

                                        Ok, I understand now.

                                        So, as a solution, what if I put an extremely unique prefix in every public function definition in the base class extension? Would that be sufficient, or is there a more elegant way to get the functionality I want?

                                        1 Reply Last reply Reply Quote 0
                                        • G Offline
                                          Grays42
                                          last edited by

                                          The code (very rough draft):

                                          module Gcode
                                          	class Sketchup;;Edge
                                          		def self.entity_id_is_gcode?(entity_id)
                                          			return @@gcode_edges.keys.include?(entity_id)
                                          		end
                                          		
                                          		def self.get_gcode_edge_by_id(entity_id)
                                          			return @@gcode_edges[entity_id]
                                          		end
                                          	end
                                          
                                          	module GcodeEdge
                                          		class Sketchup;;Edge
                                          			@@gcode_edges = {}
                                          			
                                          			def extend(base)
                                          				@is_gcode = true
                                          				@@gcode_edges[self.entityID] = self
                                          			end
                                          			
                                          			def is_gcode?
                                          				return false if @is_gcode.nil? or @is_gcode == false
                                          				return true
                                          			end
                                          			
                                          			def asdf
                                          				puts "Success! Successfully retrieved instance data while in onEraseEntity." if self.is_gcode?
                                          			end
                                          		end
                                          	end
                                          end
                                          

                                          The observer:

                                          module Gcode
                                          	class GcodeEdgeObserver < Sketchup;;EntityObserver
                                          		def onEraseEntity(entity)
                                          			puts entity.entityID #output is always the negative of the ID
                                          			
                                          			#Attempting to call on the entity actually passed to onEraseEntity
                                          			puts "Attempting to puts based on a class variable of the erased entity (should NOT work);"
                                          			entity.asdf
                                          			
                                          			#The REAL entity;
                                          			puts "Attempting to puts based on an instance variable of the erased entity (SHOULD work);"
                                          			pre_deleted_entity = Sketchup;;Edge.get_gcode_edge_by_id(-1*entity.entityID)
                                          			pre_deleted_entity.asdf
                                          
                                          	        end
                                          	end
                                          end
                                          

                                          The relevant lines from my unit tests (deleted anything not related):

                                          module Gcode
                                          		def self.unit_test_gcode_vertex
                                          			puts "-----------TESTING GcodeVertex-----------"
                                          			
                                          			p1 = Geom;;Point3d.new(0,0,0)
                                          			p2 = Geom;;Point3d.new(0,1,0)
                                          			
                                          			e1 = Sketchup.active_model.entities.add_edges(p1,p2)[0]
                                          			e1.add_observer(GcodeEdgeObserver.new)
                                          			puts e1.entityID
                                          			
                                          			e1.extend(GcodeEdge)
                                          			e1.erase!
                                          			puts "-----------------------------------------"
                                          		end
                                          end
                                          

                                          Calling unit_test_gcode over and over produces this output (notice the IDs!):

                                          -----------TESTING GcodeVertex-----------
                                          2069
                                          -2069
                                          Attempting to puts based on a class variable of the erased entity (should NOT work);
                                          Attempting to puts based on an instance variable of the erased entity (SHOULD work);
                                          Success! Successfully retrieved instance data while in onEraseEntity.
                                          -----------------------------------------
                                          -----------TESTING GcodeVertex-----------
                                          2074
                                          -2074
                                          Attempting to puts based on a class variable of the erased entity (should NOT work);
                                          Attempting to puts based on an instance variable of the erased entity (SHOULD work);
                                          Success! Successfully retrieved instance data while in onEraseEntity.
                                          -----------------------------------------
                                          -----------TESTING GcodeVertex-----------
                                          2079
                                          -2079
                                          Attempting to puts based on a class variable of the erased entity (should NOT work);
                                          Attempting to puts based on an instance variable of the erased entity (SHOULD work);
                                          Success! Successfully retrieved instance data while in onEraseEntity.
                                          -----------------------------------------
                                          -----------TESTING GcodeVertex-----------
                                          2084
                                          -2084
                                          Attempting to puts based on a class variable of the erased entity (should NOT work);
                                          Attempting to puts based on an instance variable of the erased entity (SHOULD work);
                                          Success! Successfully retrieved instance data while in onEraseEntity.
                                          -----------------------------------------
                                          -----------TESTING GcodeVertex-----------
                                          2089
                                          -2089
                                          Attempting to puts based on a class variable of the erased entity (should NOT work);
                                          Attempting to puts based on an instance variable of the erased entity (SHOULD work);
                                          Success! Successfully retrieved instance data while in onEraseEntity.
                                          -----------------------------------------
                                          

                                          So...it works! As far as I can tell, this will be a 100% consistent and reliable workaround for the fact that the wrong entity is passed into onEraseEntity.

                                          1 Reply Last reply Reply Quote 0
                                          • G Offline
                                            Grays42
                                            last edited by

                                            I figured out a workaround! I accessed the correct entity in onEraseEntity!

                                            During testing, it turned out that the entityID of the entity passed into onEraseEntity in the observer is always the negative of the entityID of the erased entity, AND the real entity still exists and can execute mixin instance methods and retrieve instance data.

                                            In other words, if real_entity.entityID == 1923, and has an observer, then once you call real_entity.erase!, the observer calls onEntityErase(wrong_entity). I discovered through testing that, inside onEntityErase, wrong_entity.entityID == -1923.

                                            So...all I did was create global_hash_of_real_entities = {}, keyed by real_entity.entityID every time a real_entity is initialized, with the value being a reference to real_entity. Inside onEntityErase, you can access the pre-deleted real_entity by evaluating global_hash_of_real_entities[-1*wrong_entity.entityID].

                                            The only caveat is that you can't use any base methods in real_entity, they'll throw errors. However, if there's some functionality you need during the cleanup routine, you can duplicate it with a mixin method and that method will still exist during onEraseEntity.

                                            I got a test to produced this output:

                                            -----------TESTING GcodeVertex-----------
                                            Attempting to puts based on an instance variable of the erased entity (should NOT work);
                                            Attempting to puts based on an instance variable of the erased entity (SHOULD work);
                                            Success! Successfully retrieved instance data while in onEraseEntity.
                                            -----------------------------------------
                                            

                                            The second example produced a string that would only successfully trigger if the workaround entity (inside onEraseEntity) had a boolean switched to true BY an instance method in the entity passed into onEraseEntity prior to being erased. The solution does involve a class variable hash, but given a unique enough name, it shouldn't cause a problem.

                                            1 Reply Last reply Reply Quote 0
                                            • 1
                                            • 2
                                            • 1 / 2
                                            • First post
                                              Last post
                                            Buy SketchPlus
                                            Buy SUbD
                                            Buy WrapR
                                            Buy eBook
                                            Buy Modelur
                                            Buy Vertex Tools
                                            Buy SketchCuisine
                                            Buy FormFonts

                                            Advertisement