Uv_tile_at question

Hello!
I’m making a simple script that lets me create a keyboard short that finds all the faces in my selection and rotates their UV texture coordinates 45 degrees. This saves me a lot of time.

I have it working great for square textures, but with anything rectangular it skews the image. I don’t think it’s the math that is incorrect, as I did some trouble shooting and used the same math to rotate a geometric face/rectangle as intended.

I’m wondering if their something in the way the UV coordinates are handled that I am missing?The pseudo script is simpilifed as: I use the uv_tile_at method to get the 8 points, modify the 4 UV points (by 45 degrees) and then reapply the material position. Happy to share my script if that’s allowed.

Thanks so much

In fact, it’s a “must” if you want meaningful help… :wink:
(I moved you topic into the right category, where you can also see how many millions of script has been shared… you may even find an answer with a properly initiated search. Personally I do not have experience with UVs)

Would be also nice if you can fill in your forum profile, than we can see which version of SketchUp you are using.

Thanks! Appreciate the advice (I updated my profile, but I am using 2023).
I copied my code below. Any criticism/tips are very welcome.

   ANGLE = Math::PI / 4

   def self.turnTexture
        MODEL.start_operation("Rotate_Textures", true)
        # creates an array with only faces that have texture coordinates
        faces = MODEL.selection.select { |entity| entity.typename == 'Face' && entity.material&.texture != nil  }
        faces.to_a unless faces.length < 0
        # iterates through each face to
        # 1  pull UV coordinates
        # 2  change those coordinates by 45 degrees with Math
        # 3  import those new coords into the face for material modification
        faces.each do |face|
          material = face.material
          reference = face.vertices.first.position
          current_mapping = face.uv_tile_at(reference, true)
          on_front = true
          new_uv_mapping = rotate_UV_Coordinates_By_Degrees(current_mapping)
          face.position_material(material, new_uv_mapping, on_front)
        end
        MODEL.commit_operation
      end

      private

      def self.rotate_UV_Coordinates_By_Degrees(positions)
        # Extract Coordinates for each point
        old_points = positions.select.with_index { |_, index| index.odd? }.map(&:to_a)
        x1, y1 = old_points[0]
        x4, y4 = old_points[3]
        # calculate midpoint coordinates
        mid_x = (x1 + x4) / 2.0
        mid_y = (y1 + y4) / 2.0
        new_uv_points = []
        # perform rotation for each point and reassign them ad 3dPoints
        new_uv_points = old_points.map { |x, y|
          translated_x = x - mid_x
          translated_y = y - mid_y
          rotated_x = (translated_x * Math.cos(ANGLE)) - (translated_y * Math.sin(ANGLE)) + mid_x
          rotated_y = (translated_x * Math.sin(ANGLE)) + (translated_y * Math.cos(ANGLE)) + mid_y
          Geom::Point3d.new(rotated_x, rotated_y, 0).tap { |point| new_uv_points << point }
          [rotated_x, rotated_y]
        }
        # Convert new points to 8 Pair UV Coordinates
        positions.each_with_index { |point, index| positions[index] = new_uv_points[index / 2] unless index.even? }
        positions
      end

does that make sense?

MODEL and ANGLE should not be constants.

Thanks for responding Dan!
Would a class variable be more appropriate? ie @@angle

Yes, if you’re transforming UVQ positions then you have to somehow deal with the texture.image_height and texture.image_width when they are not equal.

Here’s some example code where we rotate the the vertex positions instead of the UVQs and then calculate new UVQ values from the previous mapping.

And a file to apply the code to.
rotate texture.skp (123.3 KB)

# grab the first face
p face = Sketchup.active_model.active_entities.grep(Sketchup::Face).first

# 45 degree rotate 
vector = Geom::Vector3d.new(0, 0, 1)
angle = 45.degrees # Return 45 degrees in radians.
tr = Geom::Transformation.rotation(ORIGIN, vector, angle) # * tr
  
# Get the UVHelper
tw = Sketchup.create_texture_writer
uvHelp = face.get_UVHelper(true, true, tw)


