Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions Math/LineIntersections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @author Peter Kelley
* @author pgkelley4@gmail.com
*/

import type { Vector } from "./Vector";

/**
* See if two line segments intersect. This uses the
* vector cross product approach described below:
* http://stackoverflow.com/a/565282/786339
*
* @param {Object} p point object with x and y coordinates
* representing the start of the 1st line.
* @param {Object} p2 point object with x and y coordinates
* representing the end of the 1st line.
* @param {Object} q point object with x and y coordinates
* representing the start of the 2nd line.
* @param {Object} q2 point object with x and y coordinates
* representing the end of the 2nd line.
*/

export interface Point {
x: number;
y: number;
}

export function getLineSegmentsIntersection(p1: Vector, p2: Vector, p3: Vector, p4: Vector): Point | null {
const d = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x);

if (d === 0) {
// Lines are parallel or collinear
return null; // For now, we don't handle collinear intersections
}

const t = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / d;
const u = -((p1.x - p3.x) * (p2.y - p1.y) - (p1.y - p3.y) * (p2.x - p1.x)) / d;

if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
return {
x: p1.x + t * (p2.x - p1.x),
y: p1.y + t * (p2.y - p1.y),
};
}

return null;
}

/**
* Calculate the cross product of the two points.
*
* @param {Object} point1 point object with x and y coordinates
* @param {Object} point2 point object with x and y coordinates
*
* @return the cross product result as a float
*/
function crossProduct(point1: Point, point2: Point): number {
return point1.x * point2.y - point1.y * point2.x;
}

/**
* Subtract the second point from the first.
*
* @param {Object} point1 point object with x and y coordinates
* @param {Object} point2 point object with x and y coordinates
*
* @return the subtraction result as a point object
*/
function subtractPoints(point1: Point, point2: Point): Point {
return {
x: point1.x - point2.x,
y: point1.y - point2.y
}
}

/**
* See if the points are equal.
*
* @param {Object} point1 point object with x and y coordinates
* @param {Object} point2 point object with x and y coordinates
*
* @return if the points are equal
*/
function equalPoints(point1: Point, point2: Point): boolean {
return (point1.x == point2.x) && (point1.y == point2.y)
}

/**
* See if all arguments are equal.
*
* @param {...} args arguments that will be compared by '=='.
*
* @return if all arguments are equal
*/
function allEqual(...args: any[]): boolean {
var firstValue = args[0],
i;
for (i = 1; i < args.length; i += 1) {
if (args[i] != firstValue) {
return false;
}
}
return true;
}



/** Modifications by @sojs-coder
*
*
* - Type annotation
*
*/
64 changes: 64 additions & 0 deletions Math/PolygonUnion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as polygonClipping from 'polygon-clipping';
import { Vector } from './Vector';

/**
* Unions an array of polygons (each a Vector[]) and returns an array of resulting polygons (Vector[][]).
* Always returns Vector[][], even for empty or single input.
*/
export function unionPolygons(polygons: Vector[][]): Vector[][] {
if (polygons.length === 0) return [];
if (polygons.length === 1) return [polygons[0]];
try {
const convertedPolygons: polygonClipping.Polygon[] = polygons.map(polygon => {
const coords = polygon.map(vertex => vertex.toArray());
if (coords.length > 0) {
const first = coords[0];
const last = coords[coords.length - 1];
if (first[0] !== last[0] || first[1] !== last[1]) {
coords.push([first[0], first[1]]);
}
}
return [coords];
});
let result: polygonClipping.MultiPolygon = [convertedPolygons[0]];
for (let i = 1; i < convertedPolygons.length; i++) {
result = polygonClipping.union(result, [convertedPolygons[i]]);
if (!result || result.length === 0) {
console.warn('Union operation resulted in empty geometry at step', i);
return [];
}
}
if (!result || result.length === 0) {
return [];
}
// Convert all resulting polygons to Vector[][]
return result.map(polygon => {
const coords = polygon[0];
let vectors = coords.map((coord: number[]) => new Vector(coord[0], coord[1]));
if (vectors.length > 1) {
const first = vectors[0];
const last = vectors[vectors.length - 1];
if (first.x === last.x && first.y === last.y) {
vectors = vectors.slice(0, -1);
}
}
return vectors;
});
} catch (error) {
console.error('Error during polygon union:', error);
// Fallback: return all input polygons
return polygons;
}
}

// Helper function to calculate polygon area from coordinate array
function calculatePolygonArea(coords: number[][]): number {
if (coords.length < 3) return 0;
let area = 0;
for (let i = 0; i < coords.length; i++) {
const j = (i + 1) % coords.length;
area += coords[i][0] * coords[j][1];
area -= coords[j][0] * coords[i][1];
}
return Math.abs(area) / 2;
}
64 changes: 64 additions & 0 deletions Math/SpatialGrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

import type { Collider } from '../Parts/Children/Collider';
import type { Vector } from './Vector';

