Skip to content

Commit d6afe66

Browse files
authored
Merge pull request #1 from sojs-coder:dev/init
Dev/init
2 parents 93b1b96 + a997f38 commit d6afe66

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+38974
-5003
lines changed

Math/LineIntersections.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* @author Peter Kelley
3+
* @author pgkelley4@gmail.com
4+
*/
5+
6+
import type { Vector } from "./Vector";
7+
8+
/**
9+
* See if two line segments intersect. This uses the
10+
* vector cross product approach described below:
11+
* http://stackoverflow.com/a/565282/786339
12+
*
13+
* @param {Object} p point object with x and y coordinates
14+
* representing the start of the 1st line.
15+
* @param {Object} p2 point object with x and y coordinates
16+
* representing the end of the 1st line.
17+
* @param {Object} q point object with x and y coordinates
18+
* representing the start of the 2nd line.
19+
* @param {Object} q2 point object with x and y coordinates
20+
* representing the end of the 2nd line.
21+
*/
22+
23+
export interface Point {
24+
x: number;
25+
y: number;
26+
}
27+
28+
export function getLineSegmentsIntersection(p1: Vector, p2: Vector, p3: Vector, p4: Vector): Point | null {
29+
const d = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x);
30+
31+
if (d === 0) {
32+
// Lines are parallel or collinear
33+
return null; // For now, we don't handle collinear intersections
34+
}
35+
36+
const t = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / d;
37+
const u = -((p1.x - p3.x) * (p2.y - p1.y) - (p1.y - p3.y) * (p2.x - p1.x)) / d;
38+
39+
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
40+
return {
41+
x: p1.x + t * (p2.x - p1.x),
42+
y: p1.y + t * (p2.y - p1.y),
43+
};
44+
}
45+
46+
return null;
47+
}
48+
49+
/**
50+
* Calculate the cross product of the two points.
51+
*
52+
* @param {Object} point1 point object with x and y coordinates
53+
* @param {Object} point2 point object with x and y coordinates
54+
*
55+
* @return the cross product result as a float
56+
*/
57+
function crossProduct(point1: Point, point2: Point): number {
58+
return point1.x * point2.y - point1.y * point2.x;
59+
}
60+
61+
/**
62+
* Subtract the second point from the first.
63+
*
64+
* @param {Object} point1 point object with x and y coordinates
65+
* @param {Object} point2 point object with x and y coordinates
66+
*
67+
* @return the subtraction result as a point object
68+
*/
69+
function subtractPoints(point1: Point, point2: Point): Point {
70+
return {
71+
x: point1.x - point2.x,
72+
y: point1.y - point2.y
73+
}
74+
}
75+
76+
/**
77+
* See if the points are equal.
78+
*
79+
* @param {Object} point1 point object with x and y coordinates
80+
* @param {Object} point2 point object with x and y coordinates
81+
*
82+
* @return if the points are equal
83+
*/
84+
function equalPoints(point1: Point, point2: Point): boolean {
85+
return (point1.x == point2.x) && (point1.y == point2.y)
86+
}
87+
88+
/**
89+
* See if all arguments are equal.
90+
*
91+
* @param {...} args arguments that will be compared by '=='.
92+
*
93+
* @return if all arguments are equal
94+
*/
95+
function allEqual(...args: any[]): boolean {
96+
var firstValue = args[0],
97+
i;
98+
for (i = 1; i < args.length; i += 1) {
99+
if (args[i] != firstValue) {
100+
return false;
101+
}
102+
}
103+
return true;
104+
}
105+
106+
107+
108+
/** Modifications by @sojs-coder
109+
*
110+
*
111+
* - Type annotation
112+
*
113+
*/

