UNPKG

@dominicstop/utils

Version:

Yet another event emitter written in typescript.

310 lines (309 loc) 9.93 kB
import { Angle } from "./Angle"; import { BoxedCircle } from "./BoxedCircle"; import { Point } from "./Point"; import { Line } from "./Line"; export class BoxedHexagon { constructor(args) { this.circumRadius = args.circumRadius; const angleValue = args.startAngleOffset ?? { angleUnit: 'degrees', angleValue: 0, }; this.startAngleOffset = new Angle(angleValue); this.origin = (() => { switch (args.mode) { case 'relativeToCenter': const originX = args.center.x - args.circumRadius; const originY = args.center.y - args.circumRadius; return new Point({ x: originX, y: originY, }); case 'relativeToOrigin': return new Point({ x: args.origin.x, y: args.origin.y, }); } ; })(); } // MARK: Getter + Setter Pairs // --------------------------- get center() { return this.circumCircle.center; } ; set center(newCenter) { const originX = newCenter.x - this.circumRadius; const originY = newCenter.y - this.circumRadius; this.origin = new Point({ x: originX, y: originY }); } ; // MARK: Getters // ------------- get asValue() { return { center: this.boundingBox.center, circumRadius: this.circumRadius, startAngleOffset: this.startAngleOffset.asValue, }; } ; // distance between two adjacent points get sideLength() { return this.circumRadius; } ; get perimeter() { return this.circumRadius * 6; } ; get inRadius() { return this.circumRadius * Math.sqrt(3) / 2; } ; get apothem() { return this.inRadius; } ; get inCircle() { const inRadius = this.inRadius; return new BoxedCircle({ mode: 'relativeToCenter', center: this.boundingBox.center, radius: inRadius, }); } ; get circumCircle() { return new BoxedCircle({ mode: 'relativeToOrigin', origin: this.origin, radius: this.circumRadius, }); } ; get boundingBox() { return Point.getBoundingBoxForPoints(this.cornerPointsAsArray); } ; get cornerAngles() { const angles = []; const minAngle = 360 / 6; let currentAngle = this.startAngleOffset.degrees; for (let i = 0; i < 6; i++) { currentAngle += minAngle; const newAngle = new Angle({ angleUnit: 'degrees', angleValue: currentAngle }); angles.push(newAngle); } ; return angles; } ; get cornerPointsAsArray() { const centerPoint = this.circumCircle.center; return this.cornerAngles.map((angleItem) => (angleItem.getPointAlongCircle({ radius: this.circumRadius, centerPoint, isClockwise: false, }))); } ; get edgeLines() { const cornerPoints = this.cornerPointsAsArray; let lines = []; for (let index = 0; index < cornerPoints.length; index++) { const nextIndex = (index + 1) % cornerPoints.length; const pointCurrent = cornerPoints[index]; const pointNext = cornerPoints[nextIndex]; const line = new Line({ startPoint: pointCurrent, endPoint: pointNext, }); lines.push(line); } return lines; } ; get area() { return (3 * Math.sqrt(3) * Math.pow(this.circumRadius, 2)) / 2; } ; get edgeMidpoints() { return this.edgeLines.map(line => line.midPoint); } ; // MARK: Methods // ------------- clone() { return new BoxedHexagon({ mode: 'relativeToOrigin', origin: this.origin.clone(), circumRadius: this.circumRadius, startAngleOffset: this.startAngleOffset.asValue, }); } ; computeTiledHexagonAlongsideEdge(args) { const extraPositionOffset = args.extraPositionOffset ?? 0; const centerPoint = this.boundingBox.center; const apothemLine = new Line({ startPoint: centerPoint, endPoint: args.edgeLine.midPoint, }); const apothemDistance = apothemLine.distance * 2; const { stopPoint: nextCenterPoint } = apothemLine.traverseByDistance(apothemDistance + extraPositionOffset); return new BoxedHexagon({ mode: 'relativeToCenter', center: nextCenterPoint, circumRadius: this.circumRadius, }); } ; isPointInside(point) { const center = this.center; const dx = Math.abs(point.x - center.x); const dy = Math.abs(point.y - center.y); const r = this.circumRadius; return ((dx <= r && dy <= this.inRadius) && (this.inRadius * r - this.inRadius * dx - r * dy + dx * dy) >= 0); } ; rotatedByAngle(angle) { const newAngle = this.startAngleOffset.addOtherAngle(angle); return new BoxedHexagon({ mode: 'relativeToOrigin', origin: this.origin, circumRadius: this.circumRadius, startAngleOffset: newAngle.asValue, }); } ; translatedByOffset(offset) { const [newOrigin] = Point.translatePoints({ points: [this.origin], dx: offset.dx, dy: offset.dy }); return new BoxedHexagon({ mode: 'relativeToOrigin', origin: newOrigin, circumRadius: this.circumRadius, startAngleOffset: this.startAngleOffset.asValue, }); } ; computeDistanceToOther(other) { return this.center.getDistance(other.center); } ; /** * returns true if this hexagon is edge-to-edge with another hexagon. * * * in a regular hex grid, two hexagons are touching if the distance * between their centers equals exactly: `2 * sideLength` (i.e. * one full edge length apart). * * - This check uses the Manhattan distance approximation for performance and simplicity. */ isEdgeToEdgeWithOther(other) { const dx = Math.abs(this.center.x - other.center.x); const dy = Math.abs(this.center.y - other.center.y); const maxDist = this.sideLength * 2; return Math.abs(dx + dy - maxDist) < 1e-6; } /** * returns true if this hexagon is colliding with another hexagon. * * * two hexagons are considered colliding if the distance between their centers * is less than the sum of their in-radius (i.e. shortest distance from center to edge). * * * this uses squared distance to avoid unnecessary square root computation. */ isCollidingWithOther(other) { const dx = this.center.x - other.center.x; const dy = this.center.y - other.center.y; const distSq = dx * dx + dy * dy; const minDist = this.inRadius + other.inRadius; return distSq < minDist * minDist; } applyUniformScaleByFactor(args) { const { percentAmount, anchorReference } = args; const scaleFactor = percentAmount; const originalCenter = this.center; this.circumRadius *= scaleFactor; let anchor; switch (anchorReference.mode) { case 'relativeToOrigin': anchor = this.origin.clone(); break; case 'relativeToCenter': anchor = originalCenter; break; case 'relativeToRectCorner': anchor = this.boundingBox.getCornerPoint(anchorReference.cornerKey); break; } const newCenter = this.center; const deltaX = anchor.x - newCenter.x; const deltaY = anchor.y - newCenter.y; this.origin = new Point({ x: this.origin.x + deltaX, y: this.origin.y + deltaY, }); } scaledUniformallyByFactor(args) { const clone = this.clone(); clone.applyUniformScaleByFactor(args); return clone; } // MARK: Alias Init // ---------------- static recenterHexagonsRelativeToPoint(args) { const allPoints = args.hexagons.reduce((acc, curr) => { acc.push(...curr.cornerPointsAsArray); return acc; }, []); const boundingBox = Point.getBoundingBoxForPoints(allPoints); const currentCenter = boundingBox.center; const pointAdj = currentCenter.getDelta(args.centerPoint); args.hexagons.forEach(hexagon => { const adjX = hexagon.origin.x - pointAdj.x; const adjY = hexagon.origin.y - pointAdj.y; hexagon.origin = new Point({ x: adjX, y: adjY, }); }); } ; static initFromValue(args) { return new BoxedHexagon({ mode: 'relativeToCenter', ...args, }); } ; static initFromPresetHexagon(args) { // NOTE: // Flat Topped Hexagon: 0°, 60°, 120°, 180°, 240°, 300°... (offset of 60) // Pointy-topped Hexagons: 30°, 90°, 150°, 210°, 270°, 330°... (offset of 30) // const angleOffset = new Angle({ angleUnit: 'degrees', angleValue: args.hexagonType === 'flatTopped' ? 0 : 30, }); const hexagon = new BoxedHexagon(args); hexagon.startAngleOffset = angleOffset; return hexagon; } ; } ;