9670b661c4
BUG-330: the compact board sat in the same layer as the composer, so peek copy overlapped the sheet title. Overlay only below 768px; desktop stays a side column. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
6.0 KiB
TypeScript
178 lines
6.0 KiB
TypeScript
import {
|
|
isWindowScanDisplayLayer,
|
|
WINDOW_SCAN_DISPLAY_LAYER_ORDER,
|
|
WINDOW_SCAN_SCORING_LAYER_ORDER,
|
|
type WindowScanLayer,
|
|
type WindowScanTransition,
|
|
} from "./rectification-agentic/v9/refinement-packet";
|
|
import {
|
|
workingRectificationHouseTable,
|
|
type RectificationCandidateResult,
|
|
type RectificationHouseTable,
|
|
} from "./rectification-candidate-result";
|
|
|
|
export const RECTIFICATION_BOARD_SPLIT_MIN_PX = 768;
|
|
|
|
export type GroupedWindowMinute = Readonly<{
|
|
at: string;
|
|
scoringLayers: readonly WindowScanLayer[];
|
|
displayLayers: readonly WindowScanLayer[];
|
|
}>;
|
|
|
|
export type RectificationBoardDiff = Readonly<{
|
|
houseNumbers: readonly number[];
|
|
lagnaChanged: boolean;
|
|
timeChanged: boolean;
|
|
transitionMinutes: readonly string[];
|
|
liveMessage: string;
|
|
}>;
|
|
|
|
const SCORING_RANK = new Map<WindowScanLayer, number>(
|
|
WINDOW_SCAN_SCORING_LAYER_ORDER.map((layer, index) => [layer, index]),
|
|
);
|
|
const DISPLAY_RANK = new Map<WindowScanLayer, number>(
|
|
WINDOW_SCAN_DISPLAY_LAYER_ORDER.map((layer, index) => [layer, index]),
|
|
);
|
|
|
|
const EMPTY_DIFF: RectificationBoardDiff = {
|
|
houseNumbers: [],
|
|
lagnaChanged: false,
|
|
timeChanged: false,
|
|
transitionMinutes: [],
|
|
liveMessage: "",
|
|
};
|
|
|
|
function sortLayers(
|
|
layers: readonly WindowScanLayer[],
|
|
rank: ReadonlyMap<WindowScanLayer, number>,
|
|
): readonly WindowScanLayer[] {
|
|
return [...layers].sort((left, right) => (rank.get(left) ?? 99) - (rank.get(right) ?? 99));
|
|
}
|
|
|
|
export function groupWindowTransitions(
|
|
transitions: readonly WindowScanTransition[],
|
|
): readonly GroupedWindowMinute[] {
|
|
const byMinute = new Map<string, { scoring: Set<WindowScanLayer>; display: Set<WindowScanLayer> }>();
|
|
for (const item of transitions) {
|
|
const bucket = byMinute.get(item.at) ?? { scoring: new Set<WindowScanLayer>(), display: new Set<WindowScanLayer>() };
|
|
if (isWindowScanDisplayLayer(item.layer)) bucket.display.add(item.layer);
|
|
else bucket.scoring.add(item.layer);
|
|
byMinute.set(item.at, bucket);
|
|
}
|
|
return [...byMinute.entries()]
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
.map(([at, bucket]) => ({
|
|
at,
|
|
scoringLayers: sortLayers([...bucket.scoring], SCORING_RANK),
|
|
displayLayers: sortLayers([...bucket.display], DISPLAY_RANK),
|
|
}));
|
|
}
|
|
|
|
export function changedHouseNumbers(
|
|
previous: RectificationHouseTable | null,
|
|
next: RectificationHouseTable | null,
|
|
): readonly number[] {
|
|
if (!previous || !next) return [];
|
|
const changed: number[] = [];
|
|
for (let index = 0; index < 12; index += 1) {
|
|
const before = previous.houses[index];
|
|
const after = next.houses[index];
|
|
if (!before || !after) continue;
|
|
if (before.sign !== after.sign || before.occupants.join("\0") !== after.occupants.join("\0")) {
|
|
changed.push(after.house);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function groupedMinuteKey(row: GroupedWindowMinute): string {
|
|
return `${row.at}|${row.scoringLayers.join(",")}|${row.displayLayers.join(",")}`;
|
|
}
|
|
|
|
export function changedTransitionMinutes(
|
|
previous: readonly WindowScanTransition[],
|
|
next: readonly WindowScanTransition[],
|
|
): readonly string[] {
|
|
const before = new Map(groupWindowTransitions(previous).map((row) => [row.at, groupedMinuteKey(row)]));
|
|
const changed: string[] = [];
|
|
for (const row of groupWindowTransitions(next)) {
|
|
if (before.get(row.at) !== groupedMinuteKey(row)) changed.push(row.at);
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function joinHouses(houses: readonly number[]): string {
|
|
return houses.map((house) => `第${house}宫`).join("、");
|
|
}
|
|
|
|
export function rectificationBoardLiveMessage(input: Readonly<{
|
|
previous: RectificationCandidateResult | null;
|
|
next: RectificationCandidateResult | null;
|
|
houseNumbers: readonly number[];
|
|
lagnaChanged: boolean;
|
|
timeChanged: boolean;
|
|
transitionMinutes: readonly string[];
|
|
}>): string {
|
|
const nextTable = input.next ? workingRectificationHouseTable(input.next) : null;
|
|
if (!nextTable) return "";
|
|
if (!input.previous) {
|
|
return `当前本命宫位已按 ${nextTable.time} 排出,上升${nextTable.lagna}。`;
|
|
}
|
|
const parts: string[] = [];
|
|
if (input.timeChanged || input.lagnaChanged) {
|
|
parts.push(`本命宫位已按 ${nextTable.time} 重算(上升 ${nextTable.lagna})。`);
|
|
}
|
|
if (input.houseNumbers.length > 0 && !(input.timeChanged && input.houseNumbers.length === 12)) {
|
|
parts.push(`${joinHouses(input.houseNumbers)}已更新。`);
|
|
}
|
|
if (input.transitionMinutes.length > 0) {
|
|
parts.push("换升时刻已更新。");
|
|
}
|
|
return parts.join("");
|
|
}
|
|
|
|
export function diffRectificationBoard(
|
|
previous: RectificationCandidateResult | null,
|
|
next: RectificationCandidateResult | null,
|
|
): RectificationBoardDiff {
|
|
if (!next) return EMPTY_DIFF;
|
|
const previousTable = previous ? workingRectificationHouseTable(previous) : null;
|
|
const nextTable = workingRectificationHouseTable(next);
|
|
const houseNumbers = changedHouseNumbers(previousTable, nextTable);
|
|
const lagnaChanged = Boolean(previousTable && nextTable && previousTable.lagna !== nextTable.lagna);
|
|
const timeChanged = Boolean(previousTable && nextTable && previousTable.time !== nextTable.time);
|
|
const transitionMinutes = previous
|
|
? changedTransitionMinutes(previous.windowTransitions, next.windowTransitions)
|
|
: [];
|
|
return {
|
|
houseNumbers,
|
|
lagnaChanged,
|
|
timeChanged,
|
|
transitionMinutes,
|
|
liveMessage: rectificationBoardLiveMessage({
|
|
previous,
|
|
next,
|
|
houseNumbers,
|
|
lagnaChanged,
|
|
timeChanged,
|
|
transitionMinutes,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function rectificationBoardPeekCopy(result: RectificationCandidateResult | null): Readonly<{
|
|
title: string;
|
|
detail: string | null;
|
|
}> {
|
|
const table = result ? workingRectificationHouseTable(result) : null;
|
|
if (!table) {
|
|
return { title: "当前盘面", detail: "补充经历后会在这里更新" };
|
|
}
|
|
return { title: "当前盘面", detail: `${table.time} · 上升${table.lagna}` };
|
|
}
|
|
|
|
export function workingRectificationTime(result: RectificationCandidateResult | null): string | null {
|
|
if (!result) return null;
|
|
return result.selectedTime ?? result.representativeTime ?? workingRectificationHouseTable(result)?.time ?? null;
|
|
}
|