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>
115 lines
4.3 KiB
TypeScript
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);
|
|
}
|