SketchUp / api-issue-tracker

Public issue tracker for the SketchUp and LayOut's APIs
https://developer.sketchup.com/
38 stars 10 forks source link

Access hidden materials like hidden definitions #180

Open Aerilius opened 6 years ago

Aerilius commented 6 years ago

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:

Sketchup.active_model.materials.length
> 1
Sketchup.active_model.materials.to_a.length
> 0
Sketchup.active_model.materials.to_a
> []
Sketchup.active_model.materials[0]
> #<Sketchup::Material:0x000000286bddc0>

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:

(0...Sketchup.active_model.materials.length).map{ |i| Sketchup.active_model.materials[i] }

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.

prachtan commented 6 years ago

Related to #12 and #66

thomthom commented 6 years ago

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|
}
thomthom commented 5 years ago

By the way @Aerilius - how do you imagine the desired behaviour to be? Concretely, how would the API look like?

Aerilius commented 5 years ago

In an ideal API, one could for example either have

Now that materials.each traverses less materials than materials.length it's difficult to think of a solution.

DanRathbun commented 5 years ago

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
thomthom commented 5 years ago

or a collection that iterates over all, but allows to distinguish by type (like model.definitions and definition.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.

DanRathbun commented 5 years ago

BUMP! The C API got access to ALL materials with 2019.2 release.

thomthom commented 5 years ago

For the record material.owner_type was added in SketchUp 2019.2:

http://ruby.sketchup.com/Sketchup/Material.html#owner_type-instance_method

DanRathbun commented 4 years ago

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 method public (inherited from class Module.)

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 is using 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

~