Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.0528.5 KB2026-10-160.1.1542.8 KB2026-10-150.1.0541.1 KB2026-10-15
Version
0.1.1
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-10-15
Size
542.8 KB
Downloads
9
Checksum
2e36f65a5798a2a0281457d1ed7f5a4e1a74967fea2880b4961ede38808a3682
Dependencies
None

bond.rs

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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
//! How bonds are drawn
//!
//! Single, double, and triple bonds are drawn as lines. In a chain, a double bond has a line on
//! each side of the middle. In a ring, it is shifted toward the inside only. The inner line is
//! trimmed a little at both ends so that its ends meet the lines of the neighboring sides.
//!
//! Wedges are drawn as triangles. There are filled, unfilled, and hashed wedges, and in each
//! the wide side is the end nearer the viewer. Only the hashed wedge is made of short lines
//! placed in a row.

use crate::node::Kind;
use crate::style::Style;


/// One drawn line
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Line {
    pub x1: f64,
    pub y1: f64,
    pub x2: f64,
    pub y2: f64,
}


/// The triangle of a wedge
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Wedge {
    /// The point at the narrow side
    pub tip: (f64, f64),
    /// The two points at the wide side
    pub base: [(f64, f64); 2],
    /// Whether the inside is filled
    pub filled: bool,
}


/// Which side a double bond is drawn on
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Placement {
    /// Draw one line on each side of the middle
    Middle,
    /// Shift toward one side. The point to shift toward, and how much to trim the inner line
    /// at both ends
    Toward {x: f64, y: f64, inset: f64},
}


/// How one bond is drawn
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Drawn {
    pub line: Vec<Line>,
    pub wedge: Vec<Wedge>,
}


/// Turns one bond into lines and triangles
pub fn draw(
    kind: Kind,
    start: (f64, f64),
    end: (f64, f64),
    placement: Placement,
    style: &Style,
) -> Drawn {
    let span = length(start, end);

    if span <= 0.0 {
        return Drawn::default();
    }

    // Unit vector perpendicular to the direction of the bond
    let across = (-(end.1 - start.1) / span, (end.0 - start.0) / span);
    let gap = style.double_gap;

    match kind {
        Kind::Single => line_only(vec![offset(start, end, across, 0.0)]),
        Kind::Double => line_only(double(start, end, across, placement, gap)),
        Kind::Triple => line_only(vec![
            offset(start, end, across, gap),
            offset(start, end, across, 0.0),
            offset(start, end, across, -gap),
        ]),
        _ => cram(kind, start, end, across, span, style),
    }
}


/// Distance between two points
pub fn length(start: (f64, f64), end: (f64, f64)) -> f64 {
    ((end.0 - start.0).powi(2) + (end.1 - start.1).powi(2)).sqrt()
}


fn line_only(line: Vec<Line>) -> Drawn {
    Drawn {line, wedge: Vec::new()}
}


/// The two lines of a double bond
fn double(
    start: (f64, f64),
    end: (f64, f64),
    across: (f64, f64),
    placement: Placement,
    gap: f64,
) -> Vec<Line> {
    let Placement::Toward {x, y, inset} = placement else {
        return vec![
            offset(start, end, across, gap / 2.0),
            offset(start, end, across, -gap / 2.0),
        ];
    };

    // Check which side of across the point to shift toward is on
    let middle = ((start.0 + end.0) / 2.0, (start.1 + end.1) / 2.0);
    let toward = (x - middle.0) * across.0 + (y - middle.1) * across.1;
    let side = if toward >= 0.0 {gap} else {-gap};
    let inner = offset(start, end, across, side);

    vec![offset(start, end, across, 0.0), trim(inner, inset)]
}


