1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! The hover-text/label formatting and the critical-angle/pending/selected overlay
//! flags computed from a built [`FacetMap`].
use super::{FacetMap, OverlayFlags};
use indicatrix::geometry::meet_solver::Block;
use indicatrix_cut_core::{
Design,
optics_hints::{self, Risk},
};
use std::collections::BTreeSet;
impl FacetMap {
/// The facet's own short on-diagram label: `"<tier name> <index>"` (the one
/// number a cutter reads off a `GemCad` diagram), or just the tier name
/// for a tier with no listed indices (a table/culet, where the index is always
/// `0` and carries no information). `""` for a preform plane, an unnamed tier,
/// or an out-of-range id -- unlike the fuller [`Self::hover_text`] tooltip.
/// Returns an owned `String` since the index has to be formatted in.
#[must_use]
pub fn facet_label(&self, facet_id: usize) -> String {
let Some(info) = self.facets.get(facet_id) else {
return String::new();
};
let Some(tier_index) = info.tier_index else {
return String::new();
};
if info.name.is_empty() {
return String::new();
}
let has_orbit = self
.orbits
.get(tier_index)
.is_some_and(|orbit| orbit.len() > 1);
if has_orbit {
format!("{} {}", info.name, info.index_on_gear)
} else {
info.name.clone()
}
}
/// A short hover tooltip for `facet_id`: `"<tier name> · <angle>° · index
/// <i> · <block> · margin <x>° over critical"` for a pavilion facet, or the
/// same without the margin clause for a crown/girdle facet, or `"Preform"`.
/// `n_d` is the design's effective refractive index the margin is computed
/// against.
///
/// The margin clause is only printed for a pavilion facet:
/// `optics_hints::tier_margin_and_risk` (the table's own path, `state/mod.rs`)
/// only has a real windowing answer for `Block::Pavilion` -- a crown or girdle
/// facet cannot window, so a margin figure there would mean nothing and would
/// undermine the reading of the pavilion facets' real margins.
#[must_use]
pub fn hover_text(&self, facet_id: usize, n_d: f64) -> String {
let Some(info) = self.facets.get(facet_id) else {
return String::new();
};
let Some(_tier_index) = info.tier_index else {
return "Preform".to_string();
};
let block_text = match info.block {
Some(Block::Crown) => "Crown",
Some(Block::Pavilion) => "Pavilion",
Some(Block::Girdle) => "Girdle",
None => "?",
};
let name = if info.name.is_empty() {
"(unnamed)"
} else {
info.name.as_str()
};
let angle = info.angle_deg;
let index = info.index_on_gear;
if info.block == Some(Block::Pavilion) {
let margin = optics_hints::tier_margin_deg(info.angle_deg, n_d);
format!(
"{name} · {angle:.1}° · index {index} · {block_text} · margin {margin:.1}° over critical"
)
} else {
format!("{name} · {angle:.1}° · index {index} · {block_text}")
}
}
/// The critical-angle-risk (`flagged`), pending-resolve (`pending`) and
/// list-selection (`selected`) overlay flags for every facet id -- see
/// [`OverlayFlags`]'s doc comment.
///
/// `design` is read fresh here (rather than this map's own cached
/// [`super::FacetInfo::angle_deg`]) so a caller mid-edit still sees the CURRENT authored
/// angle for the windowing check, even before the mesh itself catches up.
/// Windowing risk applies only to pavilion tiers (`angle_deg < 0.0`), per
/// `optics_hints`'s own module doc comment.
///
/// `pending_tiers` marks EVERY tier index it contains, not just one: a batch
/// nudge/offset dirties several tiers at once, and a cutter watching a
/// multi-tier edit needs every one of the changed facets outlined as pending,
/// not just the first.
#[must_use]
pub fn overlay_flags(
&self,
design: &Design,
n_d: f64,
selected_tier: Option<usize>,
pending_tiers: &BTreeSet<usize>,
) -> OverlayFlags {
let plane_count = self.facets.len();
let mut flagged = vec![false; plane_count];
let mut pending = vec![false; plane_count];
let mut selected = vec![false; plane_count];
for (facet_id, info) in self.facets.iter().enumerate() {
let Some(tier_index) = info.tier_index else {
continue;
};
if pending_tiers.contains(&tier_index) {
pending[facet_id] = true;
}
if selected_tier == Some(tier_index) {
selected[facet_id] = true;
}
let Some(tier) = design.tiers.get(tier_index) else {
continue;
};
if tier.angle_deg < 0.0
&& optics_hints::windowing_risk(tier.angle_deg, n_d) == Risk::Windows
{
flagged[facet_id] = true;
}
}
OverlayFlags {
flagged,
pending,
selected,
}
}
/// Every facet-id pair that should carry a meet-point marker: for every tier
/// pair `Design::facet_meets` names as meeting each other, the full cross
/// product of that pair's surviving facet ids.
///
/// `Design::facet_meets` is resolved by the same [`indicatrix::geometry::
/// meet_solver::MeetNameResolver`] `Design::solve` itself uses (girdle/culet/
/// table fallbacks, side-prefix and plural stripping, compound vertex specs),
/// not a naive name match -- see that method's own doc comment. It only names
/// TIERS, not facets or geometry, so the actual marker POINT is left for
/// `diagram2d::render_diagram` to find: the world-space vertex the two
/// facets' mesh rings genuinely share (see that module's `meet_marker_points`).
/// Cross-producting a tier pair's orbits (rather than trying to line up
/// indices here) is deliberately generous -- a facet with no real shared
/// vertex with its candidate partner simply contributes no marker once
/// `diagram2d` fails to find one, so over-listing costs a few wasted
/// ring-vs-ring comparisons, never a wrong marker.
#[must_use]
pub fn meeting_facet_pairs(&self, design: &Design) -> Vec<(u32, u32)> {
// A `BTreeSet` rather than a linear-scan `Vec` dedup: both deterministic
// (a sorted key, not iteration order, decides output order) and avoids an
// O(pairs^2) scan on a design with many meet-named tiers.
let mut pairs: std::collections::BTreeSet<(u32, u32)> = std::collections::BTreeSet::new();
for tier_index in 0..design.tiers.len() {
let Ok(partners) = design.facet_meets(tier_index) else {
continue;
};
for partner_tier in partners {
if partner_tier == tier_index {
continue;
}
for &facet_a in self.facets_of_tier(tier_index) {
for &facet_b in self.facets_of_tier(partner_tier) {
pairs.insert((facet_a.min(facet_b), facet_a.max(facet_b)));
}
}
}
}
pairs.into_iter().collect()
}
}