北印星盘(星盘页 / 校正右栏 / 我的报告共用 VedicChartSvg)宫内不再画 「水 19°」「罗逆 29°」这类显示文本——一宫三颗星就叠三行、字号压到 8。 改成九个彩色符号横排,每行最多 3 个;逆行是符号下方一道同色横线, 第 1 宫加 As 小标,盘下方两行图例。度数、星宿、顺逆下沉到星盘页的 行星表,列从四列扩到八列,首行是上升,度数精确到分。 - 符号字体:vendored 的 9 字形 Noto Sans Symbols 子集(OFL,1.5 KB), 经 next/font/local 挂成 --font-planet-glyphs;♀ ♂ 带 U+FE0E 防 iOS 画成表情。构建镜像不能联网,所以必须自带,与 Inter 同一条理由。 - 颜色:--color-planet-* 九个 token,:root 与两个深色块各一份。只用于 认星不表吉凶,罗计共用中性灰。挂 is-* 修饰类而非行内 style——报告 文档的标记合同禁止渲染结果出现 style=,星盘不做例外。 - 数据来源:toNorthIndianChart() 加结构化 occupantGlyphs(星盘页与 markdown fence 走这条);校正与报告两条路线的模型只有引擎 / golden 显示文本,按首字解析,解析由测试锁住。 - 星宿沿用引擎返回名:仓库无中文宿名表,印度 27 宿与中国二十八宿不是 同一套,不自造对照。 tsc 0 错,lint 0 error,npm test 3471→3484 条、36 红与基线逐条相同, / 仍 Static,首屏 JS gzip +0.29%。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
133 lines
5.0 KiB
TypeScript
133 lines
5.0 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));
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|