Files
Jyotisha/frontend/src/lib/north-indian-chart-geometry.ts
T
Jesse_ChenandCursor 2fdcb14fd6 fix(report): draw North Indian charts from fences after skipHtml dropped SVG
Longform report pages kept the D1/D9/Moon and varga headings but skipHtml
stripped the engine's inline SVG. Emit a jyotish-chart JSON fence beside
each SVG and render a diamond chart on the reader without relaxing HTML
sanitization. BUG-607.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 15:11:00 +08:00

115 lines
4.3 KiB
TypeScript

/** North Indian (diamond) house polygons for a square of side S. */
export const CHART_SIZE = 400;
export type Point = Readonly<{ x: number; y: number }>;
const S = CHART_SIZE;
const H = S / 2;
const Q = S / 4;
/** House 1 is the top diamond; numbers run clockwise from there. */
export const HOUSE_POLYGONS: Readonly<Record<number, readonly Point[]>> = {
1: [{ x: H, y: 0 }, { x: 3 * Q, y: Q }, { x: H, y: H }, { x: Q, y: Q }],
2: [{ x: 0, y: 0 }, { x: H, y: 0 }, { x: Q, y: Q }],
3: [{ x: 0, y: 0 }, { x: Q, y: Q }, { x: 0, y: H }],
4: [{ x: 0, y: H }, { x: Q, y: Q }, { x: H, y: H }, { x: Q, y: 3 * Q }],
5: [{ x: 0, y: H }, { x: Q, y: 3 * Q }, { x: 0, y: S }],
6: [{ x: 0, y: S }, { x: Q, y: 3 * Q }, { x: H, y: S }],
7: [{ x: H, y: S }, { x: Q, y: 3 * Q }, { x: H, y: H }, { x: 3 * Q, y: 3 * Q }],
8: [{ x: H, y: S }, { x: 3 * Q, y: 3 * Q }, { x: S, y: S }],
9: [{ x: S, y: S }, { x: 3 * Q, y: 3 * Q }, { x: S, y: H }],
10: [{ x: S, y: H }, { x: 3 * Q, y: 3 * Q }, { x: H, y: H }, { x: 3 * Q, y: Q }],
11: [{ x: S, y: H }, { x: 3 * Q, y: Q }, { x: S, y: 0 }],
12: [{ x: S, y: 0 }, { x: 3 * Q, y: Q }, { x: H, y: 0 }],
};
export const HOUSE_NUMBERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const;
const CENTER: Point = { x: H, y: H };
export function polygonPoints(points: readonly Point[]): string {
return points.map((point) => `${point.x},${point.y}`).join(" ");
}
export function polygonCentroid(points: readonly Point[]): Point {
const areaTimes2 = points.reduce((sum, point, index) => {
const next = points[(index + 1) % points.length];
return sum + (point.x * next.y - next.x * point.y);
}, 0);
if (Math.abs(areaTimes2) < 1e-9) {
const n = points.length || 1;
return {
x: points.reduce((sum, point) => sum + point.x, 0) / n,
y: points.reduce((sum, point) => sum + point.y, 0) / n,
};
}
const cx = points.reduce((sum, point, index) => {
const next = points[(index + 1) % points.length];
return sum + (point.x + next.x) * (point.x * next.y - next.x * point.y);
}, 0) / (3 * areaTimes2);
const cy = points.reduce((sum, point, index) => {
const next = points[(index + 1) % points.length];
return sum + (point.y + next.y) * (point.x * next.y - next.x * point.y);
}, 0) / (3 * areaTimes2);
return { x: cx, y: cy };
}
export function polygonArea(points: readonly Point[]): number {
const sum = points.reduce((total, point, index) => {
const next = points[(index + 1) % points.length];
return total + (point.x * next.y - next.x * point.y);
}, 0);
return Math.abs(sum) / 2;
}
export function pointInPolygon(point: Point, polygon: readonly Point[]): boolean {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
const a = polygon[i];
const b = polygon[j];
const intersect = (a.y > point.y) !== (b.y > point.y)
&& point.x < ((b.x - a.x) * (point.y - a.y)) / ((b.y - a.y) || Number.EPSILON) + a.x;
if (intersect) inside = !inside;
}
return inside;
}
function distance(a: Point, b: Point): number {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function moveToward(from: Point, to: Point, distancePx: number): Point {
const dx = to.x - from.x;
const dy = to.y - from.y;
const length = Math.hypot(dx, dy) || 1;
return { x: from.x + (dx / length) * distancePx, y: from.y + (dy / length) * distancePx };
}
function rightAngleVertex(points: readonly Point[]): Point {
for (let index = 0; index < points.length; index += 1) {
const prev = points[(index + points.length - 1) % points.length];
const vertex = points[index];
const next = points[(index + 1) % points.length];
const d1x = prev.x - vertex.x;
const d1y = prev.y - vertex.y;
const d2x = next.x - vertex.x;
const d2y = next.y - vertex.y;
if (Math.abs(d1x * d2x + d1y * d2y) < 1e-6) return vertex;
}
return points[0];
}
function innerVertex(points: readonly Point[]): Point {
return points.reduce((best, point) => (distance(point, CENTER) < distance(best, CENTER) ? point : best));
}
export function signNumberAnchor(houseNumber: number): Point {
const polygon = HOUSE_POLYGONS[houseNumber];
const centroid = polygonCentroid(polygon);
if (polygon.length === 4) {
return moveToward(innerVertex(polygon), centroid, 12);
}
return moveToward(rightAngleVertex(polygon), centroid, 14);
}