DimensionArea and add_dimension_area?

Hello!
I’m working on a function to toggle displaying the area size of faces on the face. I’ve got the text-part to work:

Module Bob
   def self.text_on_face( txt, face, group )
      # Print the angle on the surface
      dim = group.entities.add_3d_text( txt, TextAlignCenter, "Arial", true, false, 7, 0.0, 0.0, false, 0.0 )

      # 1) Translate to center of the text
      tr = Geom::Transformation.translation( [-0.5*group.local_bounds.width, -0.5*group.local_bounds.height, 0] )
      group.transform!( tr )

      # 2) Flip the text so up is up
      tr = Geom::Transformation.rotation( ORIGIN, X_AXIS, 90.degrees )
      group.transform!( tr )

      # 3) Find the orientation of the face to orient the text
      new_Y_AXIS = face.normal.reverse          
      new_X_AXIS = new_Y_AXIS * Z_AXIS
      tr = Geom::Transformation.axes( center_of(face), new_X_AXIS, new_Y_AXIS )
      troup.transform!( tr )

      return dim
    end

   def self.center_of(face)
      pts = face.vertices.map{|v| v.position}
      Geom::Point3d.new(
         avg( pts.map{|pt| pt.x } ),
         avg( pts.map{|pt| pt.y } ),
         avg( pts.map{|pt| pt.z } )
      ).project_to_plane(
         face.plane # make sure point is on the face's plane
      )
   end
end

I would like dim to update if the face changes but of course it doesn’t. If I adjust the face now, the text doesn’t move with the face, and if the area changes, the text doesn’t update. So it doesn’t work like a Sketchup::Dimension yet.

I’ve read several earlier posts about this topic and understand that the ruby version of add_dimension_linear is limited compared to the UI version. So rather than using add_dimension_linear and trying to adjust its appearance, I’m thinking about making a subclass of Dimension with some new functionality to make this possible. I’m thinking maybe something like this:

class DimensionArea < Sketchup::Dimension
   # Magic goes here...
   # Goal is that this dimension updates when the model updates
   # Like all other dimensions do....
end

class Sketchup::Entities
   def add_dimension_area( face )
      txt = Sketchup.format_area( face.area )
      dim = Bob::text_on_face( txt, face, myGroup )
      return dim
   end
end

Would this be possible? And would it be ok to add methods to existing classes in Sketchup or better not?

1 Like

(1) It must be module Bob not Module Bob.

Ie, the keyword module means “open the named Module instance for editing (and create if it does not yet exist.)”
Meaning that "module" is an interpreter keyword, and "Module" is the identifier for class Module.
In other code, …

module Bob
  # code here
end

… is the equivalent of …

Bob ||= Module.new
Bob.module_eval do
  # code here
end

(2) It is absolutely forbidden for you to modify Ruby core and SketchUp API classes and modules in a shared coding environment such as SketchUp’s Ruby process.

It is possible to refine core and API classes that only your code can see and use, but this is an advanced coding concept. See the primer:


(3) NEVER define custom classes at the top level of Ruby’s ObjectSpace. Everything in Ruby is an object, and doing so functionally defines the class within Object. So, it makes these classes global.

The same goes for defining methods at the top level. Such methods become global as they will get inherited by everyone else’s modules and classes (as class Class is a subclass of class Module, and ALL classes are a descendent of class Object.)

ONLY Ruby core and Standard Library classes (and within SketchUp a minimum of API classes,) should be defined at the top level as global classes.

More about your needs re: dimensions below


(4) Whilst we do appreciate that you have wrapped up some of you code in a top level namespace module, … “Bob” is not unique enough. "BobGiesberts" would be a better choice.

Usually though, each of your extensions (or utilities) would be defined within a submodule of your unique namespace module.

Within the extension submodule is where you would define the custom classes used only by that extension. When you do this it will not be necessary to qualify (with preceding module name and :: scope operator,) your calls to these classes as they are local. (Ie, their class identifiers are a constant local to the submodule scope.)

1 Like

The SKP file format likely does not know how to handle custom entity classes / subclasses.

In other words, extension code cannot directly instantiate entity classes, nor insert custom entity objects into the model’s entities collections, but must instead use entities collection factory methods such as #add_dimension_linear. There is also no API means to customize any of the API collection classes (such as Sketchup::Entities,) and replace the C++ collections with ones having custom factory methods.

