The SketchUp Ruby API documentation doesn’t always tell the whole tale. Many things you discover as you go along. I’ve collected what I have come found out so far.
Materials Collection has Secret Items
Given a model that contains two materials and an Image element you would think that model.materials.length
returns 2
. But instead it returns 3
– this reveals that Image
entities contribute to the internal material list in a model. Not so surprising when you discover that Image
entities are instances of ComponentDefinition
.
The curiosity is that when you iterate the collection with #each
you do not get any of the materials that belongs to Image
entities.
1 2 3 | for material in model.materials p material.name end |
for material in model.materials p material.name end
The output of this is:
1 2 | "UV Tile" "[Brick_Colored_Blue]" |
"UV Tile" "[Brick_Colored_Blue]"
But when you iterate by the indexes you get a different set of materials:
1 2 3 | for i in (0...model.materials.count) p model.materials[i].name end |
for i in (0...model.materials.count) p model.materials[i].name end
Output:
1 2 3 | "UV Tile" "Image1" "[Brick_Colored_Blue]" |
"UV Tile" "Image1" "[Brick_Colored_Blue]"
The API hides Image related materials, but doesn’t expose any method that tells you how many non-Image materials there are. To get that number you need to do the counting yourself:
1 | model.materials.inject(0) { |total,material| total + 1 } |
model.materials.inject(0) { |total,material| total + 1 }
Another illustration of how idiosyncratic the Materials collection is:
1 2 | model.materials.include?( image_entity_material ) # => false |
model.materials.include?( image_entity_material ) # => false
If you try to create a material from the SketchUp UI with the name "Image1"
you will get an messagebox from SketchUp saying that there already exist a material with that name – but you don’t see it in the Material Browser.
Image Materials
If you have an Image
entity and want to find the associated material for it you can obtain it by digging into the definition of the Image
:
1 | image_definition.entities.find{|e|e.is_a?(Sketchup::Face)}.material |
image_definition.entities.find{|e|e.is_a?(Sketchup::Face)}.material
I used that method to create a plugin that let me adjust the opacity of Image entities.
Removing Materials
Before SketchUp 8 Maintenance Release 1 there was no API method to remove a material from a model. The only workaround way was to ensure all other materials was assigned to some entity and purge the materials collection.
Materials.remove
, which M1 introduced, had some unexpected behaviour. The material was only removed from the material list, but not from the entities in the model. So the result of using it is a model with invalid materials assigned to its entities. If you use the Fix Problems function in SketchUp it will complain about these invalid materials and remove it from the front side of faces – but not from the backside. To avoid a corrupted model you need to manually remove the material from all entities before using Materials.remove(material)
.
The reason for this behaviour is that the method exposed a low level C++ function – which didn’t clean up the model as one would expect. Hopefully it will be updated in the future as the current implementation is very prone cause some annoying model corruption.
I wrote a method to safely remove materials from a model. (Example is extracted and adapted from my TT_Lib2 library.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | module MaterialsEx # Safely removes a material from a model. # # @param [Sketchup::Material] material # @param [Sketchup::Model] model # # @return [Boolean] def self.remove( material, model = Sketchup.active_model ) # SketchUp 8.0M1 introduced model.materials.remove, which turned out to be # bugged. It didn't remove the material from the entities in the model - # leaving the model with rouge invalid materials. # To work around this all entities are processed before the method is called. # The workaround for older versions also require this to be done. for e in model.entities e.material = nil if e.respond_to?( :material ) && e.material == material e.back_material = nil if e.respond_to?( :back_material ) && e.back_material == material end for d in model.definitions next if d.image? for e in d.entities e.material = nil if e.respond_to?( :material ) && e.material == material e.back_material = nil if e.respond_to?( :back_material ) && e.back_material == material end end materials = model.materials if materials.respond_to?( :remove ) materials.remove( material ) else # Workaround for SketchUp versions older than 8.0M1. Add all materials # except the one to be removed to temporary groups and purge the materials. temp_group = model.entities.add_group for m in model.materials next if m == material g = temp_group.add_group g.material = material end materials.purge_unused temp_group.erase! true end end end # module |
module MaterialsEx # Safely removes a material from a model. # # @param [Sketchup::Material] material # @param [Sketchup::Model] model # # @return [Boolean] def self.remove( material, model = Sketchup.active_model ) # SketchUp 8.0M1 introduced model.materials.remove, which turned out to be # bugged. It didn't remove the material from the entities in the model - # leaving the model with rouge invalid materials. # To work around this all entities are processed before the method is called. # The workaround for older versions also require this to be done. for e in model.entities e.material = nil if e.respond_to?( :material ) && e.material == material e.back_material = nil if e.respond_to?( :back_material ) && e.back_material == material end for d in model.definitions next if d.image? for e in d.entities e.material = nil if e.respond_to?( :material ) && e.material == material e.back_material = nil if e.respond_to?( :back_material ) && e.back_material == material end end materials = model.materials if materials.respond_to?( :remove ) materials.remove( material ) else # Workaround for SketchUp versions older than 8.0M1. Add all materials # except the one to be removed to temporary groups and purge the materials. temp_group = model.entities.add_group for m in model.materials next if m == material g = temp_group.add_group g.material = material end materials.purge_unused temp_group.erase! true end end end # module
Renaming Materials
This is another feature which was not available until M1. Before Material.name=
was added the only way to “rename” a material was to recreate an identical material with the desired name and replace the original with the new. To clean up one had to apply the same dirty workaround for removing materials as described above. This method is very slow and inefficient, but unfortunately there was no alternative.
Speaking of Names…
The Material
class has two seemingly similar methods, #name
and #display_name
. The documentation says that #display_name
is preferred, but doesn’t explain why.
The difference is that if a material name is wrapped in square brackets, like "[MyMayerial]"
, then #name
will return "[MyMayerial]"
and display_name
will return "MyMayerial"
. The latter is how SketchUp will display the material name in the UI, while #name
returns the real name.
If you refer to a material by name, then you must use the string you get from #name
instead of #display_name
. model.materials[ material.display_name ]
will return nil
for materials wrapped in square brackets because the look-up failed.
So what the documentation should be saying is that #display_name
is preferred when you present the material name to the UI, while #name
must be used to look-up materials in the Materials
collection.
Material Thumbnails
Material.write_thumbnail
has a behaviour which limits it usability in some scenarios.
If you specify a dimension equal or larger to either the width or height of the original, then the method fails. In other words, the resolution argument must be 1px smaller than the smallest unit of either the length or width of the original texture.
This produces problems with textures with a big ratio between width and height. If you want to produce thumbnails that are max 128x128px and the material you generate the thumbnail for has a texture of 64x512px then it fails. As for that given texture the max size for the thumbnail would be 63px (1px smaller than smallest dimension.) – resulting in a thumbnail of 8×63.
For that reason the method should have ideally accepted width and height for the thumbnail in order to be truly usable. Alas. Hopefully this will be improved in a later version.
Hexadecimal Notation
The description of the Material
class mentions alternative methods of assigning materials to entities:
1
2
3 | face.material = mat1 face.material = "red" face.material = 0xff0000 |
face.material = mat1 face.material = "red" face.material = 0xff0000
Note the hexadecimal notation, it would indicate from the line before it that it produces a red material – as would people familiar with from HTML and CSS. But the reality is that it produces a blue material. The format SketchUp expects is 0xBBGGRR
.
Material.alpha vs Color.alpha
If you use a Color
object, or array, to assign a material then you might expect that the #alpha
value in the Color
object transfers to the Material
object. Instead the #alpha
value is ignored and you need to make the material transparent you must use Material.alpha=
– which takes a float value between 0.0
and 1.0
as oppose to Color
that take an integer 0-255
…
If you question the point of Color.alpha
– as of SU8 you can use View.draw
with transparent colours. Though you can wonder what the purpose was before that.
Bug with Materials and Color Alpha Channel
While making the graphics for this article I came across a Material
bug related to the alpha
channel in Color
.
As seen in the screenshot, the shadow cast from the colour swatch appear as though the swatch colours are transparent. The material’s alpha channel is opaque. But material.color.alpha
is 0
. I used the “Match Color on Screen” feature to pick a colour from a PNG image in an Image
entity.
If the material’s alpha
property is changed it overrides the color’s alpha
, but the effect is restored when the material alpha
is restored to fully opaque.
When any of the HSL properties is adjusted in the material editor the color’s alpha channel is corrected to be opaque.
It is possible to recreate the bug, via the Ruby API, by assigning the material with a color where the alpha channel is zero. This bug can be seen in SketchUp 6, 7 and 8. (No other versions tested.)
Current Material BugSplat Warning
When you use model.materials.current
there is a possibility of causing BugSplats. If the user has selected a material from the Material Library instead of picking a material that is already in the model and you try to apply that to an entity in SketchUp it will quickly BugSplat.
To avoid that you always need to check if model.material.current
is a material in the model before using it. Like so: materials.include?( materials.current )
An alternative is to attempt to re-create the current material – effectively adding it to the model yourself. The problem arise when the material contains a texture. If that material doesn’t refer to the texture with a path that is on the current computer the material needs to be extracted – and you cannot do that with the current API without the material being applied to some entity.
I wrote a method that will add the current material to the model if it doesn’t exist. It does have a caveat though, so read all the comments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | module MaterialsEx # @note Do not use within a start_operation block. This method uses a temporary # operation which will break any already initiated operations. # # @return [Sketchup::Material] def self.safely_get_current_material() model = Sketchup.active_model materials = model.materials m = materials.current return m if m.nil? # Check if material already exists. If it does - reuse it. name = m.name if x = materials[ name ] if x.color.to_i == m.color.to_i && x.alpha == m.alpha # No texture applied. if x.texture.nil? && m.texture.nil? materials.current = x return x end x_basename = File.basename( x.texture.filename ) m_basename = File.basename( m.texture.filename ) if m_basename == m.texture.filename # If the texture only have a filename - not a path. if x_basename == m.texture.filename && x.texture.width == m.texture.width && x.texture.height == m.texture.height materials.current = x return x end else # If the texture only have a filename with full path. if x.texture.filename == m.texture.filename && x.texture.width == m.texture.width && x.texture.height == m.texture.height materials.current = x return x end end else end end # Transfer name and colour. new_material = materials.add( name ) new_material.color = m.color new_material.alpha = m.alpha # Any textures require special attention. If the filename contains a valid # path to an existing file nothing special needs to be done. # # But if the filename refers to a non-existing file it needs to be written # out to a temp file. This is where things become a bit risky and hacky. # Because TextureWriter doesn't accept Material objects the orphan material # needs to be temporarily applied to the model (risky). # # Materials from SketchUp's default library only contains the name of the file # without any paths. This is because the texture is located only within the # .skm. if m.texture filename = m.texture.filename if File.exist?( filename ) new_material.texture = filename else # Create temp file to write the texture to. temp_path = File.expand_path( ENV['TMPDIR'] || ENV['TMP'] || ENV['TEMP'] ) temp_folder = File.join( temp_path, 'su_tmp_mtl' ) temp_filename = File.basename( filename ) temp_file = File.join( temp_folder, temp_filename ) unless File.exist?( temp_folder ) Dir.mkdir( temp_folder ) end # Create temp group with the orphan material and write it out. # # Wrap within start_operation and clean up with abort_operation so it # doesn't end up in the undo stack. # # (!) This means this method should not occur within any other # start_operation blocks - as operations cannot be nested. tw = Sketchup.create_texture_writer model.start_operation( 'Extract Material from Limbo' ) begin g = model.entities.add_group g.material = m tw.load( g ) tw.write( g, temp_file ) ensure model.abort_operation end # Load texture to material and clean up. new_material.texture = temp_file File.delete( temp_file ) end new_material.texture.size = [ m.texture.width, m.texture.height ] end materials.current = new_material new_material end end # module |
module MaterialsEx # @note Do not use within a start_operation block. This method uses a temporary # operation which will break any already initiated operations. # # @return [Sketchup::Material] def self.safely_get_current_material() model = Sketchup.active_model materials = model.materials m = materials.current return m if m.nil? # Check if material already exists. If it does - reuse it. name = m.name if x = materials[ name ] if x.color.to_i == m.color.to_i && x.alpha == m.alpha # No texture applied. if x.texture.nil? && m.texture.nil? materials.current = x return x end x_basename = File.basename( x.texture.filename ) m_basename = File.basename( m.texture.filename ) if m_basename == m.texture.filename # If the texture only have a filename - not a path. if x_basename == m.texture.filename && x.texture.width == m.texture.width && x.texture.height == m.texture.height materials.current = x return x end else # If the texture only have a filename with full path. if x.texture.filename == m.texture.filename && x.texture.width == m.texture.width && x.texture.height == m.texture.height materials.current = x return x end end else end end # Transfer name and colour. new_material = materials.add( name ) new_material.color = m.color new_material.alpha = m.alpha # Any textures require special attention. If the filename contains a valid # path to an existing file nothing special needs to be done. # # But if the filename refers to a non-existing file it needs to be written # out to a temp file. This is where things become a bit risky and hacky. # Because TextureWriter doesn't accept Material objects the orphan material # needs to be temporarily applied to the model (risky). # # Materials from SketchUp's default library only contains the name of the file # without any paths. This is because the texture is located only within the # .skm. if m.texture filename = m.texture.filename if File.exist?( filename ) new_material.texture = filename else # Create temp file to write the texture to. temp_path = File.expand_path( ENV['TMPDIR'] || ENV['TMP'] || ENV['TEMP'] ) temp_folder = File.join( temp_path, 'su_tmp_mtl' ) temp_filename = File.basename( filename ) temp_file = File.join( temp_folder, temp_filename ) unless File.exist?( temp_folder ) Dir.mkdir( temp_folder ) end # Create temp group with the orphan material and write it out. # # Wrap within start_operation and clean up with abort_operation so it # doesn't end up in the undo stack. # # (!) This means this method should not occur within any other # start_operation blocks - as operations cannot be nested. tw = Sketchup.create_texture_writer model.start_operation( 'Extract Material from Limbo' ) begin g = model.entities.add_group g.material = m tw.load( g ) tw.write( g, temp_file ) ensure model.abort_operation end # Load texture to material and clean up. new_material.texture = temp_file File.delete( temp_file ) end new_material.texture.size = [ m.texture.width, m.texture.height ] end materials.current = new_material new_material end end # module
I created and uploaded some kitchen furniture models using SU6. At the the time, the models were in fact photo-realistic, clear and an exact match to my own furnishings. Wanting to show the table and chairs to a friend, to promote SketchUp and explain the concept and importance of components vs. groups, I open the file and found the texture quality had gone to hell. It looks like a haze covers the whole and, before you ask, my “fog” option is not activated.
Does SU8 handle textures differently that SU6 or SU7? I had a pro license for the 6 and 7 versions but am now using the SU8 free. Though I haven’t tried since wiping out my system and installing OS X 10..7.3 on my iMac, I suspect that neither Pro 6 nor Pro 7 will work properly.
I am illiterate when it comes to using Terminal or creating or even understanding scripts/plug-ins.
In laymen terms, is this aforementioned information providing a fix for my issue? Is it possible that my own display is the cause of this “issue”?
What I believe you are referring to is the Texture Anti-Alising feature added in SketchUp 7. It affect the appearance of your texture which may or may not be desired. Best performance is with the feature enabled. It’s located in Window > Model Info.
I was wondering if I was the only one getting that bugsplat you mentioned at the end of this article. Totally annoying. I filed that bug months ago, nothing is fixed yet…
Yea, I’ve also reported it quite a while back. How did you run into it? Writing your new plugin? :)
Haha no I run into it every time I teach SketchUp, explaining about libraries… I make them add a custom library folder, click an SKM in that library, click an item in the model > Bugsplat!
Anyway since recently I make them add their materials to their model first.
hm… they selected a material from a library and then? What do they use to cause the splat? A plugin? Using the normal Paint Bucket tool should work fine.
Nope, it happens using the normal Paint Bucket tool. On all of our PCs. No special stuff. Although the error doesn’t occur when all plugins are disabled using the ruby magic line, so I guess there is some conflict there.
Yea, sound like a plugin interference.
Hm I found out which one… now notifying the creator of the plugin…
[…] Let SketchUp handle units […]