Skip to content

Commit cf41351

Browse files
committed
Flipping now works correctly with rotation
1 parent 266ed8a commit cf41351

6 files changed

Lines changed: 974 additions & 7 deletions

File tree

docs/glyphs.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ Each glyph object in the legend supports these properties. `name` and `text` are
171171
| `opacity` | number | `1` | 0 (transparent) to 1 (opaque); applied to player-tagged fills and strokes. |
172172
| `rotate` | number \| null | `0` | Degrees, −360 to 360. Negative is counter-clockwise. |
173173
| `orientation` | `"fluid"` \| `"vertical"` | `"fluid"` | After rotation, `"vertical"` keeps text upright. |
174-
| `flipx` | boolean | `false` | Mirror horizontally. |
175-
| `flipy` | boolean | `false` | Mirror vertically. |
174+
| `flipx` | boolean | `false` | Mirror horizontally. With **`fluid`** (default sheet glyphs), flip is in piece/board coordinates and rotates with the board. With **`vertical`** or text glyphs, flip is relative to the **screen** (left/right stays fixed when `board.rotate` changes). |
175+
| `flipy` | boolean | `false` | Mirror vertically. Same **`fluid`** vs **`vertical`** / text behaviour as `flipx`. |
176176
| `nudge` | `{ dx, dy }` || Offset from centre in cell units; negative `dx`/`dy` move left/up. |
177177
| `fontFamily` | string || CSS font family for text glyphs. |
178178
| `fontWeight` | string \| number || CSS font weight for text glyphs. |

