UNPKG

check2d

Version:

Polygons, Ellipses, Circles, Boxes, Lines, Points. Ray-Casting, offsets, rotation, scaling, padding, groups.

432 lines (431 loc) 14 kB
"use strict"; /* tslint:disable:cyclomatic-complexity */ Object.defineProperty(exports, "__esModule", { value: true }); exports.EPSILON = exports.RAD2DEG = exports.DEG2RAD = void 0; exports.deg2rad = deg2rad; exports.rad2deg = rad2deg; exports.almostEqual = almostEqual; exports.pointsEqual = pointsEqual; exports.getWorldPoints = getWorldPoints; exports.createEllipse = createEllipse; exports.createBox = createBox; exports.ensureVectorPoint = ensureVectorPoint; exports.ensurePolygonPoints = ensurePolygonPoints; exports.distance = distance; exports.clockwise = clockwise; exports.extendBody = extendBody; exports.bodyMoved = bodyMoved; exports.notIntersectAABB = notIntersectAABB; exports.intersectAABB = intersectAABB; exports.canInteract = canInteract; exports.checkAInB = checkAInB; exports.clonePointsArray = clonePointsArray; exports.mapVectorToArray = mapVectorToArray; exports.mapArrayToVector = mapArrayToVector; exports.getBounceDirection = getBounceDirection; exports.getSATTest = getSATTest; exports.dashLineTo = dashLineTo; exports.drawPolygon = drawPolygon; exports.drawBVH = drawBVH; exports.cloneResponse = cloneResponse; exports.returnTrue = returnTrue; exports.getGroup = getGroup; exports.bin2dec = bin2dec; exports.ensureNumber = ensureNumber; exports.groupBits = groupBits; exports.move = move; const model_1 = require("./model"); const intersect_1 = require("./intersect"); const optimized_1 = require("./optimized"); const sat_1 = require("sat"); /* helpers for faster getSATTest() and checkAInB() */ const testMap = { satCircleCircle: sat_1.testCircleCircle, satCirclePolygon: sat_1.testCirclePolygon, satPolygonCircle: sat_1.testPolygonCircle, satPolygonPolygon: sat_1.testPolygonPolygon, inCircleCircle: intersect_1.circleInCircle, inCirclePolygon: intersect_1.circleInPolygon, inPolygonCircle: intersect_1.polygonInCircle, inPolygonPolygon: intersect_1.polygonInPolygon }; function createArray(bodyType, testType) { const arrayResult = []; const bodyGroups = Object.values(model_1.BodyGroup).filter((value) => typeof value === 'number'); (0, optimized_1.forEach)(bodyGroups, (bodyGroup) => { arrayResult[bodyGroup] = (bodyGroup === model_1.BodyGroup.Circle ? testMap[`${testType}${bodyType}Circle`] : testMap[`${testType}${bodyType}Polygon`]); }); return arrayResult; } const circleSATFunctions = createArray(model_1.BodyType.Circle, 'sat'); const circleInFunctions = createArray(model_1.BodyType.Circle, 'in'); const polygonSATFunctions = createArray(model_1.BodyType.Polygon, 'sat'); const polygonInFunctions = createArray(model_1.BodyType.Polygon, 'in'); exports.DEG2RAD = Math.PI / 180; exports.RAD2DEG = 180 / Math.PI; exports.EPSILON = 1e-9; /** * convert from degrees to radians */ function deg2rad(degrees) { return degrees * exports.DEG2RAD; } /** * convert from radians to degrees */ function rad2deg(radians) { return radians * exports.RAD2DEG; } /** * Compares two numbers for approximate equality within a given tolerance. * * Useful for floating-point calculations where exact equality (`===`) * is unreliable due to rounding errors. * * @param {number} a - First number to compare * @param {number} b - Second number to compare * @param {number} [eps=EPSILON] - Allowed tolerance (default: global EPSILON) * @returns {boolean} `true` if numbers differ by less than `eps` */ function almostEqual(a, b, eps = exports.EPSILON) { return Math.abs(a - b) < eps; } /** * Compares two vectors for approximate equality within a tolerance. * * Uses {@link almostEqual} on both `x` and `y` coordinates. * Two points are considered equal if both coordinates are * within the allowed tolerance. * * @param {Vector} a - First vector * @param {Vector} b - Second vector * @returns {boolean} `true` if both vectors are approximately equal */ function pointsEqual(a, b) { return almostEqual(a.x, b.x) && almostEqual(a.y, b.y); } /** * Converts calcPoints into simple x/y Vectors and adds polygon pos to them * * @param {BasePolygon} polygon * @returns {Vector[]} */ function getWorldPoints({ calcPoints, pos }) { return (0, optimized_1.map)(calcPoints, ({ x, y }) => ({ x: x + pos.x, y: y + pos.y })); } /** * creates ellipse-shaped polygon based on params */ function createEllipse(radiusX, radiusY = radiusX, step = 1) { const steps = Math.PI * Math.hypot(radiusX, radiusY) * 2; const length = Math.max(8, Math.ceil(steps / Math.max(1, step))); const ellipse = []; for (let index = 0; index < length; index++) { const value = (index / length) * 2 * Math.PI; const x = Math.cos(value) * radiusX; const y = Math.sin(value) * radiusY; ellipse.push(new model_1.SATVector(x, y)); } return ellipse; } /** * creates box shaped polygon points */ function createBox(width, height) { return [ new model_1.SATVector(0, 0), new model_1.SATVector(width, 0), new model_1.SATVector(width, height), new model_1.SATVector(0, height) ]; } /** * ensure SATVector type point result */ function ensureVectorPoint(point = {}) { return point instanceof model_1.SATVector ? point : new model_1.SATVector(point.x || 0, point.y || 0); } /** * ensure Vector points (for polygon) in counter-clockwise order */ function ensurePolygonPoints(points = []) { const polygonPoints = (0, optimized_1.map)(points, ensureVectorPoint); return clockwise(polygonPoints) ? polygonPoints.reverse() : polygonPoints; } /** * get distance between two Vector points */ function distance(bodyA, bodyB) { const xDiff = bodyA.x - bodyB.x; const yDiff = bodyA.y - bodyB.y; return Math.hypot(xDiff, yDiff); } /** * check [is clockwise] direction of polygon */ function clockwise(points) { const length = points.length; let sum = 0; (0, optimized_1.forEach)(points, (v1, index) => { const v2 = points[(index + 1) % length]; sum += (v2.x - v1.x) * (v2.y + v1.y); }); return sum > 0; } /** * used for all types of bodies in constructor */ function extendBody(body, options = {}) { var _a; body.isStatic = !!options.isStatic; body.isTrigger = !!options.isTrigger; body.padding = options.padding || 0; // Default value should be reflected in documentation of `BodyOptions.group` body.group = (_a = options.group) !== null && _a !== void 0 ? _a : 0x7fffffff; if ('userData' in options) { body.userData = options.userData; } if (options.isCentered && body.typeGroup !== model_1.BodyGroup.Circle) { body.isCentered = true; } if (options.angle) { body.setAngle(options.angle); } } /** * check if body moved outside of its padding */ function bodyMoved(body) { const { bbox, minX, minY, maxX, maxY } = body; return (bbox.minX < minX || bbox.minY < minY || bbox.maxX > maxX || bbox.maxY > maxY); } /** * returns true if two boxes not intersect */ function notIntersectAABB(bodyA, bodyB) { return (bodyB.minX > bodyA.maxX || bodyB.minY > bodyA.maxY || bodyB.maxX < bodyA.minX || bodyB.maxY < bodyA.minY); } /** * checks if two boxes intersect */ function intersectAABB(bodyA, bodyB) { return !notIntersectAABB(bodyA, bodyB); } /** * checks if two bodies can interact (for collision filtering) * * Based on {@link https://box2d.org/documentation/md_simulation.html#filtering Box2D} * ({@link https://aurelienribon.wordpress.com/2011/07/01/box2d-tutorial-collision-filtering/ tutorial}) * * @param bodyA * @param bodyB * * @example * const body1 = { group: 0b00000000_00000000_00000001_00000000 } * const body2 = { group: 0b11111111_11111111_00000011_00000000 } * const body3 = { group: 0b00000010_00000000_00000100_00000000 } * * // Body 1 has the first custom group but cannot interact with any other groups * // except itself because the first 16 bits are all zeros, only bodies with an * // identical value can interact with it. * canInteract(body1, body1) // returns true (identical groups can always interact) * canInteract(body1, body2) // returns false * canInteract(body1, body3) // returns false * * // Body 2 has the first and second group and can interact with all other * // groups, but only if that body also can interact with is custom group. * canInteract(body2, body1) // returns false (body1 cannot interact with others) * canInteract(body2, body2) // returns true (identical groups can always interact) * canInteract(body2, body3) // returns true * * // Body 3 has the third group but can interact with the second group. * // This means that Body 2 and Body 3 can interact with each other but no other * // body can interact with Body 1 because it doesn't allow interactions with * // any other custom group. * canInteract(body3, body1) // returns false (body1 cannot interact with others) * canInteract(body3, body2) // returns true * canInteract(body3, body3) // returns true (identical groups can always interact) */ function canInteract({ group: groupA }, { group: groupB }) { const categoryA = groupA >> 16; const categoryB = groupB >> 16; const maskA = groupA & 0xffff; const maskB = groupB & 0xffff; return (categoryA & maskB) !== 0 && (categoryB & maskA) !== 0; // Box2D rules } /** * checks if body a is in body b */ function checkAInB(bodyA, bodyB) { const check = bodyA.typeGroup === model_1.BodyGroup.Circle ? circleInFunctions : polygonInFunctions; return check[bodyB.typeGroup](bodyA, bodyB); } /** * clone sat vector points array into vector points array */ function clonePointsArray(points) { return (0, optimized_1.map)(points, ({ x, y }) => ({ x, y })); } /** * change format from SAT.js to poly-decomp * * @param position */ function mapVectorToArray({ x, y } = { x: 0, y: 0 }) { return [x, y]; } /** * change format from poly-decomp to SAT.js * * @param positionAsArray */ function mapArrayToVector([x, y] = [0, 0]) { return { x, y }; } /** * given 2 bodies calculate vector of bounce assuming equal mass and they are circles */ function getBounceDirection(body, collider) { const v2 = new model_1.SATVector(collider.x - body.x, collider.y - body.y); const v1 = new model_1.SATVector(body.x - collider.x, body.y - collider.y); const len = v1.dot(v2.normalize()) * 2; return new model_1.SATVector(v2.x * len - v1.x, v2.y * len - v1.y).normalize(); } /** * returns correct sat.js testing function based on body types */ function getSATTest(bodyA, bodyB) { const check = bodyA.typeGroup === model_1.BodyGroup.Circle ? circleSATFunctions : polygonSATFunctions; return check[bodyB.typeGroup]; } /** * draws dashed line on canvas context */ function dashLineTo(context, fromX, fromY, toX, toY, dash = 2, gap = 4) { const xDiff = toX - fromX; const yDiff = toY - fromY; const arc = Math.atan2(yDiff, xDiff); const offsetX = Math.cos(arc); const offsetY = Math.sin(arc); let posX = fromX; let posY = fromY; let dist = Math.hypot(xDiff, yDiff); while (dist > 0) { const step = Math.min(dist, dash); context.moveTo(posX, posY); context.lineTo(posX + offsetX * step, posY + offsetY * step); posX += offsetX * (dash + gap); posY += offsetY * (dash + gap); dist -= dash + gap; } } /** * draw polygon * * @param context * @param polygon * @param isTrigger */ function drawPolygon(context, { pos, calcPoints }, isTrigger = false) { const lastPoint = calcPoints[calcPoints.length - 1]; const fromX = pos.x + lastPoint.x; const fromY = pos.y + lastPoint.y; if (calcPoints.length === 1) { context.arc(fromX, fromY, 1, 0, Math.PI * 2); } else { context.moveTo(fromX, fromY); } (0, optimized_1.forEach)(calcPoints, (point, index) => { const toX = pos.x + point.x; const toY = pos.y + point.y; if (isTrigger) { const prev = calcPoints[index - 1] || lastPoint; dashLineTo(context, pos.x + prev.x, pos.y + prev.y, toX, toY); } else { context.lineTo(toX, toY); } }); } /** * draw body bounding body box */ function drawBVH(context, body, isTrigger = true) { drawPolygon(context, { pos: { x: body.minX, y: body.minY }, calcPoints: createBox(body.maxX - body.minX, body.maxY - body.minY) }, isTrigger); } /** * clone response object returning new response with previous ones values */ function cloneResponse(response) { const clone = new model_1.Response(); const { a, b, overlap, overlapN, overlapV, aInB, bInA } = response; clone.a = a; clone.b = b; clone.overlap = overlap; clone.overlapN = overlapN.clone(); clone.overlapV = overlapV.clone(); clone.aInB = aInB; clone.bInA = bInA; return clone; } /** * dummy fn used as default, for optimization */ function returnTrue() { return true; } /** * for groups */ function getGroup(group) { return Math.max(0, Math.min(group, 0x7fffffff)); } /** * binary string to decimal number */ function bin2dec(binary) { return Number(`0b${binary}`.replace(/\s/g, '')); } /** * helper for groupBits() * * @param input - number or binary string */ function ensureNumber(input) { return typeof input === 'number' ? input : bin2dec(input); } /** * create group bits from category and mask * * @param category - category bits * @param mask - mask bits (default: category) */ function groupBits(category, mask = category) { return (ensureNumber(category) << 16) | ensureNumber(mask); } function move(body, speed = 1, updateNow = true) { if (!speed) { return; } const moveX = Math.cos(body.angle) * speed; const moveY = Math.sin(body.angle) * speed; body.setPosition(body.x + moveX, body.y + moveY, updateNow); }