├── LICENSE ├── README.md ├── fxmanifest.lua └── polyzone.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Michael Afrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mgo-polyzone-js 2 | A PolyZone script made using Javascript for FiveM 3 | 4 | # Support 5 | https://ko-fi.com/manugo 6 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | games { 'gta5' } 3 | lua54 'on' 4 | 5 | author 'ManuGO ' 6 | description 'MGO PolyZone Javascript' 7 | version '1.0.0' 8 | 9 | client_scripts { 10 | 'polyzone.js', 11 | } 12 | 13 | server_script {} 14 | -------------------------------------------------------------------------------- /polyzone.js: -------------------------------------------------------------------------------- 1 | const LOCAL_EVENT_PREFIX = '__PolyZoneJS__:'; 2 | const defaultColorWalls = [0, 255, 0]; 3 | const defaultColorOutline = [255, 0, 0]; 4 | const defaultColorGrid = [255, 255, 255]; 5 | const HEAD_BONE = 0x796e; 6 | 7 | const PolyVectorTools = { 8 | vector2: (x, y) => ({ 9 | x: x * 1.0, 10 | y: y * 1.0 11 | }), 12 | vector3: (x, y, z) => ({ 13 | x: x * 1.0, 14 | y: y * 1.0, 15 | z: z * 1.0 16 | }), 17 | vector4: (x, y, z, w) => ({ 18 | x: x * 1.0, 19 | y: y * 1.0, 20 | z: z * 1.0, 21 | heading: w * 1.0 22 | }), 23 | diff2D: (p1, p2) => { 24 | return PolyVectorTools.vector2(Math.abs(p2.x - p1.x), Math.abs(p2.y - p1.y)); 25 | }, 26 | distance2D: (p1, p2) => { 27 | const x = p1.x - p2.x; 28 | const y = p1.y - p2.y; 29 | return Math.sqrt(x * x + y * y); 30 | }, 31 | midpoint2D: (p1, p2) => PolyVectorTools.vector2((p1.x + p2.x) / 2, (p1.y + p2.y) / 2) 32 | }; 33 | 34 | // Utility functions 35 | const _isLeft = (p0, p1, p2) => { 36 | const p0x = p0.x; 37 | const p0y = p0.y; 38 | return (p1.x - p0x) * (p2.y - p0y) - (p2.x - p0x) * (p1.y - p0y); 39 | }; 40 | 41 | const _wn_inner_loop = (p0, p1, p2, wn) => { 42 | const p2y = p2.y; 43 | if (p0.y <= p2y) { 44 | if (p1.y > p2y) { 45 | if (_isLeft(p0, p1, p2) > 0) { 46 | return wn + 1; 47 | } 48 | } 49 | } else { 50 | if (p1.y <= p2y) { 51 | if (_isLeft(p0, p1, p2) < 0) { 52 | return wn - 1; 53 | } 54 | } 55 | } 56 | return wn; 57 | }; 58 | 59 | const addBlip = (pos) => { 60 | const blip = AddBlipForCoord(pos.x, pos.y, 0.0); 61 | SetBlipColour(blip, 7); 62 | SetBlipDisplay(blip, 8); 63 | SetBlipScale(blip, 1.0); 64 | SetBlipAsShortRange(blip, true); 65 | return blip; 66 | }; 67 | 68 | const clearTbl = (tbl) => { 69 | // Only works with contiguous (array-like) tables 70 | return tbl && tbl.length ? new Array(tbl.length).fill(null) : []; 71 | }; 72 | 73 | const copyTbl = (tbl) => { 74 | // Only a shallow copy, and only works with contiguous (array-like) tables 75 | return tbl && tbl.length && typeof tbl === 'array' ? [...tbl] : []; 76 | }; 77 | 78 | // Winding Number Algorithm - http://geomalgorithms.com/a03-_inclusion.html 79 | const windingNumber = (point, poly) => { 80 | let wn = 0; // winding number counter 81 | // loop through all edges of the polygon 82 | for (let i = 0; i < poly.length - 1; i++) { 83 | wn = _wn_inner_loop(poly[i], poly[i + 1], point, wn); 84 | } 85 | // test last point to first point, completing the polygon 86 | wn = _wn_inner_loop(poly[poly.length - 1], poly[0], point, wn); 87 | // the point is outside only when this winding number wn===0, otherwise it's inside 88 | return wn !== 0; 89 | }; 90 | 91 | // Detects intersection between two lines 92 | const isIntersecting = (a, b, c, d) => { 93 | // Store calculations in local variables for performance 94 | const ax_minus_cx = a.x - c.x; 95 | const bx_minus_ax = b.x - a.x; 96 | const dx_minus_cx = d.x - c.x; 97 | const ay_minus_cy = a.y - c.y; 98 | const by_minus_ay = b.y - a.y; 99 | const dy_minus_cy = d.y - c.y; 100 | const denominator = bx_minus_ax * dy_minus_cy - by_minus_ay * dx_minus_cx; 101 | const numerator1 = ay_minus_cy * dx_minus_cx - ax_minus_cx * dy_minus_cy; 102 | const numerator2 = ay_minus_cy * bx_minus_ax - ax_minus_cx * by_minus_ay; 103 | 104 | // Detect coincident lines 105 | if (denominator == 0) { 106 | return numerator1 == 0 && numerator2 == 0; 107 | } 108 | 109 | const r = numerator1 / denominator; 110 | const s = numerator2 / denominator; 111 | 112 | return r >= 0 && r <= 1 && s >= 0 && s <= 1; 113 | }; 114 | 115 | // https://rosettacode.org/wiki/Shoelace_formula_for_polygonal_area#Lua 116 | const calculatePolygonArea = (points) => { 117 | function det2(i, j) { 118 | return points[i].x * points[j].y - points[j].x * points[i].y; 119 | } 120 | let sum = points.length > 2 ? det2(points.length - 1, 0) : 0; 121 | for (let i = 0; i < points.length - 1; i++) { 122 | sum = sum + det2(i, i + 1); 123 | } 124 | return Math.abs(0.5 * sum); 125 | }; 126 | 127 | //Debug drawing functions 128 | const drawWall = (p1, p2, minZ, maxZ, color, a) => { 129 | const bottomLeft = [p1.x, p1.y, minZ]; 130 | const topLeft = [p1.x, p1.y, maxZ]; 131 | const bottomRight = [p2.x, p2.y, minZ]; 132 | const topRight = [p2.x, p2.y, maxZ]; 133 | DrawPoly(...bottomLeft, ...topLeft, ...bottomRight, color[0], color[1], color[2], a); 134 | DrawPoly(...topLeft, ...topRight, ...bottomRight, color[0], color[1], color[2], a); 135 | DrawPoly(...bottomRight, ...topRight, ...topLeft, color[0], color[1], color[2], a); 136 | DrawPoly(...bottomRight, ...topLeft, ...bottomLeft, color[0], color[1], color[2], a); 137 | }; 138 | 139 | // Grid creation functions 140 | // Calculates the points of the rectangle that make up the grid cell at grid position (cellX, cellY) 141 | const calculateGridCellPoints = (cellX, cellY, min, gridCellWidth, gridCellHeight) => { 142 | // min added to initial point, in order to shift the grid cells to the poly's starting position 143 | const x = cellX * gridCellWidth + min.x; 144 | const y = cellY * gridCellHeight + min.y; 145 | return [ 146 | PolyVectorTools.vector2(x, y), 147 | PolyVectorTools.vector2(x + gridCellWidth, y), 148 | PolyVectorTools.vector2(x + gridCellWidth, y + gridCellHeight), 149 | PolyVectorTools.vector2(x, y + gridCellHeight), 150 | PolyVectorTools.vector2(x, y) 151 | ]; 152 | }; 153 | 154 | const pointInPoly = (point, range, poly) => { 155 | const { x, y, z } = point; 156 | const { x: minX, y: minY } = poly.min; 157 | const { x: maxX, y: maxY } = poly.max; 158 | const minZ = poly.minZ; 159 | const maxZ = poly.maxZ; 160 | const xDistance = Math.min(Math.abs(minX - x), Math.abs(maxX - x)); 161 | const yDistance = Math.min(Math.abs(minY - y), Math.abs(maxY - y)); 162 | let zDistance = 0; 163 | 164 | if (minZ && maxZ) { 165 | zDistance = Math.min(Math.abs(minZ - z), Math.abs(maxZ - z)); 166 | } else if (minZ) { 167 | zDistance = Math.abs(minZ - z); 168 | } else if (maxZ) { 169 | zDistance = Math.abs(maxZ - z); 170 | } 171 | 172 | // Checks if point is within the polygon's bounding box 173 | if (x < minX || x > maxX || y < minY || y > maxY || (minZ && z < minZ) || (maxZ && z > maxZ)) { 174 | if (range) { 175 | return xDistance <= range && yDistance <= range && zDistance <= range; 176 | } else { 177 | return false; 178 | } 179 | } 180 | 181 | // Returns true if the grid cell associated with the point is entirely inside the poly 182 | const grid = poly.grid; 183 | if (grid) { 184 | const gridDivisions = poly.gridDivisions; 185 | const size = poly.size; 186 | const gridPosX = x - minX; 187 | const gridPosY = y - minY; 188 | const gridCellX = Math.floor((gridPosX * gridDivisions) / size.x); 189 | const gridCellY = Math.floor((gridPosY * gridDivisions) / size.y); 190 | let gridCellValue = grid[gridCellY + 1] && grid[gridCellY + 1][gridCellX + 1]; 191 | if (!gridCellValue || poly.lazyGrid) { 192 | gridCellValue = poly.isGridCellInsidePoly(gridCellX, gridCellY, poly); 193 | grid[gridCellY + 1][gridCellX + 1] = gridCellValue; 194 | } 195 | if (gridCellValue) return true; 196 | } 197 | 198 | return windingNumber(point, poly.points); 199 | }; 200 | 201 | class PolyZone { 202 | constructor(points, options) { 203 | if (!points) { 204 | console.log(`[PolyZone] Error: Passed null points table to PolyZone:Create() {name="${options.name}"}`); 205 | return; 206 | } 207 | if (points.length < 3) { 208 | console.log(`[PolyZone] Warning: Passed points table with less than 3 points to PolyZone:Create() {name="${options.name}"}`); 209 | } 210 | this.name = options.name || null; 211 | this.points = points; 212 | this.center = options.center; 213 | this.size = options.size; 214 | this.max = options.max; 215 | this.min = options.min; 216 | this.area = options.area; 217 | this.minZ = Number(options.minZ) * 1.0 || null; 218 | this.maxZ = Number(options.maxZ) * 1.0 || null; 219 | this.useGrid = options.useGrid || true; 220 | this.gridDivisions = Number(options.gridDivisions) || 25; 221 | this.debugColors = options.debugColors || {}; 222 | this.debugPoly = options.debugPoly || false; 223 | this.debugGrid = options.debugGrid || false; 224 | this.debugBlip = options.debugBlip || false; 225 | this.lazyGrid = this.debugGrid ? false : options.lazyGrid || true; 226 | this.data = options.data || {}; 227 | this.isPolyZone = true; 228 | this.lines = null; 229 | this.calculatePoly(); 230 | this.initDebug(); 231 | } 232 | 233 | addDebugBlip() { 234 | return addBlip(this.center || this.center); 235 | } 236 | 237 | initDebug() { 238 | if (this.debugBlip) { 239 | this.addDebugBlip(); 240 | } 241 | 242 | const debugEnabled = this.debugPoly || this.debugGrid; 243 | if (!debugEnabled) { 244 | return; 245 | } 246 | 247 | const debugInterval = setInterval(() => { 248 | if (!this.destroyed) { 249 | this.draw(); 250 | if (this.debugGrid && this.lines) { 251 | this.drawGrid(); 252 | } 253 | } else { 254 | clearInterval(debugInterval); 255 | } 256 | }, 5); 257 | } 258 | 259 | isGridCellInsidePoly(cellX, cellY) { 260 | const gridCellPoints = calculateGridCellPoints(cellX, cellY, this.min, this.gridCellWidth, this.gridCellHeight); 261 | // Connect the polygon to its starting point 262 | const polyPoints = [...this.points, this.points[1]]; 263 | // If none of the points of the grid cell are in the polygon, the grid cell can't be in it 264 | let isOnePointInPoly = false; 265 | for (let i = 0; i < gridCellPoints.length - 1; i++) { 266 | const cellPoint = gridCellPoints[i]; 267 | const x = cellPoint.x; 268 | const y = cellPoint.y; 269 | if (windingNumber(cellPoint, this.points)) { 270 | isOnePointInPoly = true; 271 | // If we are drawing the grid (this.lines ~= nil), we need to go through all the points, 272 | // and therefore can't break out of the loop early 273 | if (this.lines) { 274 | if (!this.gridXPoints[x]) { 275 | this.gridXPoints[x] = {}; 276 | } 277 | if (!this.gridYPoints[y]) { 278 | this.gridYPoints[y] = {}; 279 | } 280 | this.gridXPoints[x][y] = true; 281 | this.gridYPoints[y][x] = true; 282 | } else { 283 | break; 284 | } 285 | } 286 | } 287 | 288 | if (!isOnePointInPoly) { 289 | return false; 290 | } 291 | 292 | // If any of the grid cell's lines intersects with any of the polygon's lines 293 | // then the grid cell is not completely within the poly 294 | for (let i = 0; i < gridCellPoints.length - 1; i++) { 295 | const gridCellP1 = gridCellPoints[i]; 296 | const gridCellP2 = gridCellPoints[i + 1]; 297 | for (let j = 0; j < polyPoints.length - 1; j++) { 298 | if (isIntersecting(gridCellP1, gridCellP2, polyPoints[j], polyPoints[j + 1])) { 299 | return false; 300 | } 301 | } 302 | } 303 | 304 | return true; 305 | } 306 | 307 | calculateLinesForDrawingGrid() { 308 | const lines = []; 309 | 310 | Object.entries(this.gridXPoints).forEach(([xValue, yTable]) => { 311 | const yValues = Object.keys(yTable); 312 | if (yValues.length >= 2) { 313 | yValues.sort(); 314 | let minY = yValues[0]; 315 | let lastY = yValues[0]; 316 | yValues.forEach((yValue, index) => { 317 | if (yValue - lastY > this.gridCellHeight * 1.0) { 318 | lines.push({ min: PolyVectorTools.vector2(xValue, minY), max: PolyVectorTools.vector2(xValue, lastY) }); 319 | minY = yValue; 320 | } else if (index == yValues.length - 1) { 321 | lines.push({ min: PolyVectorTools.vector2(xValue, minY), max: PolyVectorTools.vector2(xValue, yValue) }); 322 | } 323 | lastY = yValue; 324 | }); 325 | } 326 | }); 327 | 328 | // Same as above, but for gridYPoints instead of gridXPoints 329 | Object.entries(this.gridYPoints).forEach(([yValue, xTable]) => { 330 | const xValues = Object.keys(xTable); 331 | if (xValues.length >= 2) { 332 | xValues.sort(); 333 | let minX = xValues[0]; 334 | let lastX = xValues[0]; 335 | xValues.forEach((xValue, index) => { 336 | if (xValue - lastX > this.gridCellWidth * 1.0) { 337 | lines.push({ min: PolyVectorTools.vector2(minX, yValue), max: PolyVectorTools.vector2(lastX, yValue) }); 338 | minX = xValue; 339 | } else if (index == xValues.length - 1) { 340 | lines.push({ min: PolyVectorTools.vector2(minX, yValue), max: PolyVectorTools.vector2(xValue, yValue) }); 341 | } 342 | lastX = xValue; 343 | }); 344 | } 345 | }); 346 | 347 | return lines; 348 | } 349 | 350 | createGrid() { 351 | this.gridArea = 0.0; 352 | this.gridCellWidth = this.size.x / this.gridDivisions; 353 | this.gridCellHeight = this.size.y / this.gridDivisions; 354 | const isInside = {}; 355 | const gridCellArea = this.gridCellWidth * this.gridCellHeight; 356 | for (let y = 1; y <= this.gridDivisions; y++) { 357 | isInside[y] = {}; 358 | for (let x = 1; x <= this.gridDivisions; x++) { 359 | if (this.isGridCellInsidePoly(x - 1, y - 1)) { 360 | this.gridArea = this.gridArea + gridCellArea; 361 | isInside[y][x] = true; 362 | } 363 | } 364 | } 365 | this.grid = isInside; 366 | this.gridCoverage = this.gridArea / this.area; 367 | if (this.debugGrid) { 368 | const coverage = (this.gridCoverage * 100).toFixed(2); 369 | print( 370 | `[PolyZone] Debug: Grid Coverage at ${coverage}% with ${this.gridDivisions} divisions. Optimal coverage for memory usage and startup time is 80-90%` 371 | ); 372 | this.lines = this.calculateLinesForDrawingGrid(); 373 | } 374 | } 375 | 376 | calculatePoly() { 377 | if (!this.min || !this.max || !this.size || !this.center || !this.area) { 378 | let [minX, minY] = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; 379 | let [maxX, maxY] = [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]; 380 | this.points.forEach((point) => { 381 | minX = Math.min(minX, point.x); 382 | minY = Math.min(minY, point.y); 383 | maxX = Math.max(maxX, point.x); 384 | maxY = Math.max(maxY, point.y); 385 | }); 386 | this.min = PolyVectorTools.vector2(minX, minY); 387 | this.max = PolyVectorTools.vector2(maxX, maxY); 388 | this.size = PolyVectorTools.diff2D(this.max, this.min); 389 | this.center = PolyVectorTools.midpoint2D(this.max, this.min); 390 | this.area = calculatePolygonArea(this.points); 391 | } 392 | 393 | this.boundingRadius = Math.sqrt(this.size.y * this.size.y + this.size.x * this.size.x) / 2; 394 | if (this.useGrid && !this.lazyGrid) { 395 | if (this.debugGrid) { 396 | this.gridXPoints = {}; 397 | this.gridYPoints = {}; 398 | this.lines = {}; 399 | } 400 | this.createGrid(); 401 | } else if (this.useGrid) { 402 | const isInside = {}; 403 | for (let i = 1; i <= this.gridDivisions; i++) { 404 | isInside[i] = {}; 405 | } 406 | this.grid = isInside; 407 | this.gridCellWidth = this.size.x / this.gridDivisions; 408 | this.gridCellHeight = this.size.y / this.gridDivisions; 409 | } 410 | } 411 | 412 | transformPoint(point) { 413 | return point; 414 | } 415 | 416 | draw() { 417 | const zDrawDist = 45.0; 418 | const oColor = this.debugColors.outline || defaultColorOutline; 419 | const wColor = this.debugColors.walls || defaultColorWalls; 420 | const plyPed = PlayerPedId(); 421 | const plyPos = GetEntityCoords(plyPed); 422 | const minZ = (this.minZ || plyPos.z - zDrawDist) * 1.0; 423 | const maxZ = (this.maxZ || plyPos.z + zDrawDist) * 1.0; 424 | const points = this.points; 425 | 426 | points.forEach((currentPoint, index) => { 427 | const point = this.transformPoint(currentPoint); 428 | DrawLine(point.x, point.y, minZ, point.x, point.y, maxZ, oColor[0], oColor[1], oColor[2], 164); 429 | // If it is not last 430 | if (index < points.length - 1) { 431 | const p2 = this.transformPoint(points[index + 1]); 432 | DrawLine(point.x, point.y, maxZ, p2.x, p2.y, maxZ, oColor[0], oColor[1], oColor[2], 184); 433 | drawWall(point, p2, minZ, maxZ, wColor, 48); 434 | } 435 | }); 436 | 437 | if (points.length > 2) { 438 | const firstPoint = this.transformPoint(points[0]); 439 | const lastPoint = this.transformPoint(points[points.length - 1]); 440 | DrawLine(firstPoint.x, firstPoint.y, maxZ, lastPoint.x, lastPoint.y, maxZ, oColor[0], oColor[1], oColor[2], 184); 441 | drawWall(firstPoint, lastPoint, minZ, maxZ, wColor, 48); 442 | } 443 | } 444 | 445 | drawPoly(poly) { 446 | this.draw(poly); 447 | } 448 | 449 | drawGrid() { 450 | let minZ = this.minZ; 451 | let maxZ = this.maxZ; 452 | if (!minZ || !maxZ) { 453 | const plyPed = PlayerPedId(); 454 | const plyPos = GetEntityCoords(plyPed); 455 | minZ = plyPos.z - 46.0; 456 | maxZ = plyPos.z - 45.0; 457 | } 458 | const color = this.debugColors.grid || defaultColorGrid; 459 | 460 | this.lines.forEach((line) => { 461 | DrawLine(line.min.x, line.min.y, maxZ, line.max.x, line.max.y, maxZ, color[0], color[1], color[2], 196); 462 | }); 463 | } 464 | 465 | isPointInside(point, range) { 466 | if (this.destroyed) { 467 | console.log(`[PolyZone] Warning: Called isPointInside on destroyed zone {name=${this.name}`); 468 | return false; 469 | } 470 | return pointInPoly(point, range, this); 471 | } 472 | 473 | destroy() { 474 | this.destroyed = true; 475 | if (this.debugPoly || this.debugGrid) { 476 | print(`[PolyZone] Debug: Destroying zone {name=${this.name}}`); 477 | } 478 | } 479 | 480 | // Helper functions 481 | getPlayerPosition() { 482 | const coords = GetEntityCoords(PlayerPedId()); 483 | return PolyVectorTools.vector3(coords[0], coords[1], coords[2]); 484 | } 485 | 486 | getPlayerHeadPosition() { 487 | return GetPedBoneCoords(PlayerPedId(), HEAD_BONE); 488 | } 489 | 490 | onPointInOut(getPointCb, onPointInOutCb, range, waitInMS) { 491 | // Localize the waitInMS value for performance reasons (default of 500 ms) 492 | const _waitInMS = waitInMS || 500; 493 | let isInside = false; 494 | const interval = setInterval(() => { 495 | if (!this.destroyed) { 496 | if (!this.paused) { 497 | const point = getPointCb(); 498 | const newIsInside = this.isPointInside(point, range); 499 | if (newIsInside !== isInside) { 500 | onPointInOutCb(newIsInside, point); 501 | isInside = newIsInside; 502 | } 503 | } else { 504 | clearInterval(interval); 505 | } 506 | } else { 507 | clearInterval(interval); 508 | } 509 | }, _waitInMS); 510 | } 511 | 512 | onPlayerInOut(onPointInOutCb, waitInMS) { 513 | return this.onPointInOut(this.getPlayerPosition, onPointInOutCb, waitInMS); 514 | } 515 | 516 | onPlayerRangeInOut(onPointInOutCb, range, waitInMS) { 517 | return this.onPointInOut(this.getPlayerPosition, onPointInOutCb, range, waitInMS); 518 | } 519 | 520 | setPaused(paused) { 521 | this.paused = paused; 522 | } 523 | 524 | isPaused() { 525 | return this.paused; 526 | } 527 | 528 | getBoundingBoxMin() { 529 | return this.min; 530 | } 531 | 532 | getBoundingBoxMax() { 533 | return this.max; 534 | } 535 | 536 | getBoundingBoxSize() { 537 | return this.size; 538 | } 539 | 540 | getBoundingBoxCenter() { 541 | return this.center; 542 | } 543 | } 544 | --------------------------------------------------------------------------------