/** 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> = { 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)); } /** * Where a house's planet symbols sit. The raw centroid crowds the sign number * in the triangular houses, so the group is nudged away from the vertex the * number hangs on — the centre for the four diamonds, the right angle for the * eight triangles. */ export function occupantAnchor(houseNumber: number): Point { const polygon = HOUSE_POLYGONS[houseNumber]; const centroid = polygonCentroid(polygon); const isDiamond = polygon.length === 4; const anchorVertex = isDiamond ? innerVertex(polygon) : rightAngleVertex(polygon); const push = isDiamond ? 0.05 : 0.18; return { x: centroid.x + (centroid.x - anchorVertex.x) * push, y: centroid.y + (centroid.y - anchorVertex.y) * push, }; } 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); }