Math/PolygonUnion.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as polygonClipping from 'polygon-clipping';
2+
import { Vector } from './Vector';
3+
4+
/**
5+
* Unions an array of polygons (each a Vector[]) and returns an array of resulting polygons (Vector[][]).
6+
* Always returns Vector[][], even for empty or single input.
7+
*/
8+
export function unionPolygons(polygons: Vector[][]): Vector[][] {
9+
if (polygons.length === 0) return [];
10+
if (polygons.length === 1) return [polygons[0]];
11+
try {
12+
const convertedPolygons: polygonClipping.Polygon[] = polygons.map(polygon => {
13+
const coords = polygon.map(vertex => vertex.toArray());
14+
if (coords.length > 0) {
15+
const first = coords[0];
16+
const last = coords[coords.length - 1];
17+
if (first[0] !== last[0] || first[1] !== last[1]) {
18+
coords.push([first[0], first[1]]);
19+
}
20+
}
21+
return [coords];
22+
});
23+
let result: polygonClipping.MultiPolygon = [convertedPolygons[0]];
24+
for (let i = 1; i < convertedPolygons.length; i++) {
25+
result = polygonClipping.union(result, [convertedPolygons[i]]);
26+
if (!result || result.length === 0) {
27+
console.warn('Union operation resulted in empty geometry at step', i);
28+
return [];
29+
}
30+
}
31+
if (!result || result.length === 0) {
32+
return [];
33+
}
34+
// Convert all resulting polygons to Vector[][]
35+
return result.map(polygon => {
36+
const coords = polygon[0];
37+
let vectors = coords.map((coord: number[]) => new Vector(coord[0], coord[1]));
38+
if (vectors.length > 1) {
39+
const first = vectors[0];
40+
const last = vectors[vectors.length - 1];
41+
if (first.x === last.x && first.y === last.y) {
42+
vectors = vectors.slice(0, -1);
43+
}
44+
}
45+
return vectors;
46+
});
47+
} catch (error) {
48+
console.error('Error during polygon union:', error);
49+
// Fallback: return all input polygons
50+
return polygons;
51+
}
52+
}
53+
54+
// Helper function to calculate polygon area from coordinate array
55+
function calculatePolygonArea(coords: number[][]): number {
56+
if (coords.length < 3) return 0;
57+
let area = 0;
58+
for (let i = 0; i < coords.length; i++) {
59+
const j = (i + 1) % coords.length;
60+
area += coords[i][0] * coords[j][1];
61+
area -= coords[j][0] * coords[i][1];
62+
}
63+
return Math.abs(area) / 2;
64+
}

Math/SpatialGrid.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
import type { Collider } from '../Parts/Children/Collider';
3+
import type { Vector } from './Vector';
4+
5+
export class SpatialGrid {
6+
private cells: Map<string, Collider[]>;
7+
private cellSize: number;
8+
9+
constructor(cellSize: number) {
10+
this.cells = new Map();
11+
this.cellSize = cellSize;
12+
}
13+
14+
private getKey(x: number, y: number): string {
15+
return `${Math.floor(x / this.cellSize)}_${Math.floor(y / this.cellSize)}`;
16+
}
17+
18+
clear() {
19+
this.cells.clear();
20+
}
21+
22+
insert(collider: Collider) {
23+
const start = collider.realWorldStart;
24+
const end = collider.realWorldEnd;
25+
26+
const startX = Math.floor(start.x / this.cellSize);
27+
const startY = Math.floor(start.y / this.cellSize);
28+
const endX = Math.floor(end.x / this.cellSize);
29+
const endY = Math.floor(end.y / this.cellSize);
30+
31+
for (let x = startX; x <= endX; x++) {
32+
for (let y = startY; y <= endY; y++) {
33+
const key = `${x}_${y}`;
34+
if (!this.cells.has(key)) {
35+
this.cells.set(key, []);
36+
}
37+
this.cells.get(key)!.push(collider);
38+
}
39+
}
40+
}
41+
42+
query(collider: Collider): Collider[] {
43+
const candidates = new Set<Collider>();
44+
const start = collider.realWorldStart;
45+
const end = collider.realWorldEnd;
46+
47+
const startX = Math.floor(start.x / this.cellSize);
48+
const startY = Math.floor(start.y / this.cellSize);
49+
const endX = Math.floor(end.x / this.cellSize);
50+
const endY = Math.floor(end.y / this.cellSize);
51+
52+
for (let x = startX; x <= endX; x++) {
53+
for (let y = startY; y <= endY; y++) {
54+
const key = `${x}_${y}`;
55+
if (this.cells.has(key)) {
56+
for (const other of this.cells.get(key)!) {
57+
candidates.add(other);
58+
}
59+
}
60+
}
61+
}
62+
return Array.from(candidates);
63+
}
64+
}