export class SpatialGrid {
private cells: Map<string, Collider[]>;
private cellSize: number;

constructor(cellSize: number) {
this.cells = new Map();
this.cellSize = cellSize;
}

private getKey(x: number, y: number): string {
return `${Math.floor(x / this.cellSize)}_${Math.floor(y / this.cellSize)}`;
}

clear() {
this.cells.clear();
}

insert(collider: Collider) {
const start = collider.realWorldStart;
const end = collider.realWorldEnd;

const startX = Math.floor(start.x / this.cellSize);
const startY = Math.floor(start.y / this.cellSize);
const endX = Math.floor(end.x / this.cellSize);
const endY = Math.floor(end.y / this.cellSize);

for (let x = startX; x <= endX; x++) {
for (let y = startY; y <= endY; y++) {
const key = `${x}_${y}`;
if (!this.cells.has(key)) {
this.cells.set(key, []);
}
this.cells.get(key)!.push(collider);
}
}
}

query(collider: Collider): Collider[] {
const candidates = new Set<Collider>();
const start = collider.realWorldStart;
const end = collider.realWorldEnd;

const startX = Math.floor(start.x / this.cellSize);
const startY = Math.floor(start.y / this.cellSize);
const endX = Math.floor(end.x / this.cellSize);
const endY = Math.floor(end.y / this.cellSize);

for (let x = startX; x <= endX; x++) {
for (let y = startY; y <= endY; y++) {
const key = `${x}_${y}`;
if (this.cells.has(key)) {
for (const other of this.cells.get(key)!) {
candidates.add(other);
}
}
}
}
return Array.from(candidates);
}
}
14 changes: 11 additions & 3 deletions Math/Vector.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Point } from "./LineIntersections";

export class Vector {
x: number;
y: number;
Expand Down Expand Up @@ -54,7 +56,9 @@ export class Vector {
}
normalize(): Vector {
const len = this.length();
if (len === 0) throw new Error("Cannot normalize zero-length vector");
if (len === 0) {
return new Vector(0, 0);
}
return new Vector(this.x / len, this.y / len);
}
dot(other: Vector): number {
Expand All @@ -80,7 +84,11 @@ export class Vector {
this.y += other.y;
return this;
}
static From(scalar: number): Vector {
return new Vector(scalar, scalar);
static From(scalar: number | Point): Vector {
if (typeof scalar === "number") {
return new Vector(scalar, scalar);
} else {
return new Vector(scalar.x, scalar.y);
}
}
}
6 changes: 3 additions & 3 deletions Parts/CharacterMovement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class CharacterMovement extends Part {
this.type = "CharacterMovement";
}

act(_delta: number): void {
act(delta: number): void {
if (!this.input) {
if (!this.warned.has("MissingInput")) this.top?.warn(`CharacterMovement <${this.name}> (${this.id}) is missing an input property. Please create an input on the scene and pass it.`) ? this.warned.add("MissingInput") : null;
return;
Expand All @@ -26,7 +26,7 @@ export class CharacterMovement extends Part {
if (!transform) {
return;
}

const speed = this.speed * delta;
const keys = this.input.downkeys;
let dx = 0;
let dy = 0;
Expand Down Expand Up @@ -66,7 +66,7 @@ export class CharacterMovement extends Part {
dy *= Math.SQRT1_2;
}
if (dx !== 0 || dy !== 0) {
transform.move(new Vector(dx * this.speed, dy * this.speed));
transform.move(new Vector(dx * speed, dy * speed));
}
}
}
25 changes: 22 additions & 3 deletions Parts/Children/BoxCollider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { Game } from "../Game";
import { Part } from "../Part";
import { Collider } from "./Collider";
import { Transform } from "./Transform";
import { MultiPolygonCollider } from "./MultiPolygonCollider";
import { PolygonCollider } from "./PolygonCollider";
import type { Polygon } from "martinez-polygon-clipping";

export class BoxCollider extends Collider {
start: Vector;
Expand All @@ -16,8 +18,8 @@ export class BoxCollider extends Collider {
private lastRotation: number = NaN;
private lastScale: Vector = new Vector(NaN, NaN);

constructor({ width, height, tag }: { width: number; height: number; tag?: string }) {
super({ tag });
constructor({ width, height, tag = "<Untagged>" }: { width: number; height: number; tag?: string }) {
super({ tag, allowMerge: tag !== '<Untagged>' });
this.name = "BoxCollider";
this.width = width;
this.height = height;
Expand All @@ -36,6 +38,10 @@ export class BoxCollider extends Collider {
return this.rotatedCorners;
}

getGeometry(): Polygon {
return [this.worldVertices.map(v => v.toArray())];
}

updateCollider(transform: Transform) {
const cos = Math.cos(transform.rotation);
const sin = Math.sin(transform.rotation);
Expand Down Expand Up @@ -78,7 +84,7 @@ export class BoxCollider extends Collider {
narrowPhaseCheck(other: Collider): boolean {
if (other instanceof BoxCollider) {
return this.checkBoxVsBox(this, other);
} else if (other instanceof PolygonCollider) {
} else if (other instanceof PolygonCollider || other instanceof MultiPolygonCollider) {
return other.narrowPhaseCheck(this);
}

Expand All @@ -96,6 +102,19 @@ export class BoxCollider extends Collider {
return true;
}

override _updateVerticesAfterMerge(polygons: Vector[][][]): void {
const newCollider = new MultiPolygonCollider({ polygons, tag: this.tag });

newCollider.active = this.active;
newCollider.allowMerge = this.allowMerge;

const parent = this.parent;
if (parent) {
parent.removeChild(this);
parent.addChild(newCollider);
}
}

act(delta: number) {
super.act(delta);
}
Expand Down
Loading