import { z } from "zod" import type { WidgetGenerator } from "@/widgets/types" import { CanvasImpl } from "@/widgets/utils/canvas-impl" import { PADDING } from "@/widgets/utils/constants" import { CSS_COLOR_PATTERN } from "@/widgets/utils/css-color" import { createHeightSchema, createWidthSchema } from "@/widgets/utils/schemas" import { theme } from "@/widgets/utils/theme" // The main Zod schema for the factorization diagram. export const FactorizationDiagramPropsSchema = z .object({ type: z .literal("factorizationDiagram") .describe("Identifies this as a factorization diagram widget."), width: createWidthSchema(), height: createHeightSchema(), numberToFactor: z .number() .int() .positive() .describe( "The integer to be factored and displayed as a rectangular array of dots." ), dotColor: z .string() .regex(CSS_COLOR_PATTERN, "invalid css color") .describe("CSS color for the dots in the array.") }) .strict() .describe( "Creates a rectangular array of numbered dots to visually represent the factors of a number. The widget automatically determines the number of rows and columns by finding the factors closest to the square root of the number." ) export type FactorizationDiagramProps = z.infer< typeof FactorizationDiagramPropsSchema > /** * Finds the pair of integer factors of a number that are closest to its square root. * This results in the most "squarish" possible layout for the rectangular array. * @param n The number to factor. * @returns An object with `rows` and `columns`. */ function findBestFactors(n: number): { rows: number; columns: number } { if (n <= 0) return { rows: 0, columns: 0 } const sqrt = Math.floor(Math.sqrt(n)) for (let i = sqrt; i >= 1; i--) { if (n % i === 0) { // Return the smaller factor as rows and the larger as columns return { rows: i, columns: n / i } } } return { rows: 1, columns: n } // Fallback for prime numbers } /** * Generates an SVG diagram of a rectangular array of numbered dots to illustrate factors. */ export const generateFactorizationDiagram: WidgetGenerator< typeof FactorizationDiagramPropsSchema > = async (props) => { const { width, height, numberToFactor, dotColor } = props // The widget's logic now determines the best row and column count. const { rows, columns } = findBestFactors(numberToFactor) if (rows === 0 || columns === 0) { return `` } const canvas = new CanvasImpl({ chartArea: { left: 0, top: 0, width, height }, fontPxDefault: 12, lineHeightDefault: 1.2 }) const availableWidth = width - 2 * PADDING const availableHeight = height - 2 * PADDING const cellWidth = availableWidth / columns const cellHeight = availableHeight / rows const dotRadius = Math.min(cellWidth, cellHeight) * 0.4 const fontSize = Math.max(8, dotRadius * 0.9) for (let i = 0; i < numberToFactor; i++) { const row = Math.floor(i / columns) const col = i % columns const dotNumber = i + 1 const cx = PADDING + col * cellWidth + cellWidth / 2 const cy = PADDING + row * cellHeight + cellHeight / 2 canvas.drawCircle(cx, cy, dotRadius, { fill: dotColor, stroke: "none" }) canvas.drawText({ x: cx, y: cy, text: String(dotNumber), fill: theme.colors.black, fontPx: fontSize, fontWeight: theme.font.weight.medium, anchor: "middle", dominantBaseline: "middle" }) } const { svgBody, vbMinX, vbMinY, width: finalWidth, height: finalHeight } = canvas.finalize(PADDING) return `${svgBody}` }