Fix handling of multiple texmod transforms

This commit is contained in:
Lars Mueller 2023-05-27 14:24:54 +02:00
parent e07d646203
commit 8e5bdc817c
3 changed files with 109 additions and 68 deletions

@ -123,35 +123,106 @@ function texmod:invert(channels --[[set with keys "r", "g", "b", "a"]])
end
function texmod:flip(flip_axis --[["x" or "y"]])
assert(flip_axis == "x" or flip_axis == "y")
return new{
type = "transform",
base = self,
flip_axis = flip_axis
}
return self:transform(assert(
(flip_axis == "x" and "fx")
or (flip_axis == "y" and "fy")
or (not flip_axis and "i")))
end
function texmod:rotate(deg)
assert(deg % 90 == 0)
deg = deg % 360
assert(deg % 90 == 0, "only multiples of 90° supported")
return new{
type = "transform",
base = self,
rotation_deg = deg
}
return self:transform(("r%d"):format(deg))
end
-- First flip, then rotate counterclockwise
function texmod:transform(flip_axis, rotation_deg)
assert(flip_axis == nil or flip_axis == "x" or flip_axis == "y")
rotation_deg = (rotation_deg or 0) % 360
assert(rotation_deg % 90 == 0, "only multiples of 90° supported")
-- D4 group transformations (see https://proofwiki.org/wiki/Definition:Dihedral_Group_D4),
-- represented using indices into a table of matrices
-- TODO (...) try to come up with a more elegant solution
do
-- Matrix multiplication for composition: First applies a, then b <=> b * a
local function mat_2x2_compose(a, b)
local a_1_1, a_1_2, a_2_1, a_2_2 = unpack(a)
local b_1_1, b_1_2, b_2_1, b_2_2 = unpack(b)
return {
a_1_1 * b_1_1 + a_2_1 * b_1_2, a_1_2 * b_1_1 + a_2_2 * b_1_2;
a_1_1 * b_2_1 + a_2_1 * b_2_2, a_1_2 * b_2_1 + a_2_2 * b_2_2
}
end
local r90 ={
0, -1;
1, 0
}
local fx = {
-1, 0;
0, 1
}
local fy = {
1, 0;
0, -1
}
local r180 = mat_2x2_compose(r90, r90)
local r270 = mat_2x2_compose(r180, r90)
local fxr90 = mat_2x2_compose(fx, r90)
local fyr90 = mat_2x2_compose(fy, r90)
local transform_mats = {[0] = {1, 0; 0, 1}, r90, r180, r270, fx, fxr90, fy, fyr90}
local transform_idx_by_name = {i = 0, r90 = 1, r180 = 2, r270 = 3, fx = 4, fxr90 = 5, fy = 6, fyr90 = 7}
-- Lookup tables for getting the flipped axis / rotation angle
local flip_by_idx = {
[4] = "x",
[5] = "x",
[6] = "y",
[7] = "y",
}
local rot_by_idx = {
[1] = 90,
[2] = 180,
[3] = 270,
[5] = 90,
[7] = 90,
}
local idx_by_mat_2x2 = {}
local function transform_idx(mat)
-- note: assumes mat[i] in {-1, 0, 1}
return mat[1] + 3*(mat[2] + 3*(mat[3] + 3*mat[4]))
end
for i = 0, 7 do
idx_by_mat_2x2[transform_idx(transform_mats[i])] = i
end
-- Compute a multiplication table
local composition_idx = {}
local function ij_idx(i, j)
return i*8 + j
end
for i = 0, 7 do
for j = 0, 7 do
composition_idx[ij_idx(i, j)] = assert(idx_by_mat_2x2[
transform_idx(mat_2x2_compose(transform_mats[i], transform_mats[j]))])
end
end
function texmod:transform(...)
if select("#", ...) == 0 then return self end
local idx = ...
if type(idx) == "string" then
idx = assert(transform_idx_by_name[idx:lower()])
end
local base = self
if self.type == "transform" then
-- Merge with a `^[transform` base image
assert(transform_mats[idx])
base = self.base
idx = composition_idx[ij_idx(self.idx, idx)]
end
assert(transform_mats[idx])
if idx == 0 then return base end -- identity
return new{
type = "transform",
base = self,
rotation_deg = rotation_deg ~= 0 and rotation_deg or nil,
flip_axis = flip_axis
}
base = base,
idx = idx,
-- Redundantly store this information for convenience. Do not modify!
flip_axis = flip_by_idx[idx],
rotation_deg = rot_by_idx[idx] or 0,
}:transform(select(2, ...))
end
end
function texmod:verticalframe(framecount, frame)

@ -79,34 +79,25 @@ function pr.invert(r)
end
do
local transforms = {
[0] = {},
{rotation_deg = 90},
{rotation_deg = 180},
{rotation_deg = 270},
{flip_axis = "x"},
{flip_axis = "x", rotation_deg = 90},
{flip_axis = "y"},
{flip_axis = "y", rotation_deg = 90},
}
function pr.transform(r)
-- Note: While it isn't documented, `[transform` is indeed case-insensitive.
if r:match_charset"[iI]" then
return
return pr.transform(r)
end
local idx = r:match_charset"[0-7]"
if idx then
return tonumber(idx), pr.transform(r)
end
local flip_axis
if r:match_charset"[fF]" then
flip_axis = assert(r:match_charset"[xXyY]", "axis expected"):lower()
local flip_axis = assert(r:match_charset"[xXyY]", "axis expected")
return "f" .. flip_axis, pr.transform(r)
end
local rot_deg
if r:match_charset"[rR]" then
rot_deg = r:int()
local deg = r:match_str"%d"
-- Be strict here: Minetest won't recognize other ways to write these numbers (or other numbers)
assert(deg == "90" or deg == "180" or deg == "270")
return ("r%d"):format(deg), pr.transform(r)
end
if flip_axis or rot_deg then
return flip_axis, rot_deg
end
local transform = assert(transforms[r:int()], "out of range")
return transform.flip_axis, transform.rotation_deg
-- return nothing, we're done
end
end

@ -51,28 +51,7 @@ function pw:invert(w)
end
function pw:transform(w)
local rot_deg, flip_axis = self.rotation_deg or 0, self.flip_axis
if rot_deg == 0 and flip_axis == nil then
w.str"I"
elseif rot_deg == 0 then
w.str(assert(({x = "FX", y = "FY"})[flip_axis]))
elseif flip_axis == nil then
w.str"R"; w.int(self.rotation_deg)
elseif rot_deg == 90 then
w.str(assert(({x = "FX", y = "FY"})[flip_axis]))
w.str"R90"
elseif rot_deg == 180 then
-- Rotating by 180° is equivalent to flipping both axes;
-- if one axis was already flipped, that is undone -
-- thus it is equivalent to flipping the other axis
w.str(assert(({x = "FY", y = "FX"})[flip_axis]))
elseif rot_deg == 270 then
-- Rotating by 270° is equivalent to first rotating by 180°, then rotating by 90°;
-- first flipping an axis and then rotating by 180°
-- is equivalent to flipping the other axis as shown above
w.str(assert(({x = "FY", y = "FX"})[flip_axis]))
w.str"R90"
end
w.int(self.idx)
end
function pw:verticalframe(w)