"use strict"; // Layer: math/geometry // Shared primitive geometry helpers used by placement, collision, mechanical bodies, // editor hit-testing, and world obstacle queries. (function (global) { const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => { const n = Number(v); return Number.isFinite(n) ? n : fallback; }); const clampValue = global.TarinaiCoreHelpers?.clampNumber || ((v, min, max) => Math.max(min, Math.min(max, v))); function orientedRectAabb(cx, cy, halfW, halfH, angle) { const c = Math.cos(num(angle)); const s = Math.sin(num(angle)); const ex = Math.abs(c) * num(halfW) + Math.abs(s) * num(halfH); const ey = Math.abs(s) * num(halfW) + Math.abs(c) * num(halfH); return { left: num(cx) - ex, right: num(cx) + ex, top: num(cy) - ey, bottom: num(cy) + ey, cos: c, sin: s }; } function rectAabb(rect) { if (!rect) return null; if (!rect.oriented) return rect; const aabb = orientedRectAabb(rect.cx, rect.cy, rect.halfW, rect.halfH, rect.angle); return { left: num(rect.left, aabb.left), right: num(rect.right, aabb.right), top: num(rect.top, aabb.top), bottom: num(rect.bottom, aabb.bottom), cx: num(rect.cx), cy: num(rect.cy), halfW: num(rect.halfW), halfH: num(rect.halfH), angle: num(rect.angle), cos: Number.isFinite(rect.cos) ? rect.cos : aabb.cos, sin: Number.isFinite(rect.sin) ? rect.sin : aabb.sin, oriented: true, type: rect.type, item: rect.item, }; } function rectLocalPoint(rect, x, y) { const dx = num(x) - num(rect?.cx); const dy = num(y) - num(rect?.cy); const c = Number.isFinite(rect?.cos) ? rect.cos : Math.cos(num(rect?.angle)); const s = Number.isFinite(rect?.sin) ? rect.sin : Math.sin(num(rect?.angle)); return { x: dx * c + dy * s, y: -dx * s + dy * c }; } function rectWorldVector(rect, x, y) { const c = Number.isFinite(rect?.cos) ? rect.cos : Math.cos(num(rect?.angle)); const s = Number.isFinite(rect?.sin) ? rect.sin : Math.sin(num(rect?.angle)); return { x: num(x) * c - num(y) * s, y: num(x) * s + num(y) * c }; } function rectWorldNormal(rect, nx, ny) { return rectWorldVector(rect, nx, ny); } function rectCorners(rect, padding = 0) { const r = rectAabb(rect); const pad = Math.max(0, num(padding)); if (!r) return []; if (!r.oriented) { return [ { x: num(r.left) - pad, y: num(r.top) - pad }, { x: num(r.right) + pad, y: num(r.top) - pad }, { x: num(r.right) + pad, y: num(r.bottom) + pad }, { x: num(r.left) - pad, y: num(r.bottom) + pad }, ]; } const c = Number.isFinite(r.cos) ? r.cos : Math.cos(num(r.angle)); const s = Number.isFinite(r.sin) ? r.sin : Math.sin(num(r.angle)); const hw = num(r.halfW) + pad; const hh = num(r.halfH) + pad; const out = []; for (const [lx, ly] of [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]]) { out.push({ x: num(r.cx) + lx * c - ly * s, y: num(r.cy) + lx * s + ly * c }); } return out; } function rectAxes(rect) { if (!rect?.oriented) return [{ x: 1, y: 0 }, { x: 0, y: 1 }]; const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(num(rect.angle)); const s = Number.isFinite(rect.sin) ? rect.sin : Math.sin(num(rect.angle)); return [{ x: c, y: s }, { x: -s, y: c }]; } function projectPoints(points, axis) { let min = Infinity; let max = -Infinity; for (const p of points || []) { const v = num(p.x) * axis.x + num(p.y) * axis.y; if (v < min) min = v; if (v > max) max = v; } return { min, max }; } function aabbOverlap(a, b, padding = 0) { const pad = Math.max(0, num(padding)); return Boolean(a && b && num(a.left) <= num(b.right) + pad && num(a.right) >= num(b.left) - pad && num(a.top) <= num(b.bottom) + pad && num(a.bottom) >= num(b.top) - pad); } function rectOverlapInfo(a, b, padding = 0) { if (!aabbOverlap(a, b, padding)) return null; const ptsA = rectCorners(a, padding); const ptsB = rectCorners(b, padding); if (!ptsA.length || !ptsB.length) return null; let bestOverlap = Infinity; let bestAxis = null; for (const axis of [...rectAxes(a), ...rectAxes(b)]) { const len = Math.hypot(axis.x, axis.y) || 1; const n = { x: axis.x / len, y: axis.y / len }; const pa = projectPoints(ptsA, n); const pb = projectPoints(ptsB, n); const overlap = Math.min(pa.max, pb.max) - Math.max(pa.min, pb.min); if (overlap <= 0) return null; if (overlap < bestOverlap) { bestOverlap = overlap; bestAxis = n; } } if (!bestAxis) return null; const acx = num(a.cx, (num(a.left) + num(a.right)) * 0.5); const acy = num(a.cy, (num(a.top) + num(a.bottom)) * 0.5); const bcx = num(b.cx, (num(b.left) + num(b.right)) * 0.5); const bcy = num(b.cy, (num(b.top) + num(b.bottom)) * 0.5); if ((bcx - acx) * bestAxis.x + (bcy - acy) * bestAxis.y < 0) bestAxis = { x: -bestAxis.x, y: -bestAxis.y }; return { overlap: bestOverlap, nx: bestAxis.x, ny: bestAxis.y, x: (Math.max(num(a.left), num(b.left)) + Math.min(num(a.right), num(b.right))) * 0.5, y: (Math.max(num(a.top), num(b.top)) + Math.min(num(a.bottom), num(b.bottom))) * 0.5, }; } function rectsOverlap(a, b, margin = 0) { return Boolean(rectOverlapInfo(a, b, margin)); } function pointInRect(x, y, rect, padding = 0) { if (!rect) return false; const pad = Math.max(0, num(padding)); if (rect.oriented) { const p = rectLocalPoint(rect, x, y); return Math.abs(p.x) <= num(rect.halfW) + pad && Math.abs(p.y) <= num(rect.halfH) + pad; } return num(x) >= num(rect.left) - pad && num(x) <= num(rect.right) + pad && num(y) >= num(rect.top) - pad && num(y) <= num(rect.bottom) + pad; } function circleOverlapsRect(x, y, radius, rect, padding = 0) { if (!rect) return false; const rr = Math.max(0, num(radius)) + Math.max(0, num(padding)); if (rect.oriented) { const local = rectLocalPoint(rect, x, y); const clx = clampValue(local.x, -num(rect.halfW), num(rect.halfW)); const cly = clampValue(local.y, -num(rect.halfH), num(rect.halfH)); return Math.hypot(local.x - clx, local.y - cly) < rr; } const cx = clampValue(num(x), num(rect.left), num(rect.right)); const cy = clampValue(num(y), num(rect.top), num(rect.bottom)); return Math.hypot(num(x) - cx, num(y) - cy) < rr; } function pointSegmentDistance(px, py, ax, ay, bx, by) { const dx = num(bx) - num(ax); const dy = num(by) - num(ay); const lenSq = dx * dx + dy * dy; if (lenSq <= 0.0001) return Math.hypot(num(px) - num(ax), num(py) - num(ay)); const u = clampValue(((num(px) - num(ax)) * dx + (num(py) - num(ay)) * dy) / lenSq, 0, 1); return Math.hypot(num(px) - (num(ax) + dx * u), num(py) - (num(ay) + dy * u)); } function segmentAabbHit(x1, y1, x2, y2, left, top, right, bottom) { if ((x1 >= left && x1 <= right && y1 >= top && y1 <= bottom) || (x2 >= left && x2 <= right && y2 >= top && y2 <= bottom)) return true; const minX = Math.min(left, right), maxX = Math.max(left, right); const minY = Math.min(top, bottom), maxY = Math.max(top, bottom); let t0 = 0, t1 = 1; const clip = (p, q) => { if (Math.abs(p) < 1e-9) return q >= 0; const t = q / p; if (p < 0) { if (t > t1) return false; if (t > t0) t0 = t; } else { if (t < t0) return false; if (t < t1) t1 = t; } return true; }; const dx = num(x2) - num(x1); const dy = num(y2) - num(y1); return clip(-dx, num(x1) - minX) && clip(dx, maxX - num(x1)) && clip(-dy, num(y1) - minY) && clip(dy, maxY - num(y1)); } function segmentIntersectsOrientedRect(x1, y1, x2, y2, r, padding = 0) { if (!r) return false; if (!r.oriented) return segmentAabbHit(x1, y1, x2, y2, r.left - padding, r.top - padding, r.right + padding, r.bottom + padding); const a = rectLocalPoint(r, x1, y1); const b = rectLocalPoint(r, x2, y2); return segmentAabbHit(a.x, a.y, b.x, b.y, -(r.halfW || 0) - padding, -(r.halfH || 0) - padding, (r.halfW || 0) + padding, (r.halfH || 0) + padding); } function oneWayFenceAllowsMotion(rect, prevX, prevY, motionX, motionY) { if (!rect?.oneWay) return false; const nx = num(rect.oneWayNx); const ny = num(rect.oneWayNy, -1); const prevSide = (num(prevX) - num(rect.cx)) * nx + (num(prevY) - num(rect.cy)) * ny; const motion = num(motionX) * nx + num(motionY) * ny; const sourceMargin = Math.max(1, num(rect.halfH) * 0.2); return motion > 0.01 || (prevSide < -sourceMargin && motion >= -0.01); } function oneWayFenceAllows(obj, rect, motionX = null, motionY = null) { if (!rect?.oneWay || !obj) return false; const hinted = Number.isFinite(Number(motionX)) || Number.isFinite(Number(motionY)); const vx = hinted ? num(motionX) : num(obj.vx) + num(obj.impulseVx); const vy = hinted ? num(motionY) : num(obj.vy) + num(obj.impulseVy); const prevX = Number.isFinite(Number(obj.prevX)) ? Number(obj.prevX) : num(obj.x) - vx * 0.016; const prevY = Number.isFinite(Number(obj.prevY)) ? Number(obj.prevY) : num(obj.y) - vy * 0.016; return oneWayFenceAllowsMotion(rect, prevX, prevY, vx, vy); } global.TarinaiGeometry = Object.freeze({ num, clampValue, orientedRectAabb, rectAabb, rectLocalPoint, rectWorldVector, rectWorldNormal, rectCorners, rectAxes, projectPoints, aabbOverlap, rectOverlapInfo, rectsOverlap, pointInRect, circleOverlapsRect, pointSegmentDistance, segmentAabbHit, segmentIntersectsOrientedRect, oneWayFenceAllowsMotion, oneWayFenceAllows, }); })(typeof window !== "undefined" ? window : globalThis);