packages/core/src/lib/geometry/regular-polygon.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import type { LayoutBox } from '../layout/types.js';
2 import { createPolygonPath } from './polygon.js';
3 import {
4 resolveNumericCornerRadii,
5 type RoundedCornerPathOptions,
6 } from './rounded-corners.js';
7 import {
8 finiteNumber,
9 integerAtLeast,
10 nonnegativeNumber,
11 pathRangeError,
12 type GeometryErrorFactory,
13 } from './validation.js';
14
15 /**
16 * Center, side count, and rotation for a regular polygon path.
17 *
18 * @see {@link RegularPolygonPathOptions}
19 */
20 export interface RegularPolygonPathPosition {
21 /** Number of sides of the regular polygon. See {@link RegularPolygonPathPosition}. */
22 sides: number;
23 /** Horizontal coordinate of the center. See {@link RegularPolygonPathPosition}. */
24 cx?: number;
25 /** Vertical coordinate of the center. See {@link RegularPolygonPathPosition}. */
26 cy?: number;
27 /** Rotation applied to the containing geometry. See {@link RegularPolygonPathPosition}. */
28 rotation?: number;
29 }
30
31 /**
32 * Exactly one size measure for a regular polygon: circumradius, inradius, or side length.
33 *
34 * @see {@link RegularPolygonPathOptions}
35 */
36 export type RegularPolygonNumericSize =
37 | {
38 /** Distance from the polygon center to a vertex. See {@link RegularPolygonNumericSize}. */
39 circumradius: number;
40 /**
41 * Not accepted in this variant; use the alternative fields instead. See
42 * {@link RegularPolygonNumericSize}.
43 */
44 inradius?: never;
45 /**
46 * Not accepted in this variant; use the alternative fields instead. See
47 * {@link RegularPolygonNumericSize}.
48 */
49 sideLength?: never;
50 }
51 | {
52 /**
53 * Not accepted in this variant; use the alternative fields instead. See
54 * {@link RegularPolygonNumericSize}.
55 */
56 circumradius?: never;
57 /** Distance from the polygon center to an edge. See {@link RegularPolygonNumericSize}. */
58 inradius: number;
59 /**
60 * Not accepted in this variant; use the alternative fields instead. See
61 * {@link RegularPolygonNumericSize}.
62 */
63 sideLength?: never;
64 }
65 | {
66 /**
67 * Not accepted in this variant; use the alternative fields instead. See
68 * {@link RegularPolygonNumericSize}.
69 */
70 circumradius?: never;
71 /**
72 * Not accepted in this variant; use the alternative fields instead. See
73 * {@link RegularPolygonNumericSize}.
74 */
75 inradius?: never;
76 /** Length of one polygon edge. See {@link RegularPolygonNumericSize}. */
77 sideLength: number;
78 };
79
80 /**
81 * Geometry and construction options for a regular polygon path.
82 *
83 * @see {@link RegularPolygonPathPosition}
84 * @see {@link RegularPolygonNumericSize}
85 * @see {@link RoundedCornerPathOptions}
86 * @see {@link createRegularPolygonPath}
87 */
88 export type RegularPolygonPathOptions = RegularPolygonPathPosition &
89 RegularPolygonNumericSize &
90 RoundedCornerPathOptions;
91
92 export interface ResolvedRegularPolygonGeometry {
93 path: Path2D;
94 vertices: readonly (readonly [number, number])[];
95 bounds?: LayoutBox;
96 }
97
98 const SIZE_KEYS = ['circumradius', 'inradius', 'sideLength'] as const;
99
100 export function resolveRegularPolygonGeometry(
101 options: Readonly<RegularPolygonPathOptions>,
102 errorFactory: GeometryErrorFactory = pathRangeError,
103 ): ResolvedRegularPolygonGeometry {
104 const sides = integerAtLeast(options.sides, 3, 'sides', errorFactory);
105 const sizeKeys = SIZE_KEYS.filter(key => options[key] !== undefined);
106 if (sizeKeys.length !== 1) {
107 throw errorFactory(
108 'circumradius|inradius|sideLength',
109 options,
110 'must provide exactly one size option',
111 );
112 }
113
114 const sizeKind = sizeKeys[0];
115 const size = nonnegativeNumber(options[sizeKind], sizeKind, errorFactory);
116 const cx = finiteNumber(options.cx ?? 0, 'cx', errorFactory);
117 const cy = finiteNumber(options.cy ?? 0, 'cy', errorFactory);
118 const rotation = finiteNumber(
119 options.rotation ?? -Math.PI / 2,
120 'rotation',
121 errorFactory,
122 );
123 const radii = resolveNumericCornerRadii(sides, options, errorFactory);
124 const circumradius = sizeKind === 'circumradius' ? size :
125 sizeKind === 'inradius' ? size / Math.cos(Math.PI / sides) :
126 size / (2 * Math.sin(Math.PI / sides));
127
128 let left = Number.POSITIVE_INFINITY;
129 let right = Number.NEGATIVE_INFINITY;
130 let top = Number.POSITIVE_INFINITY;
131 let bottom = Number.NEGATIVE_INFINITY;
132 const vertices = Array.from({ length: sides }, (_, index) => {
133 const angle = rotation + (index * Math.PI * 2) / sides;
134 const vertex = [
135 cx + circumradius * Math.cos(angle),
136 cy + circumradius * Math.sin(angle),
137 ] as const;
138 left = Math.min(left, vertex[0]);
139 right = Math.max(right, vertex[0]);
140 top = Math.min(top, vertex[1]);
141 bottom = Math.max(bottom, vertex[1]);
142 return vertex;
143 });
144
145 return {
146 path: circumradius === 0 ? new Path2D() :
147 createPolygonPath({ coords: vertices, cornerRadii: radii }),
148 vertices,
149 bounds: {
150 x: left,
151 y: top,
152 width: right - left,
153 height: bottom - top,
154 },
155 };
156 }
157
158 /**
159 * Creates a Canvas Path2D for regular polygon geometry from validated options.
160 *
161 * @param options - Polygon center, radius, side count, and rotation. See
162 * {@link RegularPolygonPathOptions} .
163 * @returns A native Canvas path containing the regular polygon.
164 *
165 * @see {@link RegularPolygonPathOptions}
166 */
167 export function createRegularPolygonPath(
168 options: Readonly<RegularPolygonPathOptions>,
169 ): Path2D {
170 return resolveRegularPolygonGeometry(options).path;
171 }
172
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.