/// The wedge kinds
fn cram(
    kind: Kind,
    start: (f64, f64),
    end: (f64, f64),
    across: (f64, f64),
    span: f64,
    style: &Style,
) -> Drawn {
    let (tip, wide) = if kind.is_reversed() {(end, start)} else {(start, end)};
    let half = style.wedge_width / 2.0;

    if matches!(kind, Kind::Hash | Kind::HashBack) {
        return line_only(hash(tip, wide, across, span, style));
    }

    Drawn {
        line: Vec::new(),
        wedge: vec![Wedge {
            tip,
            base: [
                (wide.0 + across.0 * half, wide.1 + across.1 * half),
                (wide.0 - across.0 * half, wide.1 - across.1 * half),
            ],
            filled: matches!(kind, Kind::Wedge | Kind::WedgeBack),
        }],
    }
}


/// A hashed wedge. Lines are placed from the narrow side to the wide side,
/// each a little longer than the one before
fn hash(
    tip: (f64, f64),
    wide: (f64, f64),
    across: (f64, f64),
    span: f64,
    style: &Style,
) -> Vec<Line> {
    let count = (span / style.hash_gap).round().max(2.0) as usize;
    let half = style.wedge_width / 2.0;
    let mut found = Vec::with_capacity(count);

    for step in 1..=count {
        let ratio = step as f64 / count as f64;
        let middle = (
            tip.0 + (wide.0 - tip.0) * ratio,
            tip.1 + (wide.1 - tip.1) * ratio,
        );
        let reach = half * ratio;
        found.push(Line {
            x1: middle.0 + across.0 * reach,
            y1: middle.1 + across.1 * reach,
            x2: middle.0 - across.0 * reach,
            y2: middle.1 - across.1 * reach,
        });
    }

    found
}


/// The line with both ends moved by amount in the direction of across
fn offset(start: (f64, f64), end: (f64, f64), across: (f64, f64), amount: f64) -> Line {
    Line {
        x1: start.0 + across.0 * amount,
        y1: start.1 + across.1 * amount,
        x2: end.0 + across.0 * amount,
        y2: end.1 + across.1 * amount,
    }
}


