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" // The main Zod schema for the division model diagram. export const DivisionModelDiagramPropsSchema = z .object({ type: z .literal("divisionModelDiagram") .describe("Identifies this as a division model widget."), width: createWidthSchema(), height: createHeightSchema(), dividend: z .number() .int() .min(0) .describe("The total number of objects to be divided (the dividend)."), divisor: z .number() .int() .positive() .describe("The number of objects in each full row (the divisor)."), objectColor: z .string() .regex(CSS_COLOR_PATTERN, "invalid css color") .describe("CSS color for the objects.") }) .strict() .describe( "Creates a visual model for division with remainders by arranging objects into a grid. The number of full rows represents the quotient, and the items in the last, incomplete row represent the remainder." ) export type DivisionModelDiagramProps = z.infer< typeof DivisionModelDiagramPropsSchema > /** * Generates an SVG diagram to model division with remainders. */ export const generateDivisionModelDiagram: WidgetGenerator< typeof DivisionModelDiagramPropsSchema > = async (props) => { const { width, height, dividend, divisor, objectColor } = props if (divisor <= 0) { // Cannot divide by zero or a negative number in this model. return `` } const canvas = new CanvasImpl({ chartArea: { left: 0, top: 0, width, height }, fontPxDefault: 12, lineHeightDefault: 1.2 }) // Calculate the quotient and remainder. const quotient = Math.floor(dividend / divisor) const remainder = dividend % divisor // Determine the grid dimensions. const numRows = quotient + (remainder > 0 ? 1 : 0) const numCols = divisor if (numRows === 0) { return `` } // Calculate the size of each cell in the grid. const availableWidth = width - 2 * PADDING const availableHeight = height - 2 * PADDING const cellWidth = availableWidth / numCols const cellHeight = availableHeight / numRows // Calculate the radius of the objects to fit within the cells with some padding. const objectRadius = Math.min(cellWidth, cellHeight) * 0.4 // Loop through the total number of objects and draw them in the grid. for (let i = 0; i < dividend; i++) { const row = Math.floor(i / divisor) const col = i % divisor // Calculate the center coordinates for the current object. const cx = PADDING + col * cellWidth + cellWidth / 2 const cy = PADDING + row * cellHeight + cellHeight / 2 canvas.drawCircle(cx, cy, objectRadius, { fill: objectColor, stroke: "none" }) } // Finalize the canvas to get the SVG body and calculated viewBox. const { svgBody, vbMinX, vbMinY, width: finalWidth, height: finalHeight } = canvas.finalize(PADDING) return `${svgBody}` }