src/renderers/_base.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { unionPolys } from "../common/polys";
1212
import { hex2rgb, rgb2hex, afterOpacity, lighten } from "../common/colours";
1313
import { CompassDirection, edges2corners, getBoardFill, BoardReturn } from "../boards";
1414
import { cairoCatalan, cairoCollinear, cobweb, conhex, conicalHex, dvgc, fracturedFlat, hexOfCir, hexOfHex, hexOfTri, hexOfTriF, hexSlanted, moon, onyx, pentagonal, bentTri, star, pyramidHex, rectOfHex, rectOfTri, snubSquare, snubSquareCells, sowing, squares, squaresDiamonds, squaresStacked, stackingTriangles, vertex, wheel } from "../boards";
15-
import { isoFaceGlyphDrawSize, isoFaceGlyphPlacement, resolveGlyphRotationDegrees } from "./isometric/faceGlyphFit";
15+
import { isoFaceGlyphDrawSize, isoFaceGlyphPlacement, resolveGlyphFlipAxes, resolveGlyphRotationDegrees } from "./isometric/faceGlyphFit";
1616
import {
1717
computeAnnulusPlacement,
1818
computeSidebarPlacement,
@@ -1030,11 +1030,14 @@ export abstract class RendererBase {
10301030
size = factor * cellsize;
10311031
}
10321032

1033-
if (g.flipx !== undefined && g.flipx) {
1034-
use.flip("x");
1033+
const { flipx, flipy } = resolveGlyphFlipAxes(g, boardRotation, rotationOpts);
1034+
const flipOriginX = layout === "isoFace" ? layerIsoFaceX + layerIsoFaceDrawSize / 2 : 0;
1035+
const flipOriginY = layout === "isoFace" ? layerIsoFaceY + layerIsoFaceDrawSize / 2 : 0;
1036+
if (flipx) {
1037+
use.scale(-1, 1, flipOriginX, flipOriginY);
10351038
}
1036-
if (g.flipy !== undefined && g.flipy) {
1037-
use.flip("y");
1039+
if (flipy) {
1040+
use.scale(1, -1, flipOriginX, flipOriginY);
10381041
}
10391042

10401043
if (g.nudge !== undefined) {

src/renderers/isometric/faceGlyphFit.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,37 @@ export const resolveGlyphRotationDegrees = (
124124
return rotation;
125125
};
126126

127+
export type GlyphFlipAxes = { flipx: boolean; flipy: boolean };
128+
129+
/** Effective flip axes at bake time; upright flat glyphs swap at 90°/270° for screen-stable mirrors. */
130+
export const resolveGlyphFlipAxes = (
131+
g: Glyph,
132+
boardRotation: number,
133+
opts?: GlyphRotationOptions,
134+
): GlyphFlipAxes => {
135+
const flipx = g.flipx === true;
136+
const flipy = g.flipy === true;
137+
if (!flipx && !flipy) {
138+
return { flipx: false, flipy: false };
139+
}
140+
141+
const counterRotate = opts?.counterRotateWithBoard !== false;
142+
const stabilize =
143+
counterRotate &&
144+
glyphKeepsUpright(g) &&
145+
opts?.rotateFluidWithBoard !== true;
146+
147+
if (!stabilize) {
148+
return { flipx, flipy };
149+
}
150+
151+
const r = ((boardRotation % 360) + 360) % 360;
152+
if (r === 90 || r === 270) {
153+
return { flipx: flipy, flipy: flipx };
154+
}
155+
return { flipx, flipy };
156+
};
157+
127158
/**
128159
* Conservative shrink so a centered square in face UV stays inside a skewed cube face parallelogram.
129160
*/

test/faceGlyphFit.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
multiplyIsoAffine,
1111
projectedTopCenterMatrix,
1212
resolveFaceInset,
13+
resolveGlyphFlipAxes,
1314
resolveGlyphRotationDegrees,
1415
} from "../src/renderers/isometric/faceGlyphFit";
1516
import { genCube } from "../src/renderers/isometric/cubes";
@@ -121,4 +122,23 @@ describe("iso face glyph fit", () => {
121122
expect(resolveGlyphRotationDegrees({ name: "piecepack-number-1", colour: 1, orientation: "vertical" }, 90, { counterRotateWithBoard: true })).to.equal(-90);
122123
expect(resolveGlyphRotationDegrees({ name: "piecepack-number-1", colour: 1 }, 90, isoTop)).to.equal(90);
123124
});
125+
126+
it("should swap flip axes for upright legend glyphs at 90° and 270° board rotation", () => {
127+
const verticalFlip = { name: "arimaa-camel", colour: 1, orientation: "vertical" as const, flipx: true };
128+
const legendOpts = { counterRotateWithBoard: true, rotateFluidWithBoard: false };
129+
const isoTop = { counterRotateWithBoard: true, rotateFluidWithBoard: true };
130+
131+
expect(resolveGlyphFlipAxes(verticalFlip, 0, legendOpts)).to.deep.equal({ flipx: true, flipy: false });
132+
expect(resolveGlyphFlipAxes(verticalFlip, 180, legendOpts)).to.deep.equal({ flipx: true, flipy: false });
133+
expect(resolveGlyphFlipAxes(verticalFlip, 90, legendOpts)).to.deep.equal({ flipx: false, flipy: true });
134+
expect(resolveGlyphFlipAxes(verticalFlip, 270, legendOpts)).to.deep.equal({ flipx: false, flipy: true });
135+
136+
const fluidFlip = { name: "arimaa-camel", colour: 1, orientation: "fluid" as const, flipx: true };
137+
expect(resolveGlyphFlipAxes(fluidFlip, 90, legendOpts)).to.deep.equal({ flipx: true, flipy: false });
138+
139+
expect(resolveGlyphFlipAxes(verticalFlip, 90, isoTop)).to.deep.equal({ flipx: true, flipy: false });
140+
141+
const bothFlip = { name: "arimaa-camel", colour: 1, orientation: "vertical" as const, flipx: true, flipy: true };
142+
expect(resolveGlyphFlipAxes(bothFlip, 90, legendOpts)).to.deep.equal({ flipx: true, flipy: true });
143+
});
124144
});

0 commit comments

Comments
 (0)