/// Trims both ends of a line by amount
fn trim(line: Line, amount: f64) -> Line {
    let span = length((line.x1, line.y1), (line.x2, line.y2));

    if amount <= 0.0 || span <= amount * 2.0 {
        return line;
    }

    let step = ((line.x2 - line.x1) / span * amount, (line.y2 - line.y1) / span * amount);

    Line {
        x1: line.x1 + step.0,
        y1: line.y1 + step.1,
        x2: line.x2 - step.0,
        y2: line.y2 - step.1,
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    fn plain(kind: Kind, start: (f64, f64), end: (f64, f64)) -> Drawn {
        draw(kind, start, end, Placement::Middle, &Style::default())
    }

    #[test]
    fn a_single_bond_is_one_line() {
        let found = plain(Kind::Single, (0.0, 0.0), (1.0, 0.0));
        assert_eq!(found.line, vec![Line {x1: 0.0, y1: 0.0, x2: 1.0, y2: 0.0}]);
        assert!(found.wedge.is_empty());
    }

    #[test]
    fn a_double_bond_straddles_the_line() {
        let style = Style::default();
        let found = plain(Kind::Double, (0.0, 0.0), (1.0, 0.0)).line;
        assert_eq!(found.len(), 2);
        assert!((found[0].y1 + found[1].y1).abs() < 1e-12, "真ん中をはさんでいません");
        assert!(((found[0].y1 - found[1].y1).abs() - style.double_gap).abs() < 1e-12);
    }

    #[test]
    fn a_triple_bond_keeps_the_middle_line() {
        let style = Style::default();
        let found = plain(Kind::Triple, (0.0, 0.0), (1.0, 0.0)).line;
        assert_eq!(found.len(), 3);
        assert_eq!(found[1], Line {x1: 0.0, y1: 0.0, x2: 1.0, y2: 0.0});
        assert!((found[0].y1.abs() - style.double_gap).abs() < 1e-12);
        assert!((found[0].y1 + found[2].y1).abs() < 1e-12);
    }

    #[test]
    fn a_double_bond_in_a_ring_leans_to_the_inside() {
        let style = Style::default();
        let toward = Placement::Toward {x: 0.5, y: 1.0, inset: 0.0};
        let found = draw(Kind::Double, (0.0, 0.0), (1.0, 0.0), toward, &style).line;
        assert_eq!(found.len(), 2);
        let edge = Line {x1: 0.0, y1: 0.0, x2: 1.0, y2: 0.0};
        assert_eq!(found[0], edge, "外側の線が辺の上にありません");
        assert!(found[1].y1 > 0.0, "内側の線が中心の側にありません");
        assert!((found[1].y1 - style.double_gap).abs() < 1e-12);
    }

    #[test]
    fn the_inside_line_is_trimmed_at_both_ends() {
        let style = Style::default();
        let toward = Placement::Toward {x: 0.5, y: 1.0, inset: 0.1};
        let found = draw(Kind::Double, (0.0, 0.0), (1.0, 0.0), toward, &style).line;
        assert!((found[1].x1 - 0.1).abs() < 1e-12);
        assert!((found[1].x2 - 0.9).abs() < 1e-12);
    }

    #[test]
    fn the_inside_line_flips_with_the_center() {
        let style = Style::default();
        let below = Placement::Toward {x: 0.5, y: -1.0, inset: 0.0};
        let found = draw(Kind::Double, (0.0, 0.0), (1.0, 0.0), below, &style).line;
        assert!(found[1].y1 < 0.0);
    }

    #[test]
    fn a_wedge_is_a_triangle_with_its_point_at_the_start() {
        let style = Style::default();
        let found = plain(Kind::Wedge, (0.0, 0.0), (1.0, 0.0)).wedge;
        assert_eq!(found.len(), 1);
        assert_eq!(found[0].tip, (0.0, 0.0));
        assert!(found[0].filled);
        assert!((found[0].base[0].1 - found[0].base[1].1).abs() - style.wedge_width < 1e-12);
        assert_eq!(found[0].base[0].0, 1.0);
    }

    #[test]
    fn the_backward_wedge_turns_around() {
        let found = plain(Kind::WedgeBack, (0.0, 0.0), (1.0, 0.0)).wedge;
        assert_eq!(found[0].tip, (1.0, 0.0));
        assert_eq!(found[0].base[0].0, 0.0);
    }

    #[test]
    fn a_hollow_wedge_is_not_filled() {
        assert!(!plain(Kind::Hollow, (0.0, 0.0), (1.0, 0.0)).wedge[0].filled);
        assert!(!plain(Kind::HollowBack, (0.0, 0.0), (1.0, 0.0)).wedge[0].filled);
    }

    #[test]
    fn a_hashed_wedge_is_a_row_of_growing_lines() {
        let found = plain(Kind::Hash, (0.0, 0.0), (3.0, 0.0));
        assert!(found.wedge.is_empty());
        assert!(found.line.len() >= 2);
        let first = (found.line[0].y1 - found.line[0].y2).abs();
        let last = found.line.last().map(|one| (one.y1 - one.y2).abs()).unwrap();
        assert!(first < last, "細い側から太い側へ広がっていません");
        assert!((last - Style::default().wedge_width).abs() < 1e-12);
    }

    #[test]
    fn the_hashed_wedge_can_face_the_other_way() {
        let found = plain(Kind::HashBack, (0.0, 0.0), (3.0, 0.0)).line;
        let first = (found[0].y1 - found[0].y2).abs();
        assert!(found[0].x1 > 2.0, "細い側が終わりの側にありません");
        assert!(first < Style::default().wedge_width);
    }

    #[test]
    fn the_offset_follows_the_direction_of_the_bond() {
        let style = Style::default();
        let found = plain(Kind::Double, (0.0, 0.0), (0.0, 1.0)).line;
        assert!(((found[0].x1 - found[1].x1).abs() - style.double_gap).abs() < 1e-12);
        assert_eq!(found[0].y1, 0.0);
    }

    #[test]
    fn a_bond_of_no_length_draws_nothing() {
        let found = plain(Kind::Single, (1.0, 1.0), (1.0, 1.0));
        assert!(found.line.is_empty() && found.wedge.is_empty());
    }
}