UNPKG

7.25 kBPlain TextView Raw
1import type { NodeGeometry, Indexable } from './types';
2
3/**
4 * Line Geometry
5 * @beta
6 *
7 * @remarks
8 * This interface simply represents a line geometry.
9 */
10export interface LineGeometry {
11
12 /**
13 * X start of the line.
14 */
15 x1: number
16
17 /**
18 * Y start of the line.
19 */
20 y1: number
21
22 /**
23 * X end of the line.
24 */
25 x2: number
26
27 /**
28 * Y end of the line.
29 */
30 y2: number
31}
32
33/**
34 * Line Constructor Properties
35 * @beta
36 * @typeParam CustomDataType - Type of the custom data property (optional, inferred automatically).
37 */
38export interface LineProps<CustomDataType = void> extends LineGeometry {
39
40 /**
41 * Custom data
42 */
43 data?: CustomDataType
44}
45
46/**
47 * Class representing a Line
48 * @typeParam CustomDataType - Type of the custom data property (optional, inferred automatically).
49 *
50 * @example Without custom data (JS/TS):
51 * ```typescript
52 * const line = new Line({
53 * x1: 10,
54 * y1: 20,
55 * x2: 30,
56 * y2: 40,
57 * });
58 * ```
59 *
60 * @example With custom data (JS/TS):
61 * ```javascript
62 * const line = new Line({
63 * x1: 10,
64 * y1: 20,
65 * x2: 30,
66 * y2: 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 line1 = new Line({
87 * x1: 10,
88 * y1: 20,
89 * x2: 30,
90 * y2: 40,
91 * data: entity,
92 * });
93 *
94 * // You can also pass in a generic type for the data property
95 * const line2 = new Line<ObjectData>({
96 * x1: 10,
97 * y1: 20,
98 * x2: 30,
99 * y2: 40,
100 * });
101 * line2.data = entity;
102 * ```
103 *
104 * @example With custom class extending Line (implements {@link LineGeometry} (x1, y1, x2, y2)):
105 * ```javascript
106 * // extending inherits the qtIndex method
107 * class Laser extends Line {
108 *
109 * constructor(props) {
110 * // call super to set x1, y1, x2, y2 (and data, if given)
111 * super(props);
112 * this.color = props.color;
113 * }
114 * }
115 *
116 * const laser = new Laser({
117 * color: 'green',
118 * x1: 10,
119 * y1: 20,
120 * x2: 30,
121 * y2: 40,
122 * });
123 * ```
124 *
125 * @example With custom class and mapping {@link LineGeometry}:
126 * ```javascript
127 * // no need to extend if you don't implement LineGeometry
128 * class Laser {
129 *
130 * constructor(color) {
131 * this.color = color;
132 * this.start = [10, 20];
133 * this.end = [30, 40];
134 * }
135 *
136 * // add a qtIndex method to your class
137 * qtIndex(node) {
138 * // map your properties to LineGeometry
139 * return Line.prototype.qtIndex.call({
140 * x1: this.start[0],
141 * y1: this.start[1],
142 * x2: this.end[0],
143 * y2: this.end[1],
144 * }, node);
145 * }
146 * }
147 *
148 * const laser = new Laser('green');
149 * ```
150 *
151 * @example With custom object that implements {@link LineGeometry}:
152 * ```javascript
153 * const player = {
154 * name: 'Jane',
155 * health: 100,
156 * x1: 10,
157 * y1: 20,
158 * x2: 30,
159 * y2: 40,
160 * qtIndex: Line.prototype.qtIndex,
161 * });
162 * ```
163 *
164 * @example With custom object and mapping {@link LineGeometry}:
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 * start: [10, 20],
173 * end: [30, 40],
174 * qtIndex: function(node) {
175 * return Line.prototype.qtIndex.call({
176 * x1: this.start[0],
177 * y1: this.start[1],
178 * x2: this.end[0],
179 * y2: this.end[1],
180 * }, node);
181 * },
182 * });
183 * ```
184 */
185export class Line<CustomDataType = void> implements LineGeometry, Indexable {
186
187 /**
188 * X start of the line.
189 */
190 x1: number;
191
192 /**
193 * Y start of the line.
194 */
195 y1: number;
196
197 /**
198 * X end of the line.
199 */
200 x2: number;
201
202 /**
203 * Y end of the line.
204 */
205 y2: number;
206
207 /**
208 * Custom data.
209 */
210 data?: CustomDataType;
211
212 /**
213 * Line Constructor
214 * @param props - Line properties
215 * @typeParam CustomDataType - Type of the custom data property (optional, inferred automatically).
216 */
217 constructor(props:LineProps<CustomDataType>) {
218
219 this.x1 = props.x1;
220 this.y1 = props.y1;
221 this.x2 = props.x2;
222 this.y2 = props.y2;
223 this.data = props.data;
224 }
225
226 /**
227 * Determine which quadrant this line belongs to.
228 * @beta
229 * @param node - Quadtree node to be checked
230 * @returns Array containing indexes of intersecting subnodes (0-3 = top-right, top-left, bottom-left, bottom-right)
231 */
232 qtIndex(node:NodeGeometry): number[] {
233
234 const indexes:number[] = [],
235 w2 = node.width/2,
236 h2 = node.height/2,
237 x2 = node.x + w2,
238 y2 = node.y + h2;
239
240 //an array of node origins where the array index equals the node index
241 const nodes = [
242 [x2, node.y],
243 [node.x, node.y],
244 [node.x, y2],
245 [x2, y2],
246 ];
247
248 //test all nodes for line intersections
249 for(let i=0; i<nodes.length; i++) {
250 if(Line.intersectRect(this.x1, this.y1, this.x2, this.y2,
251 nodes[i][0], nodes[i][1], nodes[i][0] + w2, nodes[i][1] + h2)) {
252 indexes.push(i);
253 }
254 }
255
256 return indexes;
257 }
258
259 /**
260 * check if a line segment (the first 4 parameters) intersects an axis aligned rectangle (the last 4 parameters)
261 * @beta
262 *
263 * @remarks
264 * There is a bug where detection fails on corner intersections
265 * when the line enters/exits the node exactly at corners (45°)
266 * {@link https://stackoverflow.com/a/18292964/860205}
267 *
268 * @param x1 - line start X
269 * @param y1 - line start Y
270 * @param x2 - line end X
271 * @param y2 - line end Y
272 * @param minX - rectangle start X
273 * @param minY - rectangle start Y
274 * @param maxX - rectangle end X
275 * @param maxY - rectangle end Y
276 * @returns true if the line segment intersects the axis aligned rectangle
277 */
278 static intersectRect(x1:number, y1:number, x2:number, y2:number, minX:number, minY:number, maxX:number, maxY:number): boolean {
279
280 // Completely outside
281 if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY))
282 return false;
283
284 // Single point inside
285 if ((x1 >= minX && x1 <= maxX && y1 >= minY && y1 <= maxY) || (x2 >= minX && x2 <= maxX && y2 >= minY && y2 <= maxY))
286 return true;
287
288 const m = (y2 - y1) / (x2 - x1);
289
290 let y = m * (minX - x1) + y1;
291 if (y > minY && y < maxY) return true;
292
293 y = m * (maxX - x1) + y1;
294 if (y > minY && y < maxY) return true;
295
296 let x = (minY - y1) / m + x1;
297 if (x > minX && x < maxX) return true;
298
299 x = (maxY - y1) / m + x1;
300 if (x > minX && x < maxX) return true;
301
302 return false;
303 }
304}
\No newline at end of file