So subclassing Sketchup::Dimension or Sketchup::DimensionLinear is not likely the answer to your desires.


With regard to UI version, I think you mean the application (and therefore API) version. The Sketchup::Entities#add_dimension_linear factory method was added for SketchUp 2014, so it should be fine for your version Make 2017.
(It is also not very likely that any large number of users are running any versions below SU2014, as most extension authros do not support those older versions as they do not run Ruby 2.x.)


There are several ways. One is to group the dimension and face together.

There are also observer classes that your extension can use to “watch” the face or dimension objects for a change, and react in some way (such as changing the dimension text.)

If you can be more descriptive of your desire, and perhaps post snip images, it might help us help you.


The center of a face …

   def self.center_of(face)
      pts = face.vertices.map{|v| v.position}
      Geom::Point3d.new(
         avg( pts.map{|pt| pt.x } ),
         avg( pts.map{|pt| pt.y } ),
         avg( pts.map{|pt| pt.z } )
      ).project_to_plane(
         face.plane # make sure point is on the face's plane
      )
   end

All geometric objects have a bounding box. Depending upon the number of edges (vertices) and complexity of the face, you can get a simple center or a centroid. (There are other topics in this category that has covered this.)

For a simple center …

center = face.bounds.center

However, a bounding box is aligned to the model axes. And if the face is complex with many edges and shaped weirdly off-axes, then this will not really give you a true center or centroid.

Do a search using the mag glass icon from within this category on “centroid”, etc.


FYI, a neat thing with Ruby 2.x.
There is a shorthand for an iterator that simply calls one method upon all members of a collection …

pts = face.vertices.map { |v| v.position }

… can be written as …

pts = face.vertices.map(&:position)
3 Likes

