Files
Jyotisha/frontend/src/lib/planet-glyphs.ts
T
Jesse_ChenandClaude Fable 5.1 5a1dcbd2a1
Independent Staging Quality Gate / validate (push) Failing after 8m58s
Independent Staging Quality Gate / publish (push) Skipped
feat(chart): 星盘改用行星符号与颜色,度数进行星表
北印星盘(星盘页 / 校正右栏 / 我的报告共用 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
2026-09-18 00:39:13 +00:00

187 lines
5.5 KiB
TypeScript

/**
* Planet symbols for the North Indian chart and the chart page planet table.
*
* The chart used to print display text inside each house ("水 19°", "罗逆 29°"),
* which stacked three labels on top of each other whenever a house held three
* planets. The chart now carries symbols only; degrees, nakshatra and direction
* live in the planet table under the chart.
*
* Colour is an identification aid, not a verdict: nothing here says benefic or
* malefic. Rahu and Ketu deliberately share one neutral grey.
*
* ♀ and ♂ are followed by U+FE0E (VARIATION SELECTOR-15, text presentation).
* Without it iOS renders both as colour emoji.
*/
export const PLANET_GLYPH_KEYS = [
"sun",
"moon",
"mars",
"mercury",
"jupiter",
"venus",
"saturn",
"rahu",
"ketu",
] as const;
export type PlanetGlyphKey = (typeof PLANET_GLYPH_KEYS)[number];
const TEXT_PRESENTATION = "\uFE0E";
export const PLANET_GLYPH: Readonly<Record<PlanetGlyphKey, string>> = {
sun: "☉",
moon: "☽",
mars: `♂${TEXT_PRESENTATION}`,
mercury: "☿",
jupiter: "♃",
venus: `♀${TEXT_PRESENTATION}`,
saturn: "♄",
rahu: "☊",
ketu: "☋",
};
/** One-character Chinese name, as used in the chart legend. */
export const PLANET_GLYPH_ZH: Readonly<Record<PlanetGlyphKey, string>> = {
sun: "日",
moon: "月",
mars: "火",
mercury: "水",
jupiter: "木",
venus: "金",
saturn: "土",
rahu: "罗",
ketu: "计",
};
/** Full Chinese name, as used in the planet table. */
export const PLANET_NAME_ZH: Readonly<Record<PlanetGlyphKey, string>> = {
sun: "太阳",
moon: "月亮",
mars: "火星",
mercury: "水星",
jupiter: "木星",
venus: "金星",
saturn: "土星",
rahu: "北交点",
ketu: "南交点",
};
export const PLANET_ABBR: Readonly<Record<PlanetGlyphKey, string>> = {
sun: "Su",
moon: "Mo",
mars: "Ma",
mercury: "Me",
jupiter: "Ju",
venus: "Ve",
saturn: "Sa",
rahu: "Ra",
ketu: "Ke",
};
/**
* Colour modifier per body; both nodes share one. The colours themselves are
* `--color-planet-*` in globals.css, one value per theme.
*
* A class rather than an inline `style`: the personal report's markup contract
* forbids a `style=` attribute anywhere in the rendered document, and that rule
* is load-bearing (it is what keeps agent-authored content from carrying its
* own CSS), so the chart may not be the one exception.
*/
export const PLANET_COLOR_CLASS: Readonly<Record<PlanetGlyphKey, string>> = {
sun: "is-sun",
moon: "is-moon",
mars: "is-mars",
mercury: "is-mercury",
jupiter: "is-jupiter",
venus: "is-venus",
saturn: "is-saturn",
rahu: "is-node",
ketu: "is-node",
};
export const ASCENDANT_COLOR_CLASS = "is-ascendant";
/** The North Indian first house is always the ascendant, so the mark needs no data. */
export const ASCENDANT_MARK = "As";
export const ASCENDANT_NAME_ZH = "上升";
/** Chinese names the engines emit, keyed by their first character. */
const ZH_FIRST_CHARACTER: Readonly<Record<string, PlanetGlyphKey>> = {
日: "sun",
太: "sun", // 太阳
月: "moon",
火: "mars",
水: "mercury",
木: "jupiter",
金: "venus",
土: "saturn",
罗: "rahu", // 罗睺
计: "ketu", // 计都
};
const EN_NAMES: Readonly<Record<string, PlanetGlyphKey>> = {
sun: "sun",
moon: "moon",
mars: "mars",
mercury: "mercury",
jupiter: "jupiter",
venus: "venus",
saturn: "saturn",
rahu: "rahu",
ketu: "ketu",
};
const ASCENDANT_NAMES = new Set(["as", "asc", "ascendant", "lagna"]);
/** 上升 / 上升点 / 上升星座 — the report's own wording for the same point. */
const ASCENDANT_ZH_PREFIX = "上升";
/**
* Maps a label to a glyph key. Accepts every spelling the three call sites can
* produce: the report chart block's one-character names ("水", "罗"), the
* rectification house table's full Chinese names ("水星", "罗睺"), and the
* report evidence bundle's English celestial names ("Mercury", "Rahu").
*/
export function planetGlyphKey(name: string): PlanetGlyphKey | null {
const trimmed = name.trim();
if (!trimmed) return null;
const latin = trimmed.match(/^[A-Za-z]+/);
if (latin) return EN_NAMES[latin[0].toLowerCase()] ?? null;
return ZH_FIRST_CHARACTER[trimmed[0]!] ?? null;
}
export type ChartOccupant =
| { kind: "planet"; key: PlanetGlyphKey; retrograde: boolean; label: string }
| { kind: "ascendant"; label: string }
| { kind: "text"; label: string };
/**
* Reads one occupant string. The rectification board and the personal report
* both receive their house tables as engine/agent display text and have no
* structured planet list to join against — their contracts are golden-fixture
* shaped and may not be invented here — so those two routes are parsed.
* `toNorthIndianChart` instead attaches `occupantGlyphs` and never reaches this.
*/
export function parseChartOccupant(label: string): ChartOccupant {
const trimmed = label.trim();
if (!trimmed) return { kind: "text", label };
if (ASCENDANT_NAMES.has(trimmed.toLowerCase()) || trimmed.startsWith(ASCENDANT_ZH_PREFIX)) {
return { kind: "ascendant", label: trimmed };
}
const key = planetGlyphKey(trimmed);
if (!key) return { kind: "text", label: trimmed };
return { kind: "planet", key, retrograde: trimmed.includes("逆"), label: trimmed };
}
/** Reading order for the chart legend: 日 月 水 金 火 木 土 罗 计. */
export const PLANET_LEGEND_ORDER = [
"sun",
"moon",
"mercury",
"venus",
"mars",
"jupiter",
"saturn",
"rahu",
"ketu",
] as const satisfies readonly PlanetGlyphKey[];