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