From 1008102deb0722329d35aebaa602df7dd596dcbf Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Thu, 26 Jun 2025 22:03:48 -0700 Subject: [PATCH 1/2] feat: integrate text rendering components --- lib/drawGraphicsToCanvas.ts | 11 ++++ lib/getSvgFromGraphicsObject.ts | 35 +++++++++-- lib/index.ts | 1 + lib/mergeGraphics.ts | 1 + lib/translateGraphics.ts | 4 ++ lib/types.ts | 10 ++++ .../InteractiveGraphics.tsx | 21 ++++++- site/components/InteractiveGraphics/Text.tsx | 37 ++++++++++++ .../InteractiveGraphics/hooks/index.ts | 1 + .../hooks/useFilterTexts.ts | 19 ++++++ site/components/InteractiveGraphics/index.ts | 1 + site/utils/getGraphicsBounds.ts | 12 ++++ site/utils/getGraphicsFilteredByStep.ts | 3 + site/utils/getMaxStep.ts | 9 ++- .../utils/getTableItemsFromGraphicsObjects.ts | 10 ++++ tests/__snapshots__/cartesian-rect.snap.svg | 8 ++- tests/__snapshots__/rectangles.snap.svg | 4 +- tests/__snapshots__/texts.snap.svg | 59 +++++++++++++++++++ tests/getSvgFromGraphicsObject.test.ts | 19 ++++++ tests/mergeGraphics.test.ts | 6 ++ tests/translateGraphics.test.ts | 2 + 21 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 site/components/InteractiveGraphics/Text.tsx create mode 100644 site/components/InteractiveGraphics/hooks/useFilterTexts.ts create mode 100644 tests/__snapshots__/texts.snap.svg diff --git a/lib/drawGraphicsToCanvas.ts b/lib/drawGraphicsToCanvas.ts index a39e322..df851b5 100644 --- a/lib/drawGraphicsToCanvas.ts +++ b/lib/drawGraphicsToCanvas.ts @@ -79,6 +79,7 @@ export function getBounds(graphics: GraphicsObject): Viewbox { { x: circle.center.x, y: circle.center.y - circle.radius }, // top { x: circle.center.x, y: circle.center.y + circle.radius }, // bottom ]), + ...(graphics.texts || []).map((text) => text.position), ] if (points.length === 0) { @@ -290,6 +291,16 @@ export function drawGraphicsToCanvas( }) } + // Draw texts + if (graphics.texts && graphics.texts.length > 0) { + graphics.texts.forEach((text) => { + const projected = applyToPoint(matrix, text.position) + ctx.fillStyle = text.color || "black" + ctx.font = `${text.fontSize ?? 12}px sans-serif` + ctx.fillText(text.text, projected.x, projected.y) + }) + } + // Restore the original transform ctx.restore() } diff --git a/lib/getSvgFromGraphicsObject.ts b/lib/getSvgFromGraphicsObject.ts index 189f2c5..0093ab5 100644 --- a/lib/getSvgFromGraphicsObject.ts +++ b/lib/getSvgFromGraphicsObject.ts @@ -183,12 +183,12 @@ export function getSvgFromGraphicsObject( name: "polyline", type: "element", attributes: { - "data-points": line.points.map((p) => `${p.x},${p.y}`).join(" "), - "data-type": "line", - "data-label": line.label || "", - points: projectedPoints + "data-points": line.points .map((p) => `${p.x},${p.y}`) .join(" "), + "data-type": "line", + "data-label": line.label || "", + points: projectedPoints.map((p) => `${p.x},${p.y}`).join(" "), fill: "none", stroke: line.strokeColor || "black", "stroke-width": (line.strokeWidth @@ -202,7 +202,9 @@ export function getSvgFromGraphicsObject( }), }, }, - ...(shouldRenderLabel("lines") && line.label && projectedPoints.length > 0 + ...(shouldRenderLabel("lines") && + line.label && + projectedPoints.length > 0 ? [ { name: "text", @@ -300,6 +302,29 @@ export function getSvgFromGraphicsObject( }, } }), + // Texts + ...(graphics.texts || []).map((txt) => { + const projected = projectPoint( + { x: txt.position.x, y: txt.position.y }, + matrix, + ) + return { + name: "text", + type: "element", + attributes: { + "data-type": "text", + "data-label": txt.text, + "data-x": txt.position.x.toString(), + "data-y": txt.position.y.toString(), + x: projected.x.toString(), + y: projected.y.toString(), + fill: txt.color || "black", + "font-size": (txt.fontSize ?? 12).toString(), + "font-family": "sans-serif", + }, + children: [{ type: "text", value: txt.text }], + } + }), // Crosshair lines and coordinates (initially hidden) { name: "g", diff --git a/lib/index.ts b/lib/index.ts index e370448..ebcc6ba 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -13,6 +13,7 @@ export type { Line, Rect, Circle, + Text, GraphicsObject, Viewbox, CenterViewbox, diff --git a/lib/mergeGraphics.ts b/lib/mergeGraphics.ts index 4596901..bbf3dcd 100644 --- a/lib/mergeGraphics.ts +++ b/lib/mergeGraphics.ts @@ -10,5 +10,6 @@ export const mergeGraphics = ( points: [...(graphics1.points ?? []), ...(graphics2.points ?? [])], lines: [...(graphics1.lines ?? []), ...(graphics2.lines ?? [])], circles: [...(graphics1.circles ?? []), ...(graphics2.circles ?? [])], + texts: [...(graphics1.texts ?? []), ...(graphics2.texts ?? [])], } } diff --git a/lib/translateGraphics.ts b/lib/translateGraphics.ts index 74f1c22..41a5f38 100644 --- a/lib/translateGraphics.ts +++ b/lib/translateGraphics.ts @@ -24,5 +24,9 @@ export function translateGraphics( ...circle, center: { x: circle.center.x + dx, y: circle.center.y + dy }, })), + texts: graphics.texts?.map((text) => ({ + ...text, + position: { x: text.position.x + dx, y: text.position.y + dy }, + })), } } diff --git a/lib/types.ts b/lib/types.ts index 216e6ce..5972f06 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -39,11 +39,21 @@ export interface Circle { label?: string } +export interface Text { + position: { x: number; y: number } + text: string + color?: string + fontSize?: number + layer?: string + step?: number +} + export interface GraphicsObject { points?: Point[] lines?: Line[] rects?: Rect[] circles?: Circle[] + texts?: Text[] coordinateSystem?: "cartesian" | "screen" title?: string } diff --git a/site/components/InteractiveGraphics/InteractiveGraphics.tsx b/site/components/InteractiveGraphics/InteractiveGraphics.tsx index dcf497b..cb84705 100644 --- a/site/components/InteractiveGraphics/InteractiveGraphics.tsx +++ b/site/components/InteractiveGraphics/InteractiveGraphics.tsx @@ -15,6 +15,7 @@ import { Line } from "./Line" import { Point } from "./Point" import { Rect } from "./Rect" import { Circle } from "./Circle" +import { Text } from "./Text" import { getGraphicsBounds } from "site/utils/getGraphicsBounds" import { useIsPointOnScreen, @@ -23,6 +24,7 @@ import { useFilterPoints, useFilterRects, useFilterCircles, + useFilterTexts, } from "./hooks" import { DimensionOverlay } from "../DimensionOverlay" import { getMaxStep } from "site/utils/getMaxStep" @@ -30,7 +32,7 @@ import { ContextMenu } from "./ContextMenu" import { Marker, MarkerPoint } from "./Marker" export type GraphicsObjectClickEvent = { - type: "point" | "line" | "rect" | "circle" + type: "point" | "line" | "rect" | "circle" | "text" index: number object: any } @@ -60,6 +62,7 @@ export const InteractiveGraphics = ({ ...(graphics.lines?.map((l) => l.layer!).filter(Boolean) ?? []), ...(graphics.rects?.map((r) => r.layer!).filter(Boolean) ?? []), ...(graphics.points?.map((p) => p.layer!).filter(Boolean) ?? []), + ...(graphics.texts?.map((t) => t.layer!).filter(Boolean) ?? []), ]), ) const maxStep = getMaxStep(graphics) @@ -309,6 +312,7 @@ export const InteractiveGraphics = ({ realToScreen, size, ) + const filterTexts = useFilterTexts(isPointOnScreen, filterLayerAndStep) const filterAndLimit = ( objects: T[] | undefined, @@ -337,12 +341,17 @@ export const InteractiveGraphics = ({ () => filterAndLimit(graphics.circles, filterCircles), [graphics.circles, filterCircles, objectLimit], ) + const filteredTexts = useMemo( + () => filterAndLimit(graphics.texts, filterTexts), + [graphics.texts, filterTexts, objectLimit], + ) const totalFilteredObjects = filteredLines.length + filteredRects.length + filteredPoints.length + - filteredCircles.length + filteredCircles.length + + filteredTexts.length const isLimitReached = objectLimit && totalFilteredObjects > objectLimit return ( @@ -461,6 +470,14 @@ export const InteractiveGraphics = ({ interactiveState={interactiveState} /> ))} + {filteredTexts.map((txt) => ( + + ))} `${x.toFixed(2)}, ${y.toFixed(2)}`} width={size.width} diff --git a/site/components/InteractiveGraphics/Text.tsx b/site/components/InteractiveGraphics/Text.tsx new file mode 100644 index 0000000..f1c4c04 --- /dev/null +++ b/site/components/InteractiveGraphics/Text.tsx @@ -0,0 +1,37 @@ +import type * as Types from "lib/types" +import { applyToPoint } from "transformation-matrix" +import type { InteractiveState } from "./InteractiveState" + +export const Text = ({ + textObj, + interactiveState, + index, +}: { + textObj: Types.Text + interactiveState: InteractiveState + index: number +}) => { + const { realToScreen, onObjectClicked } = interactiveState + const { position, text, color, fontSize } = textObj + const screenPos = applyToPoint(realToScreen, position) + + return ( +
+ onObjectClicked?.({ type: "text", index, object: textObj }) + } + > + {text} +
+ ) +} diff --git a/site/components/InteractiveGraphics/hooks/index.ts b/site/components/InteractiveGraphics/hooks/index.ts index 532ccc4..197623d 100644 --- a/site/components/InteractiveGraphics/hooks/index.ts +++ b/site/components/InteractiveGraphics/hooks/index.ts @@ -7,3 +7,4 @@ export { useFilterLines } from "./useFilterLines" export { useFilterPoints } from "./useFilterPoints" export { useFilterRects } from "./useFilterRects" export { useFilterCircles } from "./useFilterCircles" +export { useFilterTexts } from "./useFilterTexts" diff --git a/site/components/InteractiveGraphics/hooks/useFilterTexts.ts b/site/components/InteractiveGraphics/hooks/useFilterTexts.ts new file mode 100644 index 0000000..b17640c --- /dev/null +++ b/site/components/InteractiveGraphics/hooks/useFilterTexts.ts @@ -0,0 +1,19 @@ +import { useMemo } from "react" + +interface Text { + position: { x: number; y: number } + layer?: string + step?: number +} + +export const useFilterTexts = ( + isPointOnScreen: (point: { x: number; y: number }) => boolean, + filterLayerAndStep: (obj: { layer?: string; step?: number }) => boolean, +) => { + return useMemo(() => { + return (text: Text) => { + if (!filterLayerAndStep(text)) return false + return isPointOnScreen(text.position) + } + }, [isPointOnScreen, filterLayerAndStep]) +} diff --git a/site/components/InteractiveGraphics/index.ts b/site/components/InteractiveGraphics/index.ts index 9c4bd37..8001fec 100644 --- a/site/components/InteractiveGraphics/index.ts +++ b/site/components/InteractiveGraphics/index.ts @@ -1,3 +1,4 @@ export { InteractiveGraphics } from "./InteractiveGraphics" export { InteractiveState } from "./InteractiveState" export { ContextMenu } from "./ContextMenu" +export { Text } from "./Text" diff --git a/site/utils/getGraphicsBounds.ts b/site/utils/getGraphicsBounds.ts index 6147e70..0b3d4e1 100644 --- a/site/utils/getGraphicsBounds.ts +++ b/site/utils/getGraphicsBounds.ts @@ -30,5 +30,17 @@ export const getGraphicsBounds = (graphics: GraphicsObject) => { bounds.maxX = Math.max(bounds.maxX, point.x) bounds.maxY = Math.max(bounds.maxY, point.y) } + for (const circle of graphics.circles ?? []) { + bounds.minX = Math.min(bounds.minX, circle.center.x - circle.radius) + bounds.minY = Math.min(bounds.minY, circle.center.y - circle.radius) + bounds.maxX = Math.max(bounds.maxX, circle.center.x + circle.radius) + bounds.maxY = Math.max(bounds.maxY, circle.center.y + circle.radius) + } + for (const text of graphics.texts ?? []) { + bounds.minX = Math.min(bounds.minX, text.position.x) + bounds.minY = Math.min(bounds.minY, text.position.y) + bounds.maxX = Math.max(bounds.maxX, text.position.x) + bounds.maxY = Math.max(bounds.maxY, text.position.y) + } return bounds } diff --git a/site/utils/getGraphicsFilteredByStep.ts b/site/utils/getGraphicsFilteredByStep.ts index 8324a34..ec12d0c 100644 --- a/site/utils/getGraphicsFilteredByStep.ts +++ b/site/utils/getGraphicsFilteredByStep.ts @@ -32,6 +32,9 @@ export function getGraphicsFilteredByStep( circles: graphics.circles?.filter( (c) => c.step === undefined || c.step === selectedStep, ), + texts: graphics.texts?.filter( + (t) => t.step === undefined || t.step === selectedStep, + ), } return filteredGraphics diff --git a/site/utils/getMaxStep.ts b/site/utils/getMaxStep.ts index 78c4de5..08859d1 100644 --- a/site/utils/getMaxStep.ts +++ b/site/utils/getMaxStep.ts @@ -16,6 +16,13 @@ export function getMaxStep(graphics: GraphicsObject) { const maxLineStep = getMaxStepFromArray(graphics.lines) const maxRectStep = getMaxStepFromArray(graphics.rects) const maxCircleStep = getMaxStepFromArray(graphics.circles) + const maxTextStep = getMaxStepFromArray(graphics.texts) - return Math.max(maxPointStep, maxLineStep, maxRectStep, maxCircleStep) + return Math.max( + maxPointStep, + maxLineStep, + maxRectStep, + maxCircleStep, + maxTextStep, + ) } diff --git a/site/utils/getTableItemsFromGraphicsObjects.ts b/site/utils/getTableItemsFromGraphicsObjects.ts index 124b0df..0697571 100644 --- a/site/utils/getTableItemsFromGraphicsObjects.ts +++ b/site/utils/getTableItemsFromGraphicsObjects.ts @@ -59,5 +59,15 @@ export const getTableItemsFromGraphicsObjects = ( }) } + if (graphicsObject.texts) { + graphicsObject.texts.forEach((text, idx) => { + objects.push({ + type: "text", + id: `text-${idx}`, + properties: flattenObject(text), + }) + }) + } + return objects } diff --git a/tests/__snapshots__/cartesian-rect.snap.svg b/tests/__snapshots__/cartesian-rect.snap.svg index 40ba06e..d0a8deb 100644 --- a/tests/__snapshots__/cartesian-rect.snap.svg +++ b/tests/__snapshots__/cartesian-rect.snap.svg @@ -1,6 +1,10 @@ - - + + + + + +
{ return (text: Text) => { if (!filterLayerAndStep(text)) return false - return isPointOnScreen(text.position) + return isPointOnScreen({ x: text.x, y: text.y }) } }, [isPointOnScreen, filterLayerAndStep]) } diff --git a/site/utils/getGraphicsBounds.ts b/site/utils/getGraphicsBounds.ts index 0b3d4e1..a07813a 100644 --- a/site/utils/getGraphicsBounds.ts +++ b/site/utils/getGraphicsBounds.ts @@ -37,10 +37,10 @@ export const getGraphicsBounds = (graphics: GraphicsObject) => { bounds.maxY = Math.max(bounds.maxY, circle.center.y + circle.radius) } for (const text of graphics.texts ?? []) { - bounds.minX = Math.min(bounds.minX, text.position.x) - bounds.minY = Math.min(bounds.minY, text.position.y) - bounds.maxX = Math.max(bounds.maxX, text.position.x) - bounds.maxY = Math.max(bounds.maxY, text.position.y) + bounds.minX = Math.min(bounds.minX, text.x) + bounds.minY = Math.min(bounds.minY, text.y) + bounds.maxX = Math.max(bounds.maxX, text.x) + bounds.maxY = Math.max(bounds.maxY, text.y) } return bounds } diff --git a/tests/__snapshots__/lines.snap.svg b/tests/__snapshots__/lines.snap.svg index 8d5b493..bfa61ce 100644 --- a/tests/__snapshots__/lines.snap.svg +++ b/tests/__snapshots__/lines.snap.svg @@ -1,6 +1,10 @@ - - + + + + + +