UNPKG

6.57 kBPlain TextView Raw
1import type { NodeGeometry, Indexable } from './types';
2
3/**
4 * Circle Geometry
5 * @beta
6 *
7 * @remarks
8 * This interface simply represents a circle geometry.
9 */
10export interface CircleGeometry {
11
12 /**
13 * X center of the circle.
14 */
15 x: number
16
17 /**
18 * Y center of the circle.
19 */
20 y: number
21
22 /**
23 * Radius of the circle.
24 */
25 r: number
26}
27
28/**
29 * Circle Constructor Properties
30 * @beta
31 * @typeParam CustomDataType - Type of the custom data property (optional, inferred automatically).
32 */
33export interface CircleProps<CustomDataType = void> extends CircleGeometry {
34
35 /**
36 * Custom data
37 */
38 data?: CustomDataType
39}
40
41/**
42 * Class representing a Circle.
43 * @typeParam CustomDataType - Type of the custom data property (optional, inferred automatically).
44 *
45 * @example Without custom data (JS/TS):
46 * ```typescript
47 * const circle = new Circle({
48 * x: 100,
49 * y: 100,
50 * r: 32,
51 * });
52 * ```
53 *
54 * @example With custom data (JS/TS):
55 * ```javascript
56 * const circle = new Circle({
57 * x: 100,
58 * y: 100,
59 * r: 32,
60 * data: {
61 * name: 'Jane',
62 * health: 100,
63 * },
64 * });
65 * ```
66 *
67 * @example With custom data (TS):
68 * ```typescript
69 * interface ObjectData {
70 * name: string
71 * health: number
72 * }
73 * const entity: ObjectData = {
74 * name: 'Jane',
75 * health: 100,
76 * };
77 *
78 * // Typescript will infer the type of the data property
79 * const circle1 = new Circle({
80 * x: 100,
81 * y: 100,
82 * r: 32,
83 * data: entity,
84 * });
85 *
86 * // You can also pass in a generic type for the data property
87 * const circle2 = new Circle<ObjectData>({
88 * x: 100,
89 * y: 100,
90 * r: 32,
91 * });
92 * circle2.data = entity;
93 * ```
94 *
95 * @example With custom class extending Circle (implements {@link CircleGeometry} (x, y, r)):
96 * ```javascript
97 * // extending inherits the qtIndex method
98 * class Bomb extends Circle {
99 *
100 * constructor(props) {
101 * // call super to set x, y, r (and data, if given)
102 * super(props);
103 * this.countdown = props.countdown;
104 * }
105 * }
106 *
107 * const bomb = new Bomb({
108 * countdown: 5,
109 * x: 10,
110 * y: 20,
111 * r: 30,
112 * });
113 * ```
114 *
115 * @example With custom class and mapping {@link CircleGeometry}:
116 * ```javascript
117 * // no need to extend if you don't implement CircleGeometry
118 * class Bomb {
119 *
120 * constructor(countdown) {
121 * this.countdown = countdown;
122 * this.position = [10, 20];
123 * this.radius = 30;
124 * }
125 *
126 * // add a qtIndex method to your class
127 * qtIndex(node) {
128 * // map your properties to CircleGeometry
129 * return Circle.prototype.qtIndex.call({
130 * x: this.position[0],
131 * y: this.position[1],
132 * r: this.radius,
133 * }, node);
134 * }
135 * }
136 *
137 * const bomb = new Bomb(5);
138 * ```
139 *
140 * @example With custom object that implements {@link CircleGeometry}:
141 * ```javascript
142 * const player = {
143 * name: 'Jane',
144 * health: 100,
145 * x: 10,
146 * y: 20,
147 * r: 30,
148 * qtIndex: Circle.prototype.qtIndex,
149 * });
150 * ```
151 *
152 * @example With custom object and mapping {@link CircleGeometry}:
153 * ```javascript
154 * // Note: this is not recommended but possible.
155 * // Using this technique, each object would have it's own qtIndex method.
156 * // Rather add qtIndex to your prototype, e.g. by using classes like shown above.
157 * const player = {
158 * name: 'Jane',
159 * health: 100,
160 * position: [10, 20],
161 * radius: 30,
162 * qtIndex: function(node) {
163 * return Circle.prototype.qtIndex.call({
164 * x: this.position[0],
165 * y: this.position[1],
166 * r: this.radius,
167 * }, node);
168 * },
169 * });
170 * ```
171 */
172export class Circle<CustomDataType = void> implements CircleGeometry, Indexable {
173
174 /**
175 * X center of the circle.
176 */
177 x: number;
178
179 /**
180 * Y center of the circle.
181 */
182 y: number;
183
184 /**
185 * Radius of the circle.
186 */
187 r: number;
188
189 /**
190 * Custom data.
191 */
192 data?: CustomDataType;
193
194 /**
195 * Circle Constructor
196 * @param props - Circle properties
197 * @typeParam CustomDataType - Type of the custom data property (optional, inferred automatically).
198 */
199 constructor(props:CircleProps<CustomDataType>) {
200
201 this.x = props.x;
202 this.y = props.y;
203 this.r = props.r;
204 this.data = props.data;
205 }
206
207 /**
208 * Determine which quadrant this circle belongs to.
209 * @param node - Quadtree node to be checked
210 * @returns Array containing indexes of intersecting subnodes (0-3 = top-right, top-left, bottom-left, bottom-right)
211 */
212 qtIndex(node:NodeGeometry): number[] {
213
214 const indexes:number[] = [],
215 w2 = node.width/2,
216 h2 = node.height/2,
217 x2 = node.x + w2,
218 y2 = node.y + h2;
219
220 //an array of node origins where the array index equals the node index
221 const nodes = [
222 [x2, node.y],
223 [node.x, node.y],
224 [node.x, y2],
225 [x2, y2],
226 ];
227
228 //test all nodes for circle intersections
229 for(let i=0; i<nodes.length; i++) {
230 if(Circle.intersectRect(this.x, this.y, this.r, nodes[i][0], nodes[i][1], nodes[i][0] + w2, nodes[i][1] + h2)) {
231 indexes.push(i);
232 }
233 }
234
235 return indexes;
236 }
237
238 /**
239 * Check if a circle intersects an axis aligned rectangle.
240 * @beta
241 * @see https://yal.cc/rectangle-circle-intersection-test/
242 * @param x - circle center X
243 * @param y - circle center Y
244 * @param r - circle radius
245 * @param minX - rectangle start X
246 * @param minY - rectangle start Y
247 * @param maxX - rectangle end X
248 * @param maxY - rectangle end Y
249 * @returns true if circle intersects rectangle
250 *
251 * @example Check if a circle intersects a rectangle:
252 * ```javascript
253 * const circ = { x: 10, y: 20, r: 30 };
254 * const rect = { x: 40, y: 50, width: 60, height: 70 };
255 * const intersect = Circle.intersectRect(
256 * circ.x,
257 * circ.y,
258 * circ.r,
259 * rect.x,
260 * rect.y,
261 * rect.x + rect.width,
262 * rect.y + rect.height,
263 * );
264 * console.log(circle, rect, 'intersect?', intersect);
265 * ```
266 */
267 static intersectRect(x:number, y:number, r:number, minX:number, minY:number, maxX:number, maxY:number): boolean {
268 const deltaX = x - Math.max(minX, Math.min(x, maxX));
269 const deltaY = y - Math.max(minY, Math.min(y, maxY));
270 return (deltaX * deltaX + deltaY * deltaY) < (r * r);
271 }
272}
\No newline at end of file