diff --git a/lib/drawGraphicsToCanvas.ts b/lib/drawGraphicsToCanvas.ts
index 997f8bb..8c2bd45 100644
--- a/lib/drawGraphicsToCanvas.ts
+++ b/lib/drawGraphicsToCanvas.ts
@@ -498,7 +498,17 @@ export function drawGraphicsToCanvas(
ctx.textAlign = alignMap[anchor]
ctx.textBaseline = baselineMap[anchor]
- ctx.fillText(text.text, projected.x, projected.y)
+ if (text.rotation) {
+ // Text.rotation is counter-clockwise in graphics (y-up) space; canvas
+ // rotate() is clockwise in screen (y-down) space, so negate.
+ ctx.save()
+ ctx.translate(projected.x, projected.y)
+ ctx.rotate((-text.rotation * Math.PI) / 180)
+ ctx.fillText(text.text, 0, 0)
+ ctx.restore()
+ } else {
+ ctx.fillText(text.text, projected.x, projected.y)
+ }
})
}
diff --git a/lib/getPngBufferFromGraphicsObject.ts b/lib/getPngBufferFromGraphicsObject.ts
index 9997e8b..724ba1a 100644
--- a/lib/getPngBufferFromGraphicsObject.ts
+++ b/lib/getPngBufferFromGraphicsObject.ts
@@ -504,6 +504,11 @@ export async function getPngBufferFromGraphicsObject(
fontSize: (text.fontSize ?? 12) * strokeScale,
color: text.color || "black",
anchorAlignment: text.anchorSide ?? "center",
+ // Text.rotation is counter-clockwise in graphics (y-up) space; the
+ // raster context rotates clockwise in screen (y-down) space, so negate.
+ rotationRadians: text.rotation
+ ? (-text.rotation * Math.PI) / 180
+ : undefined,
})
}
diff --git a/lib/getSvgFromGraphicsObject.ts b/lib/getSvgFromGraphicsObject.ts
index d625306..20aed0f 100644
--- a/lib/getSvgFromGraphicsObject.ts
+++ b/lib/getSvgFromGraphicsObject.ts
@@ -68,10 +68,26 @@ function getBounds(graphics: GraphicsObject): Bounds {
const { dx, dy } = offsetMap[anchor]
const x0 = t.x + dx
const y0 = t.y + dy
+ if (!t.rotation) {
+ return [
+ { x: x0, y: y0 },
+ { x: x0 + width, y: y0 + height },
+ ]
+ }
+ // Rotate the text box's corners about the anchor so rotated text still
+ // contributes its true extents to the bounds.
+ const rad = (t.rotation * Math.PI) / 180
+ const cos = Math.cos(rad)
+ const sin = Math.sin(rad)
return [
{ x: x0, y: y0 },
+ { x: x0 + width, y: y0 },
+ { x: x0, y: y0 + height },
{ x: x0 + width, y: y0 + height },
- ]
+ ].map((corner) => ({
+ x: t.x + (corner.x - t.x) * cos - (corner.y - t.y) * sin,
+ y: t.y + (corner.x - t.x) * sin + (corner.y - t.y) * cos,
+ }))
}),
]
@@ -616,6 +632,13 @@ export function getSvgFromGraphicsObject(
"font-family": "sans-serif",
"text-anchor": alignMap[anchor],
"dominant-baseline": baselineMap[anchor],
+ // Text.rotation is counter-clockwise in graphics (y-up) space;
+ // SVG rotate() is clockwise in screen (y-down) space, so negate.
+ ...(txt.rotation
+ ? {
+ transform: `rotate(${-txt.rotation}, ${projected.x}, ${projected.y})`,
+ }
+ : {}),
},
children: [{ type: "text", value: txt.text }],
}
diff --git a/lib/types.ts b/lib/types.ts
index de2d493..6ece1e2 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -91,6 +91,11 @@ export interface Text {
anchorSide?: NinePointAnchor
color?: string
fontSize?: number
+ /**
+ * Rotation in degrees, counter-clockwise about the anchor point in graphics
+ * (y-up) space. 90 makes the text read bottom-to-top.
+ */
+ rotation?: number
layer?: string
step?: number
}
diff --git a/tests/__snapshots__/getSvgFromGraphicsObject-rotated-texts.snap.svg b/tests/__snapshots__/getSvgFromGraphicsObject-rotated-texts.snap.svg
new file mode 100644
index 0000000..61499bf
--- /dev/null
+++ b/tests/__snapshots__/getSvgFromGraphicsObject-rotated-texts.snap.svg
@@ -0,0 +1,44 @@
+
\ No newline at end of file
diff --git a/tests/getSvgFromGraphicsObject.test.ts b/tests/getSvgFromGraphicsObject.test.ts
index 3458ea5..4753f1d 100644
--- a/tests/getSvgFromGraphicsObject.test.ts
+++ b/tests/getSvgFromGraphicsObject.test.ts
@@ -347,6 +347,33 @@ describe("getSvgFromGraphicsObject", () => {
expect(svg).toMatchSvgSnapshot(import.meta.path, "texts")
})
+ test("rotates text about its anchor", () => {
+ const input: GraphicsObject = {
+ texts: [
+ {
+ x: 0,
+ y: 0,
+ text: "SPI_SCK",
+ rotation: 90,
+ },
+ {
+ x: 2,
+ y: 0,
+ text: "FLAT",
+ },
+ ],
+ }
+
+ const svg = getSvgFromGraphicsObject(input)
+ // rotation is CCW in graphics space -> negated for SVG's screen space
+ expect(svg).toMatch(/transform="rotate\(-90, [\d.]+, [\d.]+\)"/)
+ // unrotated text carries no transform
+ const flatText = svg.split(" chunk.includes("FLAT"))
+ expect(flatText).toBeDefined()
+ expect(flatText!).not.toContain("transform=")
+ expect(svg).toMatchSvgSnapshot(import.meta.path, "rotated-texts")
+ })
+
test("respects text anchorSide", () => {
const input: GraphicsObject = {
texts: [