1 | import type { NodeGeometry, Indexable } from './types';
|
2 |
|
3 | /**
|
4 | * Circle Geometry
|
5 | * @beta
|
6 | *
|
7 | * @remarks
|
8 | * This interface simply represents a circle geometry.
|
9 | */
|
10 | export 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 | */
|
33 | export 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 | */
|
172 | export 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 |