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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Caller-authored guide geometry: plain lines and arrows.
//!
//! An interaction edge states a domain claim — this is a hydrogen bond,
//! that is a salt bridge — and takes its whole appearance from that claim. A
//! guide makes no claim. It is the arrow that points at a pocket in a figure,
//! the axis of a helix, the segment marking a distance a reader should notice.
//! Keeping it a separate type is what stops a decorative arrow from being read
//! later as detected chemistry.
//!
//! Guides draw through the same analytic glyph path as interactions, so they
//! inherit its pixel-stable marks, transparency, depth and picking without a
//! second pass or a second shader.
use crate::error::CoreError;
use crate::handle::StructureHandle;
use crate::representation::relation::RelationPattern;
use molgfx_math::{Rgba8, Vec3};
#[cfg(test)]
#[path = "guide_tests.rs"]
mod tests;
/// How a guide's ends are marked.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum GuideCap {
/// A plain segment.
#[default]
None,
/// An arrowhead at the end point.
Arrow,
/// Arrowheads at both ends, for a span or a measurement.
DoubleArrow,
}
/// Whether a polyline stops at its final point or closes back to its first.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum PolylineKind {
/// Consecutive points form an open path.
#[default]
Open,
/// The final point is connected back to the first.
Closed,
}
/// Presentation of one guide. Every field is caller state; nothing is derived
/// from chemistry.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct GuideStyle {
/// Line colour.
pub color: Rgba8,
/// Screen-space mark vocabulary shared with generic relation glyphs.
pub pattern: RelationPattern,
/// Pixel-stable line width.
pub width_pixels: f32,
/// Final alpha before weighted transparency.
pub opacity: f32,
/// Repetition period in pixels; ignored by solid lines.
pub period_pixels: f32,
/// Fraction of each period occupied by a mark.
pub duty_cycle: f32,
/// End decoration.
pub cap: GuideCap,
/// Arrowhead size in pixels.
pub arrow_pixels: f32,
}
impl Default for GuideStyle {
fn default() -> Self {
Self {
color: Rgba8::opaque(226, 232, 240),
pattern: RelationPattern::Solid,
width_pixels: 1.6,
opacity: 1.0,
period_pixels: 8.0,
duty_cycle: 0.5,
cap: GuideCap::None,
arrow_pixels: 9.0,
}
}
}
impl GuideStyle {
/// Finite, bounded values consumed by the glyph pass.
#[must_use]
pub fn sanitized(self) -> Self {
let default = Self::default();
Self {
color: self.color,
pattern: self.pattern,
width_pixels: bounded(self.width_pixels, 0.1, 64.0, default.width_pixels),
opacity: bounded(self.opacity, 0.0, 1.0, default.opacity),
period_pixels: bounded(self.period_pixels, 1.0, 256.0, default.period_pixels),
duty_cycle: bounded(self.duty_cycle, 0.05, 1.0, default.duty_cycle),
cap: self.cap,
arrow_pixels: bounded(self.arrow_pixels, 0.0, 64.0, default.arrow_pixels),
}
}
}
/// One caller-authored guide owned by a structure, so it follows that
/// structure's placement.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Guide {
owner: StructureHandle,
start: Vec3,
end: Vec3,
style: GuideStyle,
visible: bool,
}
impl Guide {
/// Creates a guide between two finite, distinct model-space points.
///
/// # Errors
///
/// Returns [`CoreError::InvalidAnnotation`] when a point is not finite or
/// the two coincide, which would leave the direction undefined.
pub fn new(
owner: StructureHandle,
start: Vec3,
end: Vec3,
style: GuideStyle,
) -> Result<Self, CoreError> {
if !start.is_finite() || !end.is_finite() {
return Err(CoreError::InvalidAnnotation {
reason: "guide endpoints must be finite",
});
}
if start.distance_squared(end) <= f32::EPSILON {
return Err(CoreError::InvalidAnnotation {
reason: "guide endpoints must be distinct",
});
}
Ok(Self {
owner,
start,
end,
style: style.sanitized(),
visible: true,
})
}
/// The structure whose placement carries this guide.
#[must_use]
pub const fn owner(&self) -> StructureHandle {
self.owner
}
/// Start point in the owner's model space.
#[must_use]
pub const fn start(&self) -> Vec3 {
self.start
}
/// End point in the owner's model space.
#[must_use]
pub const fn end(&self) -> Vec3 {
self.end
}
/// Resolved presentation.
#[must_use]
pub const fn style(&self) -> GuideStyle {
self.style
}
/// Whether the guide draws.
#[must_use]
pub const fn visible(&self) -> bool {
self.visible
}
/// Shows or hides the guide without discarding it.
pub const fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
/// Replaces the presentation, sanitizing it.
pub fn set_style(&mut self, style: GuideStyle) {
self.style = style.sanitized();
}
}
fn bounded(value: f32, minimum: f32, maximum: f32, fallback: f32) -> f32 {
if value.is_finite() {
value.clamp(minimum, maximum)
} else {
fallback
}
}