dim here (the variable in your code or a return of your #text_on_face method) is not what you think. It’s not a dimension or number, it’s not even 3D text (added as edges and faces).

It is a (Boolean)true if the #add_3d_text method returns successful.

The “to update” you can expect, that the dim to be true or not.
Change doesn’t happen by itself, as Dan said, you should describe with other wording what you want to achieve…


There is a syntax error here:


_

if the face is parallel to z plane the new_X_AXIS will be a zero length vector. Transformations doesn’t like that…

… and to extend what Dezmo said about returning the dim

In the UI, the 3D Text tool automatically wraps the text primitives within a group.

The API does not. Your code must create the group. In your “snippet” the group is actually not created.
It should be created immediately before the call to create the text primitives in your text_on_face method.

group = face.parent.entities.add_group

FYI, the Text callout / leader objects will automatically represent the area of a face if it’s arrow is attached to a face. (They are poorly named and should be named “Callout”. They are named “Leader” in LayOut.)

Before getting back to the original question, let me just say that I made a ‘snippet’ of my code in an attempt to make it more clear to read, but you’ve found all the mistakes I’ve made while doing this. I think that’s amazing.

Module and troup are typos while making the snippet. I’m using module Climbing inside module Bob, that should be unique enough. I do make a group for the text and I did catch the horizontal plane case that Dezmo mentions.

I just want to place my text on the face, approximately in the center of it, so this is just perfect. Didn’t think of this simple way to find the center of a face. Thanks!


Clear answer! Thank you


Back to the original question. Here an illustration. The Dimension updates automatically when changes are made to the face. The 3D Text doesn’t update (as expected). The Text (Annotation) also doesn’t update. What I want to make is a text on the face (like the 3D text) that behaves like a Dimension, so it’s location and text updates when changes are made to the face.
Capture

It makes sense to try using an observer. I’ll work on that. Do you think I can do this with an EntitiesObserver for the whole model, or should I make an EntityObserver for every face I want to have this text on?

Auto updating 3D Text is not a contract of the API, so it is not to be expected.

(It is after all just made up of primitive edges and faces.)

Oh yes you are correct. But this isn’t what you want anyway.

Okay, then you will have to use an observer to watch the face for changes, and update the 3D Text object (likely tagged with an AttributeDIctionary whose name corresponds to your qualified extension name,) so you can find it.
Ie, "BobGiesberts_FaceAreaLabeler" (whatever)

module BobGiesberts
  module FaceAreaLabeler

    extend self

    DICTNAME ||= "BobGiesberts_FaceAreaLabeler"

    def attach_observer(face)
      # Attach this submodule as an EntityObserver
      face.add_observer(self) if face.is_a?(Sketchup::Face)
    end

    def onChangeEntity(entity)
      return unless entity.is_a?(Sketchup::Face)
      ents = entity.parent.entities
      text_grp = ents.grep(Sketchup::Group).find { |grp|
        grp.attribute_dictionary(DICTNAME) 
      }
      if text_grp # nil if not found
        # Delete the text group's internal geometry
        # Remake the 3D Text with the face's new area
      end
    end

  end
end

Notice that the above example does not use the class paradigm for an EntityObserver. The #add_observer method(s) do not do any type checking on the passed observer argument. All that is required is for the object passed to have publicly accessible observer callback methods.
If no model specific state needs to be held within an observer, then the observer would not need to be an instanced object.

I suggest you examine Thomas’ 3D Text Editor extension. The code is not encrypted and serves as a good example of how to save 3D Text properties into attributes attached to a group that wraps the text primitives:

It is not the nesting that makes it unique, … it is the top level namespace module name that needs to be unique. (So all of your various code projects or extensions are separated from all of everyone else’s extensions.)

But if for your own use it will not be a problem. However if you ever try to publish through the Extension Warehouse, likely it would be rejected.

After all, every successful coder has an uncle named Bob. :wink:

Seriously though, often the namespace is a company or product line name. (Something trademarkable.)

I couldn’t stand not to try…
All credit to @DanRathbun
(The mistakes are mine… :wink: )
Quick and dirty code snippet:
(Sure, a lot of thing is not taken into consideration. For example, the text is not aligned, the observer is not removed … etc. )

module DezTest
  extend self

  DICTNAME ||= "DezTest_Dict"

  def attach_observer(face)
    # Attach this submodule as an EntityObserver
    face.add_observer(self) if face.is_a?(Sketchup::Face)
    text_on_face( face )
  end
  
  def text_on_face(face)
    txt = Sketchup.format_area( face.area )
    text_grp = face.parent.entities.add_group
    text_grp.entities.add_3d_text( txt, TextAlignCenter, "Arial", true, false, 7, 0.0, 0.0, false, 0.0 )
    text_grp.attribute_dictionary(DICTNAME, true) 
  end
  
  def onChangeEntity(entity)
    return unless entity.is_a?(Sketchup::Face) && entity.valid?
    ents = entity.parent.entities
    text_grp = ents.grep(Sketchup::Group).find { |grp|
      grp.attribute_dictionary(DICTNAME) 
    }
    if text_grp
      text_grp.entities.clear!
      text_on_face(entity)
    end
  end

end
DezTest.attach_observer(Sketchup.active_model.entities.grep(Sketchup::Face)[0])

textonface

4 Likes

Thanks guys, this is something I can work with!

Just one question about your comment:

When (and why?) should I remove the observer? Would this be within EntityObserver::onEraseEntity

I’m not sure, I’m also just learning the observers…
But I rather feel that if you delete the face to which the observer is attached, the observer will also “disappear”.
In this case, of course, the area “label” also loses its meaning, so if the face was deleted, I would delete the “label” as well using the onEraseEntity for this.

I would rather remove the observer from the face if the “txt_grp” associated with it has been deleted. So maybe you need to attach another observer to “txt_grp” and see if it’s deleted, then remove the observer of the face… or something like that.
I guess further investigation is necessary, how do you want to deal with the different scenarios…

BTW. There is a warning in the documentation of EntityObserver :
The methods of this observer fire in such a way that making changes to the model while inside of them is dangerous.

… but unfortunately I can’t fully interpret it. This is beyond my knowledge of English …
or deliberately worded vaguely by the creators because they don’t know the meaning or cause either :blush:

Thank Dezmo! I’ve now done the attributes the other way around: the face has an attribute with the entityID of the group with the 3D text (textGroup), like this

face.set_attribute( DICTNAME, "textEntityID", textGroup.entityID )
status = face.add_observer( self ) if face.is_a?( Sketchup::Face )

Now if the face is changed, I can find the textGroup and update its text and location.


One more question about this topic (or should I start a new thread about this?).

I noticed it’s hard to capture the actions that are done by the observer within a start_operation / commit_operation section. In other words: this approach messes up the undo-stack! I found an old article about this by @thomthom but no solution.

Changing the entity triggers the observer, which causes the 3D text to change (the text and the location). I think the solution should be in the 4th parameter of Sketchup::Model#start_operation, which is transparant: append this operation to the previous one. However, if multiple connected faces have a 3D text on them, with each an observer, and changing one face affects multiple faces, then still multiple operations are added to the undo-stack. My goal would be to have just one operation in the undo stack: the action that changed the entity / entities. Any thoughts on how to solve this? Observers are new to me, maybe I just don’t get it? This is my code

def onChangeEntity( face )

    # Find the textGroup that's attached to this face
    textGroupID = face.get_attribute( DICTNAME, "textEntityID", nil )
    return unless textGroupID
    model = Sketchup.active_model
    textGroup = model.find_entity_by_id( textGroupID )
    return unless textGroup  

    # Delete the contents of the textGroup
    textGroup.entities.clear!

    # Update the text
    model.start_operation( "BobGiesberts: text was updated", true , false, true )
    txt = Sketchup.format_area( face.area )
    textGroup = text_on_face( txt, face, textGroup )
    textGroup = align_to_face( textGroup, face )
    model.commit_operation
  end

Sorry about that, the transparent parameter does the trick, just a stupid error in my code. I started the operation after deleting the old 3D text. My bad!
Fixed code below:

def onChangeEntity( face )

    # Find the textGroup that's attached to this face
    textGroupID = face.get_attribute( DICTNAME, "textEntityID", nil )
    return unless textGroupID
    model = Sketchup.active_model
    textGroup = model.find_entity_by_id( textGroupID )
    return unless textGroup  

    model.start_operation( "BobGiesberts: text was updated", true , false, true )

    # Delete the contents of the textGroup
    textGroup.entities.clear!

    # Update the text
    txt = Sketchup.format_area( face.area )
    textGroup = text_on_face( txt, face, textGroup )
    textGroup = align_to_face( textGroup, face )
    model.commit_operation
  end
1 Like

How many text entities do you expect to update?

Reason I ask is that creating transparent operations will make multiple operations undoable in a single step. However, internally they are still recorded as separate operations (they just have a transparent flag associated with them that is used when Undo/Redo is performed.

Which leads into another details; SketchUp’s Undo stack is currently limited to 100 operations.

So you’d end up with something like this:

Move
Update3dText(Transparent)

Being two items on the undo stack. If you have multiple 3d text entities updating they’ll accumulate.

Move
Update3dText(Transparent)
Update3dText(Transparent)
Update3dText(Transparent)
Update3dText(Transparent)

If your extension ends up triggering more than a 100 of these then an undo will not be able to undo everything. The user might even experience issues before exhausting the 100 undo operations, as the number of undo-steps will be reduced by the amount of chained operations.

If that’s a possible scenario you might want to look into a way to batch up your observer events. (Not entirely sure what to suggest, but if it’s not likely to happen then there is no need to dive deeper into this.)

Another note on observers: Validate that the object you get from the observer is valid. There is no guaranty it is. If another observer triggers before yours it might have deleted it.

def onChangeEntity( face )
  return unless face.valid?
  # Do stuff to face...
end
1 Like

Be sure to read the release Notes on the v2016 observer overhaul, …

SketchUp API Release Notes: SketchUp 2016 M0 - Observer Upgrades

… and the accompanying guide …

Yes, with SketchUp 2016 we made a change such that observers were queued until the operation was done. This was because triggering observers in the middle of an operation became highly problematic if the observer made changes to the model - at any point the entities you were working on could get removed or modified making it impossible to reliably perform you action. It was also a major source of crashes because it nearby impossible to protect against third-party modifications during an operation.

So since SU2016 the contract is that when you make your operation you own it completely. No other extension is allowed to interrupt you in the midst of it. This also makes it easier to measure performance as you don’t risk other extensions from having their logic injected into your execution of the operation.

Thanks a lot for this!

I don’t expect more than 100 operations to be triggered, but I’ll try to catch such an event to prevent the scenario you described. The concept works, now I’ll work on catching these events and error handling.

Helps a lot! I didn’t know these documents exist. Thanks!

1 Like