Math/Vector.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Point } from "./LineIntersections";
2+
13
export class Vector {
24
x: number;
35
y: number;
@@ -54,7 +56,9 @@ export class Vector {
5456
}
5557
normalize(): Vector {
5658
const len = this.length();
57-
if (len === 0) throw new Error("Cannot normalize zero-length vector");
59+
if (len === 0) {
60+
return new Vector(0, 0);
61+
}
5862
return new Vector(this.x / len, this.y / len);
5963
}
6064
dot(other: Vector): number {
@@ -80,7 +84,11 @@ export class Vector {
8084
this.y += other.y;
8185
return this;
8286
}
83-
static From(scalar: number): Vector {
84-
return new Vector(scalar, scalar);
87+
static From(scalar: number | Point): Vector {
88+
if (typeof scalar === "number") {
89+
return new Vector(scalar, scalar);
90+
} else {
91+
return new Vector(scalar.x, scalar.y);
92+
}
8593
}
8694
}

Parts/CharacterMovement.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class CharacterMovement extends Part {
1616
this.type = "CharacterMovement";
1717
}
1818

19-
act(_delta: number): void {
19+
act(delta: number): void {
2020
if (!this.input) {
2121
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;
2222
return;
@@ -26,7 +26,7 @@ export class CharacterMovement extends Part {
2626
if (!transform) {
2727
return;
2828
}
29-
29+
const speed = this.speed * delta;
3030
const keys = this.input.downkeys;
3131
let dx = 0;
3232
let dy = 0;
@@ -66,7 +66,7 @@ export class CharacterMovement extends Part {
6666
dy *= Math.SQRT1_2;
6767
}
6868
if (dx !== 0 || dy !== 0) {
69-
transform.move(new Vector(dx * this.speed, dy * this.speed));
69+
transform.move(new Vector(dx * speed, dy * speed));
7070
}
7171
}
7272
}

Parts/Children/BoxCollider.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { Game } from "../Game";
33
import { Part } from "../Part";
44
import { Collider } from "./Collider";
55
import { Transform } from "./Transform";
6+
import { MultiPolygonCollider } from "./MultiPolygonCollider";
67
import { PolygonCollider } from "./PolygonCollider";
8+
import type { Polygon } from "martinez-polygon-clipping";
79

810
export class BoxCollider extends Collider {
911
start: Vector;
@@ -16,8 +18,8 @@ export class BoxCollider extends Collider {
1618
private lastRotation: number = NaN;
1719
private lastScale: Vector = new Vector(NaN, NaN);
1820

19-
constructor({ width, height, tag }: { width: number; height: number; tag?: string }) {
20-
super({ tag });
21+
constructor({ width, height, tag = "<Untagged>" }: { width: number; height: number; tag?: string }) {
22+
super({ tag, allowMerge: tag !== '<Untagged>' });
2123
this.name = "BoxCollider";
2224
this.width = width;
2325
this.height = height;
@@ -36,6 +38,10 @@ export class BoxCollider extends Collider {
3638
return this.rotatedCorners;
3739
}
3840

41+
getGeometry(): Polygon {
42+
return [this.worldVertices.map(v => v.toArray())];
43+
}
44+
3945
updateCollider(transform: Transform) {
4046
const cos = Math.cos(transform.rotation);
4147
const sin = Math.sin(transform.rotation);
@@ -78,7 +84,7 @@ export class BoxCollider extends Collider {
7884
narrowPhaseCheck(other: Collider): boolean {
7985
if (other instanceof BoxCollider) {
8086
return this.checkBoxVsBox(this, other);
81-
} else if (other instanceof PolygonCollider) {
87+
} else if (other instanceof PolygonCollider || other instanceof MultiPolygonCollider) {
8288
return other.narrowPhaseCheck(this);
8389
}
8490

@@ -96,6 +102,19 @@ export class BoxCollider extends Collider {
96102
return true;
97103
}
98104

105+
override _updateVerticesAfterMerge(polygons: Vector[][][]): void {
106+
const newCollider = new MultiPolygonCollider({ polygons, tag: this.tag });
107+
108+
newCollider.active = this.active;
109+
newCollider.allowMerge = this.allowMerge;
110+
111+
const parent = this.parent;
112+
if (parent) {
113+
parent.removeChild(this);
114+
parent.addChild(newCollider);
115+
}
116+
}
117+
99118
act(delta: number) {
100119
super.act(delta);
101120
}

0 commit comments

Comments
 (0)