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