## Sheared font - no Macros

Need help, or want to share a macro? Post here!
edwilliams16
Veteran
Posts: 1856
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

### Sheared font - no Macros

In the course of experimentation, I discovered it is possible to shear an object with a proper sequence of scalings and rotations. As far as I know, this would normally require a suitable macro to manipulate the object's transformation matrix.

The shear angle is controlled by the Dynamic data's Scale quantity ( one gives you 45 degrees, zero nothing.) The expressions are complicated, but you can copy them. Screenshot 2023-01-23 at 5.51.26 PM.png (16.2 KiB) Viewed 302 times Screenshot 2023-01-23 at 5.51.45 PM.png (14.85 KiB) Viewed 302 times
The Matrix API doesn't appear to provide a way of decomposing a matrix into a deformation (scale and shear) and Placement (base and rotation), but some web searching found a routine which I adapted.

Code: Select all

``````#BSD License https://github.com/matthew-brett/transforms3d/blob/main/LICENSE

import numpy as np
import math

def striu2mat(striu):
''' Construct shear matrix from upper triangular vector
Parameters
----------
striu : array, shape (N,)
vector giving triangle above diagonal of shear matrix.
Returns
-------
SM : array, shape (N, N)
shear matrix
Examples
--------
>>> S = [0.1, 0.2, 0.3]
>>> striu2mat(S)
array([[1. , 0.1, 0.2],
[0. , 1. , 0.3],
[0. , 0. , 1. ]])
>>> striu2mat()
array([[1., 1.],
[0., 1.]])
>>> striu2mat([1, 2])
Traceback (most recent call last):
...
ValueError: 2 is a strange number of shear elements
Notes
-----
Shear lengths are triangular numbers.
See http://en.wikipedia.org/wiki/Triangular_number
'''
n = len(striu)
# cached case
if n in _shearers:
N, inds = _shearers[n]
else: # General case
N = ((-1+math.sqrt(8*n+1))/2.0)+1 # n+1 th root
if N != math.floor(N):
raise ValueError('%d is a strange number of shear elements' %
n)
N = int(N)
inds = np.triu(np.ones((N,N)), 1).astype(bool)
M = np.eye(N)
M[inds] = striu
return M