# For each Vertex.position
# - rotate the Vertex by 45 degress around the ORIGIN
# - collect the original vertex position and the new UVQ
mapping = []
mapping = face.vertices.collect { | v |
  pos = v.position
  pos_new = pos.transform(tr)
  uvq = uvHelp.get_front_UVQ(pos_new)
  [pos, uvq]
}
 
mapping.flatten!
face.position_material(face.material, mapping, true)

nil

First of all we hope that you are wrapping your code in a unique top-level namespace module and your extension in a submodule of this namespace module.

There are reasons to use a @@var and reasons to use a @var. The main reason to use a variable is that the value will change and it will be used so many places that passing it around from method to method would make the code more difficult to maintain.

A local constant should only be used if the value will not change. This will most often not be true for a model reference, especially on the Mac where multiple models can be open at the same time.

Lastly a local reference would be preferred if the reference is only to be used within a single method. When the method ends and returns all local references go “out of scope” and are marked for garbage collection.


Okay, so let us look at your use of these references.

MODEL is only used within the turnTexture method, and not within a loop, so really it should be a local reference and the first line :

model = SketchUp.active_model

ANGLE is also only used within the second method, but within loop nested in another loop. I think again a local reference defined in the first method before the outer loop, and then passed into the second method as a parameter. Ex:

   def self.turnTexture
        angle = Math::PI / 4

… and within the loop:

          new_uv_mapping = rotate_UV_Coordinates_By_Degrees(current_mapping, angle)

And then add the angle parameter to the second method definition:

     def self.rotate_UV_Coordinates_By_Degrees(positions, angle)

… and change all ANGLE to use the angle reference.

You might even go further and create reusable references outside the old_points mapping loop …

        cos = Math.cos(ANGLE)
        sin = Math.sin(ANGLE)
        new_uv_points = []

… so you are not repeatedly calling those methods inside the loop.


Other tidbits

In Ruby the convention is to use all lower case letters for method names and separate words with an underscore character. Ie: turn_texture() rather than turnTexture(). The latter looks like VB or JavaScript. But it’s not enforced rigidly by the interpreter.

In the first method:

        faces = MODEL.selection.select { |entity| entity.typename == 'Face' && entity.material&.texture != nil  }

… can also be:

        faces = model.selection.grep(Sketchup::Face).select { |face| face.material&.texture }

Basically avoid using Entity#typename comparisons as they are slow. Grepping by class identifier is fast.
Even using entity.is_a?(Sketchup::Face) within a block is faster than comparing a typename string.

Also, you need not call BasicObject#!=() to compare against nil as in Ruby only nil and false evaluate Falsely. Everything else including 0, empty strings and empty arrays, etc. evaluate Truthy. Therefore, in a Boolean expression, any object reference to a Sketchup::Material or a Sketchup::Texture will eval as true.

1 Like

Thank you both for the quick and detailed response!
@dezmo I had looked at the get_UVHelper and wondered if that was the right direction, I’ll play around with this!
@DanRathbun Thanks for spending the time writing that critique, incredibly helpful, I really appreciate it!

2 Likes

Works! Thanks @dezmo and @DanRathbun
Incredibly helpful advice

**Almost Working Still have to work on it

1 Like

Hey Joe,
I’m not sure if you’d like hints, you seem to be more than capable of sorting this out on your own.

But for the benefit of the general public I threw together a diagram of the what the example code is doing each time it is invoked.

2 Likes

Hints are great! thank you,
For face (ie a circle) with more than 4 points, I had to slice the mapping array to 8 values for it to work properly.
The only issue I’m working out now is when I attempt to rotate a texture on a vertical face (y,z) it scales the texture. I think I need to get the correct values for the Transformation class per face in this bit of code

vector = Geom::Vector3d.new(0,0,1)
tr = Geom::Transformation.rotation(ORIGIN, vector, angle)

Score!

You want to rotate around a vector that is perpendicular to the face. This is called the Normal to the face and is part of the Ruby API Face.normal

Also, you don’t need the textureWriter and Back Face in the uvHelper

# Get the UVHelper
uvHelp = face.get_UVHelper(true, false)

JW-RotateTextures.rbz (777.0 KB)

Works as intended! Thanks so much @sWilliams, appreciate the time

3 Likes

This was helpful, thank-you for sharing lots of useful information and coding recommendations.