Skip to content
Merged
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
6 changes: 2 additions & 4 deletions lib/getCanvasObjectLabelAtPoint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getArrowGeometry } from "./arrowHelpers"
import { getRectCorners } from "./rectGeometry"
import { getProjectedRectGeometry } from "./rectGeometry"
import type {
Arrow,
Circle,
Expand Down Expand Up @@ -186,9 +186,7 @@ const isRectHit = (
matrix: Matrix,
hitSlop: number,
) => {
const corners = getRectCorners(rect).map((point) =>
projectPoint(matrix, point),
)
const corners = getProjectedRectGeometry(rect, matrix).corners

return (
isPointInPolygon(screenPoint, corners) ||
Expand Down
33 changes: 31 additions & 2 deletions lib/rectGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ export const getRectRotationRadians = (
return ((rect.ccwRotationDegrees ?? 0) * Math.PI) / 180
}

const getScreenRotationMultiplier = (matrix: Matrix) => {
// Screen coordinates have +Y downward. Orientation-preserving transforms
// therefore need the math angle inverted to look visually CCW.
const determinant = matrix.a * matrix.d - matrix.b * matrix.c
return determinant < 0 ? 1 : -1
}

export const getRectRotationRadiansForMatrix = (
rect: Pick<Rect, "ccwRotationDegrees">,
matrix: Matrix,
) => {
return getRectRotationRadians(rect) * getScreenRotationMultiplier(matrix)
}
const rotatePoint = (point: XYPoint, angleRadians: number): XYPoint => {
const cos = Math.cos(angleRadians)
const sin = Math.sin(angleRadians)
Expand All @@ -25,10 +38,26 @@ const rotatePoint = (point: XYPoint, angleRadians: number): XYPoint => {

export const getRectCorners = (
rect: Pick<Rect, "center" | "width" | "height" | "ccwRotationDegrees">,
): Point[] => {
return getRectCornersWithAngle(rect, getRectRotationRadians(rect))
}

export const getRectCornersForMatrix = (
rect: Pick<Rect, "center" | "width" | "height" | "ccwRotationDegrees">,
matrix: Matrix,
): Point[] => {
return getRectCornersWithAngle(
rect,
getRectRotationRadiansForMatrix(rect, matrix),
)
}

const getRectCornersWithAngle = (
rect: Pick<Rect, "center" | "width" | "height" | "ccwRotationDegrees">,
angleRadians: number,
): Point[] => {
const halfWidth = rect.width / 2
const halfHeight = rect.height / 2
const angleRadians = getRectRotationRadians(rect)

return [
{ x: -halfWidth, y: -halfHeight },
Expand Down Expand Up @@ -69,7 +98,7 @@ export const getProjectedRectGeometry = (
rect: Pick<Rect, "center" | "width" | "height" | "ccwRotationDegrees">,
matrix: Matrix,
) => {
const corners = getRectCorners(rect).map((point) =>
const corners = getRectCornersForMatrix(rect, matrix).map((point) =>
applyToPoint(matrix, point),
)
const center = applyToPoint(matrix, rect.center)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ export const InteractiveGraphics = ({
})

const filterRects = useFilterRects({
realToScreen,
isPointOnScreen,
doesLineIntersectViewport,
filterLayerAndStep,
Expand Down
15 changes: 12 additions & 3 deletions site/components/InteractiveGraphics/hooks/useFilterRects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getRectCornersForMatrix } from "lib/rectGeometry"
import { useMemo } from "react"
import { getRectCorners } from "lib/rectGeometry"
import type { Matrix } from "transformation-matrix"

type Rect = {
center: { x: number; y: number }
Expand All @@ -11,6 +12,7 @@ type Rect = {
}

type UseFilterRectsParams = {
realToScreen: Matrix
isPointOnScreen: (point: { x: number; y: number }) => boolean
doesLineIntersectViewport: (
p1: { x: number; y: number },
Expand All @@ -20,6 +22,7 @@ type UseFilterRectsParams = {
}

export const useFilterRects = ({
realToScreen,
isPointOnScreen,
doesLineIntersectViewport,
filterLayerAndStep,
Expand All @@ -31,7 +34,8 @@ export const useFilterRects = ({

// For rectangles, check if any corner or the center is visible
const { center } = rect
const [topLeft, topRight, bottomRight, bottomLeft] = getRectCorners(rect)
const [topLeft, topRight, bottomRight, bottomLeft] =
getRectCornersForMatrix(rect, realToScreen)

// Check if any corner or center is visible
if (
Expand All @@ -52,5 +56,10 @@ export const useFilterRects = ({
doesLineIntersectViewport(bottomLeft, topLeft)
)
}
}, [isPointOnScreen, doesLineIntersectViewport, filterLayerAndStep])
}, [
realToScreen,
isPointOnScreen,
doesLineIntersectViewport,
filterLayerAndStep,
])
}
67 changes: 34 additions & 33 deletions tests/SVGRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { describe, expect, test, beforeAll } from "bun:test"
import { act } from "react"
import { createRoot } from "react-dom/client"
import SVGRenderer from "../site/components/SVGRenderer"
import "bun-match-svg"
import * as jsdom from "jsdom"
import { getSvgsFromLogString } from "../lib"

declare global {
var IS_REACT_ACT_ENVIRONMENT: boolean
}

// Setup global DOM environment
beforeAll(() => {
const { JSDOM } = jsdom
Expand All @@ -13,10 +18,11 @@ beforeAll(() => {
global.document = dom.window.document
global.window = dom.window as unknown as Window & typeof globalThis
global.navigator = dom.window.navigator
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})

describe("SVGRenderer", () => {
test("snapshot matches expected SVG output", () => {
test("snapshot matches expected SVG output", async () => {
const container = document.createElement("div")
document.body.appendChild(container)

Expand All @@ -25,38 +31,33 @@ describe("SVGRenderer", () => {

const root = createRoot(container)

return new Promise<void>((resolve) => {
// Render the SVGs
root.render(
graphics.length > 0 ? (
<div className="space-y-8">
{graphics.map(({ title, svg }, index) => (
<SVGRenderer key={index} title={title} svg={svg} />
))}
</div>
) : null,
)
try {
await act(async () => {
// Render the SVGs
root.render(
graphics.length > 0 ? (
<div className="space-y-8">
{graphics.map(({ title, svg }, index) => (
<SVGRenderer key={index} title={title} svg={svg} />
))}
</div>
) : null,
)
})

// Use setTimeout to allow rendering to complete
setTimeout(() => {
try {
const renderedSvgs = container.querySelectorAll("svg")
expect(renderedSvgs.length).toBeGreaterThan(0)
const firstRenderedSvg = renderedSvgs[0]?.outerHTML
// Perform snapshot test
expect(firstRenderedSvg).toMatchSvgSnapshot(
import.meta.path,
"renderer-graphics",
)
root.unmount()
document.body.removeChild(container)
resolve()
} catch (error) {
root.unmount()
document.body.removeChild(container)
throw error
}
}, 50)
})
const renderedSvgs = container.querySelectorAll("svg")
expect(renderedSvgs.length).toBeGreaterThan(0)
const firstRenderedSvg = renderedSvgs[0]?.outerHTML
// Perform snapshot test
expect(firstRenderedSvg).toMatchSvgSnapshot(
import.meta.path,
"renderer-graphics",
)
} finally {
await act(async () => {
root.unmount()
})
document.body.removeChild(container)
}
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/getCanvasObjectLabelAtPoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe("getCanvasObjectLabelAtPoint", () => {
getCanvasObjectLabelAtPoint(graphics, identityMatrix, { x: 5, y: 5 }),
).toBe("Rotated rect")
expect(
getCanvasObjectLabelAtPoint(graphics, identityMatrix, { x: 8, y: -8 }),
getCanvasObjectLabelAtPoint(graphics, identityMatrix, { x: 8, y: 8 }),
).toBeNull()
})

Expand Down
50 changes: 50 additions & 0 deletions tests/rectGeometry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test"
import type { Matrix } from "transformation-matrix"
import { getProjectedRectGeometry } from "../lib/rectGeometry"
import type { Rect } from "../lib/types"

const screenMatrix = {
a: 1,
b: 0,
c: 0,
d: 1,
e: 0,
f: 0,
} as Matrix

const cartesianMatrix = {
a: 1,
b: 0,
c: 0,
d: -1,
e: 0,
f: 0,
} as Matrix

describe("rectGeometry", () => {
test("renders positive ccwRotationDegrees as visually counterclockwise on screen transforms", () => {
const rect: Rect = {
center: { x: 0, y: 0 },
width: 10,
height: 4,
ccwRotationDegrees: 30,
}

const projected = getProjectedRectGeometry(rect, screenMatrix)
expect(projected.angleDegrees).toBeCloseTo(-30)
expect(projected.corners[1]!.y).toBeLessThan(projected.corners[0]!.y)
})

test("keeps positive ccwRotationDegrees visually counterclockwise with cartesian y-flip transforms", () => {
const rect: Rect = {
center: { x: 0, y: 0 },
width: 10,
height: 4,
ccwRotationDegrees: 30,
}

const projected = getProjectedRectGeometry(rect, cartesianMatrix)
expect(projected.angleDegrees).toBeCloseTo(-30)
expect(projected.corners[1]!.y).toBeLessThan(projected.corners[0]!.y)
})
})
Loading