def decompose44(A44):
''' Decompose 4x4 homogenous affine matrix into parts.
The parts are translations, rotations, zooms, shears.
This is the same as :func:`decompose` but specialized for 4x4 affines.
Decomposes `A44` into ``T, R, Z, S``, such that::
Smat = np.array([[1, S, S],
[0,    1, S],
[0,    0,    1]])
RZS = np.dot(R, np.dot(np.diag(Z), Smat))
A44 = np.eye(4)
A44[:3,:3] = RZS
A44[:-1,-1] = T
The order of transformations is therefore shears, followed by
zooms, followed by rotations, followed by translations.
This routine only works for shape (4,4) matrices
Parameters
----------
A44 : array shape (4,4)
Returns
-------
T : array, shape (3,)
Translation vector
R : array shape (3,3)
rotation matrix
Z : array, shape (3,)
Zoom vector.  May have one negative zoom to prevent need for negative
determinant R matrix above
S : array, shape (3,)
Shear vector, such that shears fill upper triangle above
diagonal to form shear matrix (type ``striu``).
Examples
--------
>>> T = [20, 30, 40] # translations
>>> R = [[0, -1, 0], [1, 0, 0], [0, 0, 1]] # rotation matrix
>>> Z = [2.0, 3.0, 4.0] # zooms
>>> S = [0.2, 0.1, 0.3] # shears
>>> # Now we make an affine matrix
>>> A = np.eye(4)
>>> Smat = np.array([[1, S, S],
...                  [0,    1, S],
...                  [0,    0,    1]])
>>> RZS = np.dot(R, np.dot(np.diag(Z), Smat))
>>> A[:3,:3] = RZS
>>> A[:-1,-1] = T # set translations
>>> Tdash, Rdash, Zdash, Sdash = decompose44(A)
>>> np.allclose(T, Tdash)
True
>>> np.allclose(R, Rdash)
True
>>> np.allclose(Z, Zdash)
True
>>> np.allclose(S, Sdash)
True
Notes
-----
The implementation inspired by:
*Decomposing a matrix into simple transformations* by Spencer
W. Thomas, pp 320-323 in *Graphics Gems II*, James Arvo (editor),
The upper left 3x3 of the affine consists of a matrix we'll call
RZS::
RZS = R * Z *S
where R is a rotation matrix, Z is a diagonal matrix of scalings::
Z = diag([sx, sy, sz])
and S is a shear matrix of form::
S = [[1, sxy, sxz],
[0,   1, syz],
[0,   0,   1]])
Running all this through sympy (see 'derivations' folder) gives
``RZS`` as ::
[R00*sx, R01*sy + R00*sx*sxy, R02*sz + R00*sx*sxz + R01*sy*syz]
[R10*sx, R11*sy + R10*sx*sxy, R12*sz + R10*sx*sxz + R11*sy*syz]
[R20*sx, R21*sy + R20*sx*sxy, R22*sz + R20*sx*sxz + R21*sy*syz]
``R`` is defined as being a rotation matrix, so the dot products between
the columns of ``R`` are zero, and the norm of each column is 1.  Thus
the dot product::
R[:,0].T * RZS[:,1]
that results in::
[R00*R01*sy + R10*R11*sy + R20*R21*sy + sx*sxy*R00**2 + sx*sxy*R10**2 + sx*sxy*R20**2]
simplifies to ``sy*0 + sx*sxy*1`` == ``sx*sxy``.  Therefore::
R[:,1] * sy = RZS[:,1] - R[:,0] * (R[:,0].T * RZS[:,1])
allowing us to get ``sy`` with the norm, and sxy with ``R[:,0].T *
RZS[:,1] / sx``.
Similarly ``R[:,0].T * RZS[:,2]`` simplifies to ``sx*sxz``, and
``R[:,1].T * RZS[:,2]`` to ``sy*syz`` giving us the remaining
unknowns.
'''
A44 = np.asarray(A44)
T = A44[:-1,-1]
RZS = A44[:-1,:-1]
# compute scales and shears
M0, M1, M2 = np.array(RZS).T
# extract x scale and normalize
sx = math.sqrt(np.sum(M0**2))
M0 /= sx
# orthogonalize M1 with respect to M0
sx_sxy = np.dot(M0, M1)
M1 -= sx_sxy * M0
# extract y scale and normalize
sy = math.sqrt(np.sum(M1**2))
M1 /= sy
sxy = sx_sxy / sx
# orthogonalize M2 with respect to M0 and M1
sx_sxz = np.dot(M0, M2)
sy_syz = np.dot(M1, M2)
M2 -= (sx_sxz * M0 + sy_syz * M1)
# extract z scale and normalize
sz = math.sqrt(np.sum(M2**2))
M2 /= sz
sxz = sx_sxz / sx
syz = sy_syz / sy
# Reconstruct rotation matrix, ensure positive determinant
Rmat = np.array([M0, M1, M2]).T
if np.linalg.det(Rmat) < 0:
sx *= -1
Rmat[:,0] *= -1
return T, Rmat, np.array([sx, sy, sz]), np.array([sxy, sxz, syz])

def decomposeAffine(matrix):
''' decomposeAffine(App.Matrix: matrix) ->
[App.Vector: translation,
App.Rotation: rotation
list: scale
[[],[],[]]: shear]
decomposes the matrix - order of operation  shear, scale, rotate then translate
'''
matlist = matrix.A
mat = np.array([matlist[0:4],matlist[4:8],matlist[8:12],matlist[12:16]])
T, R, Z, S = decompose44(mat)
translation = App.Vector(*T.tolist())
scale = Z.tolist()
shear = [[1, S, S], [0, 1, S], [0, 0, 1]]
return translation, rotation, scale, shear

sel =Gui.Selection.getSelectionEx('',0)
path = sel.SubElementNames
obj, matrix, shp = sel.Object.getSubObject(path, retType =2)
translation, rotation, scale, shearmatrix = decomposeAffine(matrix)
print(f'translation = {translation}\nRotation Axis {rotation.Axis} Angle {math.degrees(rotation.Angle)} deg\nscale {scale}\nshear {shearmatrix}')
if hasattr(shp, 'Point'):
print(f'vertex Global loc = {shp.Point}')``````
Select the object and run.
Attachments
shear.FCStd
onekk
Veteran
Posts: 4134
Joined: Sat Jan 17, 2015 7:48 am
Contact:

### Re: Sheared font - no Macros

Nice to note and keep around just in case.

Thank you.

Regards

Carlo D.