import { axisAlignedBoundingBox } from "../collision/aa.ts"; import { Point, Vector } from "./vector.ts"; export class SplineSegment { points: [Vector, Vector, Vector, Vector]; length: number; constructor(points: [Vector, Vector, Vector, Vector]) { this.points = points; this.length = this.calculateApproxLength(100); } draw(color?: string) { const [a, b, c, d] = this.points; doodler.drawBezier(a, b, c, d, { strokeColor: color || "#ffffff50", }); } getPointAtT(t: number) { const [a, b, c, d] = this.points; const res = a.copy(); res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t)); res.add( Vector.add( Vector.add(a.copy().mult(3), b.copy().mult(-6)), c.copy().mult(3), ).mult(Math.pow(t, 2)), ); res.add( Vector.add( Vector.add(a.copy().mult(-1), b.copy().mult(3)), Vector.add(c.copy().mult(-3), d.copy()), ).mult(Math.pow(t, 3)), ); return res; } getClosestPoint(v: Vector): [Vector, number, number] { const samples = 25; const resolution = 1 / samples; let closest = this.points[0]; let closestDistance = this.points[0].dist(v); let closestT = 0; for (let i = 0; i < samples; i++) { const point = this.getPointAtT(i * resolution); const distance = v.dist(point); if (distance < closestDistance) { closest = point; closestDistance = distance; closestT = i * resolution; } } return [closest, closestDistance, closestT]; } getPointsWithinRadius(v: Vector, r: number) { const points: [number, SplineSegment][] = []; const samples = 25; const resolution = 1 / samples; for (let i = 0; i < samples + 1; i++) { const point = this.getPointAtT(i * resolution); const distance = v.dist(point); if (distance < r) { points.push([i * resolution, this]); } } return points; } tangent(t: number) { // dP(t) / dt = -3(1-t)^2 * P0 + 3(1-t)^2 * P1 - 6t(1-t) * P1 - 3t^2 * P2 + 6t(1-t) * P2 + 3t^2 * P3 const [a, b, c, d] = this.points; const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2)); res.add( Vector.add( Vector.sub(c, b).mult(6 * (1 - t) * t), Vector.sub(d, c).mult(3 * Math.pow(t, 2)), ), ); return res; } doesIntersectCircle(x: number, y: number, r: number) { const v = new Vector(x, y); const samples = 25; const resolution = 1 / samples; let distance = Infinity; let t; for (let i = 0; i < samples - 1; i++) { const a = this.getPointAtT(i * resolution); const b = this.getPointAtT((i + 1) * resolution); const ac = Vector.sub(v, a); const ab = Vector.sub(b, a); const d = Vector.add(Vector.vectorProjection(ac, ab), a); const ad = Vector.sub(d, a); const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y; let dist; if (k <= 0.0) { dist = Vector.hypot2(v, a); } else if (k >= 1.0) { dist = Vector.hypot2(v, b); } dist = Vector.hypot2(v, d); if (dist < distance) { distance = dist; t = i * resolution; } } if (distance < r) return t; return false; } intersectsCircle(circleCenter: Point, radius: number): boolean { const numSegments = 100; // Initial number of segments const minResolution = 10; // Minimum resolution to ensure accuracy for (let i = 0; i < numSegments; i++) { const t1 = i / numSegments; const t2 = (i + 1) / numSegments; const segmentStart = this.getPointAtT(t1); const segmentEnd = this.getPointAtT(t2); const segmentLength = Math.sqrt( (segmentEnd.x - segmentStart.x) ** 2 + (segmentEnd.y - segmentStart.y) ** 2, ); // Dynamically adjust resolution based on segment length const resolution = Math.max( minResolution, Math.ceil(numSegments * (segmentLength / radius)), ); for (let j = 0; j <= resolution; j++) { const t = j / resolution; const point = this.getPointAtT(t); const distance = Math.sqrt( (point.x - circleCenter.x) ** 2 + (point.y - circleCenter.y) ** 2, ); if (distance <= radius) { return true; // Intersection detected } } } return false; // No intersection found } calculateApproxLength(resolution = 25) { const stepSize = 1 / resolution; const points: Vector[] = []; for (let i = 0; i <= resolution; i++) { const current = stepSize * i; points.push(this.getPointAtT(current)); } this.length = points.reduce((acc: { prev?: Vector; length: number }, cur) => { const prev = acc.prev; acc.prev = cur; if (!prev) return acc; acc.length += cur.dist(prev); return acc; }, { prev: undefined, length: 0 }).length; return this.length; } calculateEvenlySpacedPoints(spacing: number, resolution = 1) { const points: Vector[] = []; points.push(this.points[0]); let prev = points[0]; let distSinceLastEvenPoint = 0; let t = 0; const div = Math.ceil(this.length * resolution * 10); while (t < 1) { t += 1 / div; const point = this.getPointAtT(t); distSinceLastEvenPoint += prev.dist(point); if (distSinceLastEvenPoint >= spacing) { const overshoot = distSinceLastEvenPoint - spacing; const evenPoint = Vector.add( point, Vector.sub(point, prev).normalize().mult(overshoot), ); distSinceLastEvenPoint = overshoot; points.push(evenPoint); prev = evenPoint; } prev = point; } return points; } private _aabb?: axisAlignedBoundingBox; get AABB() { if (!this._aabb) { this._aabb = this.recalculateAABB(); } return this._aabb; } recalculateAABB(): axisAlignedBoundingBox { const numPoints = 100; // You can adjust the number of points based on your needs let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (let i = 0; i < numPoints; i++) { const t = i / numPoints; const point = this.getPointAtT(t); minX = Math.min(minX, point.x); minY = Math.min(minY, point.y); maxX = Math.max(maxX, point.x); maxY = Math.max(maxY, point.y); } return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; } }