Inkscape.org
Creating New Extensions Shape unexpectedly moves after setting the node 'transform' property
  1. #1
    marksluser marksluser @marksluser
    *

    I am executing and modifying the following Inkscape extension https://github.com/jdhoek/inkscape-isometric-projection

    However, after I set a node's 'transform' property, the shape moves, even though there is a value of 0 in the translate x or translate y.
    For example, using the following translation matrix will change a 2D shape to a isometric top view:

            #   * scale vertically by cos(30°)
            #   * shear horizontally by -30°
            #   * rotate clock-wise 30°
            'to_top1':       [[cos_30,       -cos_30,    0],
                             [sin_30,       sin_30,     0]],

     

    Which is equivalent to a matrix of

    [0.866025 -0.866025  0 ]
    [0.5       0.5       0 ]
    [0         0         1 ]

    However, when running this code the shape will unexpectedly to move (translate) quite some distance from the original shape.

        tr = inkex.transforms.Transform(0.866025 0.5 -0.866025 0.5 0 0)
        node.set('transform', str(tr))

     

    I am using Inkscape 1.2 (dc2aedaf03, 2022-05-15) on Windows 10 desktop computer.

     

    Here is my extension files for reference:

    <?xml version="1.0" encoding="UTF-8"?>
    <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
      <name>Isometric Projection</name>
      <id>nl.jeroenhoek.inkscape.filter.isometric_projection_tool</id>
      <dependency type="executable" location="extensions">isometric_projection.py</dependency>
      <param name="conversion" type="optiongroup" gui-text="Convert flat projection to">
        <option value="top1">Isometric top side rotate CW</option>
        <option value="top2">Isometric top side rotate CCW</option>
        <option value="left">Isometric left-hand side</option>
        <option value="right">Isometric right-hand side</option>
      </param>
      <param name="reverse" type="bool" gui-text="Reverse transformation">false</param>
      <effect>
        <object-type>all</object-type>
        <effects-menu>
           <submenu _name="Axonometric Projection"/>
        </effects-menu>
      </effect>
      <script>
        <command reldir="extensions" interpreter="python">isometric_projection.py</command>
      </script>
    </inkscape-extension>
    
    

     

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import math
    import sys
    import inkex
    from inkex.transforms import Transform
    
    sys.path.append('/usr/share/inkscape/extensions')
    inkex.localization.localize()
    
    
    class IsometricProjectionTools(inkex.Effect):
        """
        Convert a flat 2D projection to one of the three visible sides in an
        isometric projection, and vice versa.
        """
    
        attrTransformCenterX = inkex.addNS('transform-center-x', 'inkscape')
        attrTransformCenterY = inkex.addNS('transform-center-y', 'inkscape')
    
        # Precomputed values for sine, cosine, and tangent of 30°.
        rad_30 = math.radians(30)
        cos_30 = math.cos(rad_30)
        sin_30 = 0.5  # No point in using math.sin for 30°.
        tan_30 = math.tan(rad_30)
    
        # Combined affine transformation matrices. The bottom row of these 3×3
        # matrices is omitted; it is always [0, 0, 1].
        transformations = {
            # From 2D to isometric top down view:
            #   * scale vertically by cos(30°)
            #   * shear horizontally by -30°
            #   * rotate clock-wise 30°
            'to_top1':       [[cos_30,       -cos_30,    0],
                             [sin_30,       sin_30,     0]],
    
            # From 2D to isometric top down view:
            #   * scale vertically by cos(30°)
            #   * shear horizontally by 30°
            #   * rotate clock-wise -30°
            'to_top2':       [[cos_30,       cos_30,    0],
                             [-sin_30,       sin_30,     0]],
    
            # From 2D to isometric left-hand side view:
            #   * scale horizontally by cos(30°)
            #   * shear vertically by -30°
            'to_left':      [[cos_30,       0,          0],
                             [sin_30,       1,          0]],
    
            # From 2D to isometric right-hand side view:
            #   * scale horizontally by cos(30°)
            #   * shear vertically by 30°
            'to_right':     [[cos_30,       0,          0],
                             [-sin_30,      1,          0]],
    
            # From isometric top down view to 2D:
            #   * rotate clock-wise -30°
            #   * shear horizontally by 30°
            #   * scale vertically by 1 / cos(30°)
            'from_top1':     [[tan_30,       1,          0],
                             [-tan_30,      1,          0]],
    
            # From isometric top down view to 2D:
            #   * rotate clock-wise 30°
            #   * shear horizontally by -30°
            #   * scale vertically by 1 / cos(30°)
            'from_top2':     [[tan_30,       -1,          0],
                             [tan_30,      1,          0]],
    
            # From isometric left-hand side view to 2D:
            #   * shear vertically by 30°
            #   * scale horizontally by 1 / cos(30°)
            'from_left':    [[1 / cos_30,   0,          0],
                             [-tan_30,      1,          0]],
    
            # From isometric right-hand side view to 2D:
            #   * shear vertically by -30°
            #   * scale horizontally by 1 / cos(30°)
            'from_right':   [[1 / cos_30,   0,          0],
                             [tan_30,       1,          0]]
        }
    
        def __init__(self):
            """
            Constructor.
            """
    
            inkex.Effect.__init__(self)
    
            self.arg_parser.add_argument(
                '-c', '--conversion',
                dest='conversion', default='top',
                help='Conversion to perform: (top|left|right)')
            self.arg_parser.add_argument(
                '-r', '--reverse',
                dest='reverse', default="false",
                help='Reverse the transformation from isometric projection ' +
                'to flat 2D')
    
        def getTransformCenter(self, midpoint, node):
            """
            Find the transformation center of an object. If the user set it
            manually by dragging it in Inkscape, those coordinates are used.
            Otherwise, an attempt is made to find the center of the object's
            bounding box.
            """
    
            c_x = node.get(self.attrTransformCenterX)
            c_y = node.get(self.attrTransformCenterY)
    
            # Default to dead-center.
            if c_x is None:
                c_x = 0.0
            else:
                c_x = float(c_x)
            if c_y is None:
                c_y = 0.0
            else:
                c_y = float(c_y)
    
            x = midpoint[0] + c_x
            y = midpoint[1] - c_y
    
            return [x, y]
    
        def translateBetweenPoints(self, tr, here, there):
            """
            Add a translation to a matrix that moves between two points.
            """
    
            x = there[0] - here[0]
            y = there[1] - here[1]
            tr.add_translate(x, y)
    
        def moveTransformationCenter(self, node, midpoint, center_new):
            """
            If a transformation center is manually set on the node, move it to
            match the transformation performed on the node.
            """
    
            c_x = node.get(self.attrTransformCenterX)
            c_y = node.get(self.attrTransformCenterY)
    
            if c_x is not None:
                x = str(center_new[0] - midpoint[0])
                node.set(self.attrTransformCenterX, x)
            if c_y is not None:
                y = str(midpoint[1] - center_new[1])
                node.set(self.attrTransformCenterY, y)
    
        def effect(self):
            """
            Apply the transformation. If an element already has a transformation
            attribute, it will be combined with the transformation matrix for the
            requested conversion.
            """
    
            if self.options.reverse == "true":
                conversion = "from_" + self.options.conversion
            else:
                conversion = "to_" + self.options.conversion
    
            if len(self.svg.selected) == 0:
                inkex.errormsg(_("Please select an object to perform the " +
                                 "isometric projection transformation on."))
                return
    
            # Default to the flat 2D to isometric top down view conversion if an
            # invalid identifier is passed.
            effect_matrix = self.transformations.get(
                conversion, self.transformations.get('to_top'))
    
            for id, node in self.svg.selected.items():
    
                inkex.utils.debug("x,y: " + str(node.get('x')) + ","+ str(node.get('y')))
    
                bbox = node.bounding_box()
                midpoint = [bbox.center_x, bbox.center_y]
                position = [bbox.left, bbox.top]
                #position = [float(node.get('x')), float(node.get('y'))]
                inkex.utils.debug("bbox position :" + str(position))
                center_old = self.getTransformCenter(midpoint, node)
                tr = Transform(node.get("transform"))
                #inkex.utils.debug("transform orig:" + str(tr))
                tr_efct = Transform(effect_matrix)
                inkex.utils.debug("transform efct:" + str(tr_efct))
                tr = tr @ tr_efct
                inkex.utils.debug("transform rslt:" + str(tr))
    
                # Compute the location of the transformation center after applying
                # the transformation matrix.
                center_new = center_old[:]
                #Transform(matrix).apply_to_point(center_new)
                tr.apply_to_point(center_new)
                tr.apply_to_point(midpoint)
    
                # Add a translation transformation that will move the object to
                # keep its transformation center in the same place.
                self.translateBetweenPoints(tr, center_new, center_old)
                inkex.utils.debug("transform between points:" + str(tr))
    
                # For some reason, applying a transform with 
                #no translate x or translate y components 
                #is causing the shape to move (translate)
                node.set('transform', str(tr))
                
                # Adjust the transformation center.
                #self.moveTransformationCenter(node, midpoint, center_new)
    
                #Try to move the shape back to the original position
                #With the following code, however it does not work
                bbox2 = node.bounding_box()
                midpoint2 = [bbox2.center_x, bbox2.center_y]
                position2 = [bbox.left, bbox.top]
                #position2 = [float(node.get('x')), float(node.get('y'))]
                inkex.utils.debug("bbox2 position :" + str(position2))
                diff_x = position[0] - position2[0]
                diff_y = position[1] - position2[1]
                inkex.utils.debug("position diff:" + str([diff_x,diff_y]))
    
                node.set('x', position[0])
                node.set('y', position[1])
    
    
    
    # Create effect instance and apply it.
    effect = IsometricProjectionTools()
    effect.run()
    

     

    References 

    Can someone explain why this is happening ?

    Thank you

     

  2. #2
    inklinea inklinea @inklinea⛰️

    I think using set, will completely overwrite the transform attribute.

    However, does the node already have a transform ? 

    inkex.errormsg(node.transform) should tell you the:

    Perhaps node.transform = node.transform @ tr

  3. #3
    AllanK AllanK @AllanK

    I believe the rotation is centered at the origin (0,0)..  Because the transform includes rotation, it can spin the object around the origin. I too am working on a version of the isometric extension.  I would attach my version, but don't know how to attach or publish it.  It's too long to post.

  4. #4
    AllanK AllanK @AllanK

    Attached is my current version of the isometric code.  It's doing most of what I want, but is a work in progress.

Inkscape Inkscape.org Inkscape Forum Creating New Extensions Shape unexpectedly moves after setting the node 'transform' property