Open Aerilius opened 6 years ago
Related to #12 and #66
The image definitions and material should have been part of a separate collection. Because it's too easy to mess up the definition of an image by accident. If it's not a face with four edges and a material then SketchUp will misbehave, most likely crash.
And I've seen models were an image material have been assigned to a model entities, which also caused confusion and bad behaviour.
Though there are reasons to act upon image definition and materials, but it needs to be done consciously and with care.
Maybe (had the collections been created now, they could have looked something like this:
model.definitions.each_with_images { |d|
}
model.materials.each_with_images { |d|
}
By the way @Aerilius - how do you imagine the desired behaviour to be? Concretely, how would the API look like?
In an ideal API, one could for example either have
model.materials
and model.images
(or image_materials
)model.definitions
and definition.image?
, definition.group?
, definition.hidden?
)Now that materials.each
traverses less materials than materials.length
it's difficult to think of a solution.
materials.each_with_images
is a good idea (analog to Ruby's each_cons
, each_slice
) and there is Enumerable#to_a
, so with only a single method added to the API, one could get all other array methods by each_with_images.to_a
. But it is common that each
iterates over all elements (not less than one of the specialized iterators). And the naming "with" would make people expect a second yield parameter, maybe we can think of a better naming.model.image_materials
, and evaluate whether materials.length
can be changed to give only the number of non-image materials (to be consistent with material.each
).Should perhaps the Sketchup::Material
class have a query method to determine whether it is used solely (or additionally) as an image material ?
... and why does this class not have a #used?
query method ?
not_used = model.materials.collect {|matl| not matl.used? }
... how about a Sketchup::Materials#used_by
collection getter (with type filter argument like #grep
) ?
Ex:
face_mats = model.materials.used_by(Sketchup::Face)
face_mats.each do |matl|
matl.alpha= 1.0
end
or a collection that iterates over all, but allows to distinguish by type (like
model.definitions
anddefinition.image?
,definition.group?
,definition.hidden?
)
This pattern has been a problem, because it's easy to inadvertently modify image definitions - which SU never intend to happen. Preferably the API should have guarded against that by making requests for image definitions explicit.
Should perhaps the Sketchup::Material class have a query method to determine whether it is used solely (or additionally) as an image material ?
Good idea.
... and why does this class not have a #used? query method ?
Also a good idea. Though, this might be something the manager should know about. The child object shouldn't really have to know about its parent. (Ensuring loose coupling.)
... how about a Sketchup::Materials#used_by collection getter (with type filter argument like #grep) ?
I'm not sure of the internals allow for an efficient solution for that. I think we only have a ref count. Might end up having to traverse the entire model for this.
BUMP! The C API got access to ALL materials with 2019.2 release.
For the record material.owner_type
was added in SketchUp 2019.2:
http://ruby.sketchup.com/Sketchup/Material.html#owner_type-instance_method
The only way to traverse all materials is:
(0...Sketchup.active_model.materials.length).map{ |i| Sketchup.active_model.materials[i] }
Improvement IDEAS may be taken from this Ruby class refinement for SketchUp 2019.2 or higher.
NOTE: Yes I know it looks a bit weird to have an instance method named
"public"
, but it does not interfere with the class' private methodpublic
(inherited from classModule
.)The
public
method in the example below is an instance method accessible only when called upon an instance of a material, and only in a scope that isusing
the example refinement.
# Where outer modules ::Refined and ::Refined::Sketchup are previously defined.
module Refined
module Sketchup
end
end
# A usage for this refinement:
#
# module SomeAuthor::SomeExtension
#
# using Refined::Sketchup::Materials
#
# matls = Sketchup.active_model.materials
# if matls.count_image_materials > 0
# imatl = matls.internal_materials.find {|matl| matl.image? }
# matl = matls.add("Safe Material")
# matl.texture= imatl.texture.image_rep
# end
#
# end
#
module Refined::Sketchup::Materials
# Ruby class refinement for SketchUp 2019.2 or higher.
if Sketchup.version.to_f >= 19.2
refine ::Sketchup::Materials do
# @!group Filters
#{#
# Return the superset of all material instances (incl. manager, image
# and layer owned material instances.)
#
# @return [Array<Sketchup::Material>] An array of all material instances.
def all
# Because some coders are politicking for a change in the methods
# that return the sum of materials: #count, #length and #size, we
# should not then rely upon doing this:
# (0...self.size).map { |i| self[i] }
i = 0
ary = []
while (self[i] rescue nil) # IndexError if i is out of range
ary << self[i]
i += 1
end
return ary
end
# Return the subset of only image owned material instances.
# Internal materials are hidden from the Materials manager panel.
#
# WARNING! Assigning internal materials to {Sketchup::Drawingelement}
# objects may result in corrupted model files (in older SketchUp versions.)
# In versions >= 19.2 an {ArgumentError} will be raised if this is attempted.
#
# @return [Array<Sketchup::Material>] An array of only image owned materials.
def image_owned
all = self.all
all.select { |matl| matl.owner_type == ::Sketchup::Material::OWNER_IMAGE }
end
# Return the subset of only image and layer owned material instances.
# Internal materials are hidden from the Materials manager panel.
#
# WARNING! Assigning internal materials to {Sketchup::Drawingelement}
# objects may result in corrupted model files (in older SketchUp versions.)
# In versions >= 19.2 an {ArgumentError} will be raised if this is attempted.
#
# @return [Array<Sketchup::Material>] An array of only internal materials.
def internal
all = self.all
all.select { |matl| matl.owner_type != ::Sketchup::Material::OWNER_MANAGER }
end
# Return the subset of only layer owned material instances.
# Internal materials are hidden from the Materials manager panel.
#
# WARNING! Assigning internal materials to {Sketchup::Drawingelement}
# objects may result in corrupted model files (in older SketchUp versions.)
# In versions >= 19.2 an {ArgumentError} will be raised if this is attempted.
#
# @return [Array<Sketchup::Material>] An array of only layer materials.
def layer_owned
all = self.all
all.select { |matl| matl.owner_type == ::Sketchup::Material::OWNER_LAYER }
end
# Return the subset of only public (manager owned) material instances.
# These public materials are accessible for use by end users in the
# Materials manager to use in applying materials to drawing elements.
#
# (Internal materials are hidden from the Materials manager panel.)
#
# @return [Array<Sketchup::Material>] An array of only public materials.
def public
self.to_a
end
# @!endgroup
#}#
# @!group Iterators
#{#
# Iterate only the internal image owned materials in the collection.
# Internal materials are hidden from the Materials manager panel.
# This method returns an {Enumerator} if no block is given.
#
# @yieldparam material [Sketchup::Material] Each image owned material in turn.
# @yieldreturn [nil] (Nothing is done with block return values.)
# @return [Array<Sketchup::Material>] The array of only image materials.
#
# @see #each to iterate only public materials.
# @see #each_public to (create an Enumerator) or iterate only public materials.
# @see #each_internal to iterate only non-public materials.
# @see #each_layer_owned to iterate only layer materials.
# @see #every to iterate through every material regardless of ownership.
def each_image_owned
matls = self.image_owned
if !block_given?
return matls.each # an Enumerator object
else
for matl in matls
yield matl
end
return matls
end
end
# Iterate only the internal layer owned materials in the collection.
# Internal materials are hidden from the Materials manager panel.
# This method returns an {Enumerator} if no block is given.
#
# WARNING! The material property for {Sketchup::Layer} objects are not yet
# exposed in the API. Mofifying these internal materials might cause
# application instability, crash or corrupted model files. At this time (as
# of v2021.0,) it is strongly suggested to only access and modify a layer's
# material through it's {Layer#color} property getter and {Layer#color=}
# setter methods.
#
# WARNING! Assigning internal materials to {Sketchup::Drawingelement}
# objects may result in corrupted model files (in older SketchUp versions.)
# In versions >= 19.2 an {ArgumentError} will be raised if this is attempted.
#
# @yieldparam material [Sketchup::Material] Each layer owned material in turn.
# @yieldreturn [nil] (Nothing is done with block return values.)
# @return [Array<Sketchup::Material>] The array of only layer materials.
#
# @see #each to iterate only public materials.
# @see #each_public to (create an Enumerator) or iterate only public materials.
# @see #each_image_owned to iterate only image materials.
# @see #each_internal to iterate only non-public materials.
# @see #every to iterate through every material regardless of ownership.
def each_layer_owned
matls = self.layer_owned
if !block_given?
return matls.each # an Enumerator object
else
for matl in matls
yield matl
end
return matls
end
end
# Iterate only the internal materials in the collection.
# Internal materials are hidden from the Materials manager panel.
# This method returns an {Enumerator} if no block is given.
#
# @yieldparam material [Sketchup::Material] Each internal material in turn.
# @yieldreturn [nil] (Nothing is done with block return values.)
# @return [Array<Sketchup::Material>] The array of only internal materials.
#
# @see #each to iterate only public materials. (No Enumerator support)
# @see #each_public to iterate only public materials.
# @see #each_image_owned to iterate only image owned materials.
# @see #each_layer_owned to iterate only layer materials.
# @see #every to iterate through every material regardless of ownership.
def each_internal
matls = self.internal
if !block_given?
return matls.each # an Enumerator object
else
for matl in matls
yield matl
end
return matls
end
end
# Iterate only the subset of public (manager owned) materials. Similar to
# {#each} except this method returns an {Enumerator} if no block is given.
# Internal materials are hidden from the Materials manager panel.
#
# @yieldparam material [Sketchup::Material] Each public material in turn.
# @yieldreturn [nil] (Nothing is done with block return values.)
# @return [Array<Sketchup::Material>] The array of only public materials.
#
# @see #each to iterate only public materials. (No Enumerator support)
# @see #each_image_owned to iterate only image owned materials.
# @see #each_internal to iterate only non-public materials.
# @see #each_layer_owned to iterate only layer materials.
# @see #every to iterate through every material regardless of ownership.
def each_public
matls = self.public
if !block_given?
return matls.each # an Enumerator object
else
for matl in matls
yield matl
end
return matls
end
end
# Iterate over every material in the materials collection (including
# manager, image and layer owned material instances.)
# This method returns an {Enumerator} if no block is given.
#
# Use {Sketchup::Material#owner_type} to determine the owner of a material.
#
# @yieldparam material [Sketchup::Material] Each and every material in turn.
# @yieldreturn [nil] (Nothing is done with block return values.)
# @return [Array<Sketchup::Material>] The array of all materials.
#
# @see #each to iterate only public materials. (No Enumerator support)
# @see #each_public to iterate only public materials.
# @see #each_image_owned to iterate only image owned materials.
# @see #each_internal to iterate only non-public materials.
# @see #each_layer_owned to iterate only layer materials.
def every
matls = self.all
if !block_given?
return matls.each # an Enumerator object
else
for matl in matls
yield matl
end
return matls
end
end
# @!endgroup
#}#
# @!group Quantifiers
#{#
# Return a count for the subset of only image owned material instances.
# Internal image owned materials are hidden from the Materials manager panel.
#
# @return [Integer] The sum of image owned materials.
def count_image_owned
self.all.count { |matl|
matl.owner_type == ::Sketchup::Material::OWNER_IMAGE
}
end
# Return a count for the subset of only image and layer owned material
# instances. Internal materials are hidden from the Materials manager panel.
#
# @return [Integer] The sum of image and layer owned materials.
def count_internal
self.all.count { |matl|
matl.owner_type != ::Sketchup::Material::OWNER_MANAGER
}
end
# Return a count for the subset of only layer owned material instances.
# Internal layer owned materials are hidden from the Materials manager panel.
#
# WARNING! The material property for {Sketchup::Layer} objects are not yet
# exposed in the API. Mofifying these internal materials might cause
# application instability, crash or corrupted model files. At this time (as
# of v2021.0,) it is strongly suggested to only access and modify a layer's
# material through it's {Layer#color} property getter and {Layer#color=}
# setter methods.
#
# @return [Integer] The sum of layer owned materials.
def count_layer_owned
self.all.count { |matl|
matl.owner_type == ::Sketchup::Material::OWNER_LAYER
}
end
# Return a count for the subset of only public (manager owned) material
# instances.
#
# @return [Integer] The sum of public manager owned materials.
def count_public
self.all.count { |matl|
matl.owner_type == ::Sketchup::Material::OWNER_MANAGER
}
end
# @!endgroup
#}#
end # of class refinement
refine ::Sketchup::Material do
# @!group Searching
#{#
# Returns the first image instance found that use this material.
# There may be multiple instances if the image was copied.
#
# @return [Sketchup::Image,nil>] The image using this material if found.
# `nil` if the image cannot be found or this is not an image material.
#
# @see #find_images to get an array of multiple images instances.
def find_image
return nil unless self.image?
self.find_images.first
end
# Returns the array of image instances that use this material.
# There may be multiple instances if the image was copied.
#
# @return [Array<Sketchup::Image>] The array of found image instances.
#
# @see #find_image to get the first instance or `nil` if not found.
def find_images
return [] unless self.image?
idef = self.model.definitions.find { |cdef|
next false unless cdef.image?
face = cdef.entities.grep(Sketchup::Face).first
next false unless face
face.material == self || face.back_material == self
}
idef ? idef.instances : []
end
# @!endgroup
#}#
# @!group Type Queries
#{#
# Determine if a material is internally owned by an image object.
# Internal materials are hidden from the Materials manager panel.
#
# @return [Boolean] True for an image owned material, false otherwise.
def image?
self.owner_type == ::Sketchup::Material::OWNER_IMAGE
end
# Determine if a material is internal (ie, owned by a layer or image.)
# Internal materials are hidden from the Materials manager panel.
#
# @return [Boolean] True for an internal material, false otherwise.
def internal?
self.owner_type != ::Sketchup::Material::OWNER_MANAGER
end
# Determine if a material is internally owned by an layer object.
# Internal materials are hidden from the Materials manager panel.
#
# WARNING! The material property for {Sketchup::Layer} objects are not yet
# exposed in the API. Mofifying these internal materials might cause
# application instability, crash or corrupted model files. At this time (as
# of v2021.0,) it is strongly suggested to only access and modify a layer's
# material through it's {Layer#color} property getter and {Layer#color=}
# setter methods.
#
# @return [Boolean] True for an layer owned material, false otherwise.
def layer?
self.owner_type == ::Sketchup::Material::OWNER_LAYER
end
# Determine if a material is listed in the Materials manager panel.
# These public materials are accessible to end users for their use in
# applying to drawing elements.
#
# @return [Boolean] True for a public manager material, false otherwise.
def public?
self.owner_type == ::Sketchup::Material::OWNER_MANAGER
end
# @!endgroup
#}#
end # of class refinement
end # version conditional
end # refinement module
~
Issue:
Currently, a model can contain Sketchup::Material objects that are 1. hidden from the materials browser GUI and 2. hidden from the Sketchup::Materials collection (!).
When you import an image entity (image instance of a hidden ComponentDefinition containing a face with textured material) into an empty model, the material is not accessible:
This is a contradiction! And like
to_a
, all other Enumerable methods on the Sketchup::Materials collection traverse only the visible materials. The only way to traverse all materials is:Use case:
Traversing all image files that are embedded in a model in order to optimize their file size.
Request
The inconsistency should be resolved because it can cause errors in extensions that use the Enumerable methods and
length
and assume they are consistent.Apart from that, there should be a proper way to access hidden materials, as well as to distinguish them from visible materials, e.g. like component definition's
hidden?
method.