2026-07-01 14:41:05 +09:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
// Layer: math/geometry
|
|
|
|
|
// Shared primitive geometry helpers used by placement, collision, mechanical bodies,
|
|
|
|
|
// editor hit-testing, and world obstacle queries.
|
|
|
|
|
(function (global) {
|
2026-07-16 22:12:03 +09:00
|
|
|
const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => {
|
2026-07-01 14:41:05 +09:00
|
|
|
const n = Number(v);
|
|
|
|
|
return Number.isFinite(n) ? n : fallback;
|
2026-07-16 22:12:03 +09:00
|
|
|
});
|
|
|
|
|
const clampValue = global.TarinaiCoreHelpers?.clampNumber || ((v, min, max) => Math.max(min, Math.min(max, v)));
|
2026-07-01 14:41:05 +09:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 18:01:36 +09:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 14:41:05 +09:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 16:40:06 +09:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 14:41:05 +09:00
|
|
|
global.TarinaiGeometry = Object.freeze({
|
|
|
|
|
num,
|
|
|
|
|
clampValue,
|
|
|
|
|
orientedRectAabb,
|
|
|
|
|
rectAabb,
|
|
|
|
|
rectLocalPoint,
|
|
|
|
|
rectWorldVector,
|
|
|
|
|
rectWorldNormal,
|
|
|
|
|
rectCorners,
|
|
|
|
|
rectAxes,
|
|
|
|
|
projectPoints,
|
|
|
|
|
aabbOverlap,
|
|
|
|
|
rectOverlapInfo,
|
|
|
|
|
rectsOverlap,
|
|
|
|
|
pointInRect,
|
2026-07-05 18:01:36 +09:00
|
|
|
circleOverlapsRect,
|
2026-07-01 14:41:05 +09:00
|
|
|
pointSegmentDistance,
|
|
|
|
|
segmentAabbHit,
|
|
|
|
|
segmentIntersectsOrientedRect,
|
2026-07-10 16:40:06 +09:00
|
|
|
oneWayFenceAllowsMotion,
|
|
|
|
|
oneWayFenceAllows,
|
2026-07-01 14:41:05 +09:00
|
|
|
});
|
|
|
|
|
})(typeof window !== "undefined" ? window : globalThis);
|