Files
astro/internal/svgchart/label.go
T
b612 2bf8478639 feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
2026-09-17 12:27:40 +08:00

128 lines
4.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package svgchart
// LabelBox 是文本或图形占位的保守包围盒。/ LabelBox is a conservative bounding box for a reserved label or graphic.
type LabelBox struct {
X, Y, Width, Height float64
}
// LabelPlacement 是一个候选位次:文本锚点与 text-anchor 取值。
// LabelPlacement is one candidate slot: the text anchor point and its text-anchor value.
type LabelPlacement struct {
X, Y float64
Anchor string
}
// LabelTable 记录已占位的矩形,后来的标签按候选顺序避让。
// LabelTable records reserved rectangles; later labels avoid them in candidate order.
type LabelTable struct {
boxes []LabelBox
}
// labelClearance 是落位判定要求的间隙:包围盒各向外放 1 px,写出的坐标取整后两个盒子也不会贴成相交。
const labelClearance = 1.0
// Reserve 占位一个矩形;非正宽高直接忽略。
// Reserve claims a rectangle; non-positive extents are ignored.
func (table *LabelTable) Reserve(x, y, width, height float64) {
if width <= 0 || height <= 0 {
return
}
table.boxes = append(table.boxes, LabelBox{X: x, Y: y, Width: width, Height: height})
}
// LabelTextBox 返回文本的保守包围盒,anchor 取 start、middle 或 end。
// LabelTextBox returns a text's conservative box for a start, middle, or end anchor.
func LabelTextBox(x, baseline, fontSize float64, text, anchor string) LabelBox {
if fontSize <= 0 {
fontSize = 12
}
width := EstimatedTextWidth(text, fontSize)
left := x
switch anchor {
case "middle":
left = x - width/2
case "end":
left = x - width
}
above, below := EstimatedTextExtents(fontSize)
return LabelBox{X: left, Y: baseline - above, Width: width, Height: above + below}
}
// ReserveText 占位一段文本并返回它的包围盒。
// ReserveText claims a text box and returns it.
func (table *LabelTable) ReserveText(x, baseline, fontSize float64, text, anchor string) LabelBox {
box := LabelTextBox(x, baseline, fontSize, text, anchor)
table.Reserve(box.X, box.Y, box.Width, box.Height)
return box
}
// Overlaps 报告矩形是否压到已占位矩形;两个盒子之间至少留 labelClearance。
// Overlaps reports whether a rectangle collides with any reserved box, keeping labelClearance between them.
func (table *LabelTable) Overlaps(x, y, width, height float64) bool {
for _, box := range table.boxes {
if box.X+box.Width+labelClearance <= x || x+width+labelClearance <= box.X {
continue
}
if box.Y+box.Height+labelClearance <= y || y+height+labelClearance <= box.Y {
continue
}
return true
}
return false
}
// OverlapsText 报告文本是否压到已占位矩形。
// OverlapsText reports whether a text collides with any reserved box.
func (table *LabelTable) OverlapsText(x, baseline, fontSize float64, text, anchor string) bool {
box := LabelTextBox(x, baseline, fontSize, text, anchor)
return table.Overlaps(box.X, box.Y, box.Width, box.Height)
}
// Place 按候选顺序放一段文本:返回首个不压叠的位次并占位;全部压叠时返回 false 且不占位。
// Place takes the first candidate that keeps the text clear, claims it, and reports false when none does.
func (table *LabelTable) Place(text string, fontSize float64, candidates []LabelPlacement) (LabelPlacement, bool) {
for _, candidate := range candidates {
box := LabelTextBox(candidate.X, candidate.Y, fontSize, text, candidate.Anchor)
if table.Overlaps(box.X, box.Y, box.Width, box.Height) {
continue
}
table.Reserve(box.X, box.Y, box.Width, box.Height)
return candidate, true
}
return LabelPlacement{}, false
}
// MirrorLabelAnchor 返回镜像后的 text-anchor:start 与 end 互换,middle 不变。
// MirrorLabelAnchor flips start and end, and keeps middle.
func MirrorLabelAnchor(anchor string) string {
switch anchor {
case "start":
return "end"
case "end":
return "start"
default:
return "middle"
}
}
// LabelCandidates 生成绕锚点的候选位次:原位、上下左右、四角,再按镜像 anchor 重复一遍。
// LabelCandidates lists candidate slots around an anchor: in place, then the four sides, then the
// four corners, repeated for the mirrored anchor.
func LabelCandidates(x, y float64, anchor string, radius float64) []LabelPlacement {
candidates := make([]LabelPlacement, 0, 18)
for _, current := range []string{anchor, MirrorLabelAnchor(anchor)} {
candidates = append(candidates,
LabelPlacement{X: x, Y: y, Anchor: current},
LabelPlacement{X: x, Y: y - radius, Anchor: current},
LabelPlacement{X: x, Y: y + radius, Anchor: current},
LabelPlacement{X: x + radius, Y: y, Anchor: current},
LabelPlacement{X: x - radius, Y: y, Anchor: current},
LabelPlacement{X: x + radius, Y: y - radius, Anchor: current},
LabelPlacement{X: x - radius, Y: y - radius, Anchor: current},
LabelPlacement{X: x + radius, Y: y + radius, Anchor: current},
LabelPlacement{X: x - radius, Y: y + radius, Anchor: current},
)
}
return candidates
}