Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/glyphs.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Each glyph object in the legend supports these properties. `name` and `text` are
| `orientation` | `"fluid"` \| `"vertical"` | `"fluid"` | After rotation, `"vertical"` keeps text upright. |
| `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). |
| `flipy` | boolean | `false` | Mirror vertically. Same **`fluid`** vs **`vertical`** / text behaviour as `flipx`. |
| `nudge` | `{ dx, dy }` | — | Offset from centre in cell units; negative `dx`/`dy` move left/up. |
| `nudge` | `{ dx, dy, relativeTo? }` | — | Offset from centre in glyph-scaled units; negative `dx`/`dy` move left/up. `relativeTo: "glyph"` (default) uses the glyph's rotated/flipped axes. `relativeTo: "piece"` uses the enclosing piece or face axes, so upright text can stay beside a rotating piece. |
| `fontFamily` | string | — | CSS font family for text glyphs. |
| `fontWeight` | string \| number | — | CSS font weight for text glyphs. |

Expand Down
11 changes: 10 additions & 1 deletion src/renderers/_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,16 @@ export abstract class RendererBase {
if (g.nudge.dy !== undefined) {
dy = g.nudge.dy;
}
use.dmove(dx, dy);
if (g.nudge.relativeTo === "piece") {
// Translate in the enclosing piece/face axes after glyph rotation
// and flips. Keep the same scaled units as the legacy nudge.
const matrix = use.matrixify();
matrix.e += dx * factor;
matrix.f += dy * factor;
use.matrix(matrix);
} else {
use.dmove(dx, dy);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/schemas/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,10 @@ export interface Glyph {
* The number of units to nudge the glyph from centre.
*/
nudge?: {
/**
* The offset coordinate system. 'glyph' uses the glyph's rotated/flipped axes. 'piece' uses the enclosing piece (or isometric face) axes, allowing an upright label's position to rotate with its piece. Both use glyph-scaled units.
*/
relativeTo?: "glyph" | "piece";
/**
* Negative values move the glyph to the left.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,11 @@
"description": "The number of units to nudge the glyph from centre.",
"type": "object",
"properties": {
"relativeTo": {
"description": "The offset coordinate system. 'glyph' uses the glyph's rotated/flipped axes. 'piece' uses the enclosing piece (or isometric face) axes, allowing an upright label's position to rotate with its piece. Both use glyph-scaled units.",
"enum": ["glyph", "piece"],
"default": "glyph"
},
"dx": {
"description": "Negative values move the glyph to the left.",
"type": "number",
Expand Down
40 changes: 40 additions & 0 deletions test/glyphNudge.render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from "chai";
import { SVG, registerWindow, type Svg } from "@svgdotjs/svg.js";
import { createSVGWindow } from "svgdom";
import { render, type Glyph } from "../src/index.js";

describe("glyph nudge coordinate systems", () => {
for (const rotation of [0, 30, 90, 180, 270, 330]) {
for (const relativeTo of [undefined, "glyph", "piece"] as const) {
it(`offsets upright text relative to ${relativeTo ?? "legacy glyph"} at ${rotation}°`, () => {
const window = createSVGWindow();
registerWindow(window, window.document);
const draw = SVG(window.document.documentElement) as Svg;
const glyph: Glyph = {
text: "3", scale: 0.4, rotate: 30, flipx: true,
nudge: { dx: 100, dy: -50, relativeTo },
};
render({
board: { style: "squares", width: 2, height: 2, rotate: 90 },
legend: { A: glyph },
pieces: "A-\n--",
}, { target: draw, rotate: rotation });
const layer = draw.findOne("#A")!.findOne("use")!;
const matrix = layer.matrixify();
const board = draw.findOne("#board")!.matrixify();
const x = layer.x() + layer.width() / 2;
const y = layer.y() + layer.height() / 2;
const localX = matrix.a * x + matrix.c * y + matrix.e;
const localY = matrix.b * x + matrix.d * y + matrix.f;
const actualX = board.a * localX + board.c * localY;
const actualY = board.b * localX + board.d * localY;
// Legacy nudges follow the glyph transform (including its flip);
// piece nudges follow only the board and retain the glyph's scale.
const expectedLocalX = relativeTo === "piece" ? 40 : matrix.a * 100 - matrix.c * 50;
const expectedLocalY = relativeTo === "piece" ? -20 : matrix.b * 100 - matrix.d * 50;
expect(actualX).to.be.closeTo(board.a * expectedLocalX + board.c * expectedLocalY, 0.000001);
expect(actualY).to.be.closeTo(board.b * expectedLocalX + board.d * expectedLocalY, 0.000001);
});
}
}
});