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.0
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-10-15
Size
541.1 KB
Downloads
9
Checksum
67f9c361d04f05b0299dd8e9ef5e2a0120d67be36205a76945eb5f7ef90443be
Dependencies
None

layout.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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
//! 構文木に座標を与える
//!
//! 結び目は角度と結合長だけで決まる。原点は最初の原子で、x は右、y は下を正とする。
//! 角度は東を 0 度、反時計回りを正とするので、y が下向きのぶんだけ符号が反転する。
//!
//! ラベルは、墨の箱の左右の真ん中が結び目に来るように置く。縦は大文字の高さの半分だけ
//! ベースラインを下げ、結合が大文字の胴を通るようにする。この高さはラベルの中身によらないので、
//! 鎖が長くなっても結合の高さは揃う。
//!
//! 結合は結び目から結び目へ引いたあと、両端を原子の墨の箱で切り詰める。
//! 印を置かない結び目には箱がないので、切り詰めも空きも入らない。
//!
//! 環は正多角形として置く。中の二重結合は内側へ寄せる。向きの決め方は [`crate::ring`] にある。

use crate::bond::{self, Line, Placement, Wedge};
use crate::error::Error;
use crate::font::{self, Font};
use crate::label::{self, Extent, Label};
use crate::node::{Angle, Bond, Kind, Node, Ring};
use crate::ring;
use crate::style::Style;
use std::collections::HashMap;


/// 置かれた原子
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedAtom {
    /// 結び目の位置
    pub x: f64,
    pub y: f64,
    /// ラベルの左端の位置
    pub origin_x: f64,
    /// ラベルのベースラインの位置
    pub origin_y: f64,
    pub label: Label,
}


/// 芳香環の内側に描く丸
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Circle {
    pub x: f64,
    pub y: f64,
    pub radius: f64,
}


/// 位置の決まった図
#[derive(Debug, Clone, PartialEq)]
pub struct Figure {
    pub atom: Vec<PlacedAtom>,
    pub line: Vec<Line>,
    pub wedge: Vec<Wedge>,
    pub circle: Vec<Circle>,
    /// 図の下に置く名前。`\chemname` で付ける
    pub name: Option<PlacedAtom>,
    /// 図全体を包む範囲
    pub extent: Extent,
}


/// 組み立ての途中の持ち物
struct Work {
    figure: Figure,
    /// 環を閉じる印。名前から、その原子の番号と元の文字列での位置と、書かれた結合の種別へ
    hook: HashMap<String, (usize, usize, Option<Kind>)>,
}


/// 構文木に座標を与える
pub fn layout(node: &Node, font: &Font, style: &Style) -> Result<Figure, Error> {
    let mut work = Work {
        figure: Figure {
            atom: Vec::new(),
            line: Vec::new(),
            wedge: Vec::new(),
            circle: Vec::new(),
            name: None,
            extent: Extent::default(),
        },
        hook: HashMap::new(),
    };
    walk(&mut work, node, (0.0, 0.0), 0.0, None, font, style)?;

    if let Some((name, (_, at, _))) = work.hook.iter().min_by_key(|(_, (_, at, _))| *at) {
        let reason = format!("環を閉じる印 {name} が 1 つしかありません");

        return Err(Error::new(reason, *at));
    }

    work.figure.extent = measure(&work.figure, style);

    Ok(work.figure)
}


/// 節を 1 つ置き、そこから出ていく結合と環をたどる。返すのは置いた原子の番号
///
/// outward は、この原子が環の頂点であるときの、環の外を向く向きである。
/// 向きを書かなかった結合はそちらへ伸びる。環の頂点でなければ真横へ伸びる。
fn walk(
    work: &mut Work,
    node: &Node,
    vertex: (f64, f64),
    incoming: f64,
    outward: Option<f64>,
    font: &Font,
    style: &Style,
) -> Result<usize, Error> {
    let label = label::build(&node.atom.source, font, style);
    let origin_x = vertex.0 - (label.extent.left + label.extent.right) / 2.0;
    let origin_y = vertex.1 + font::cap_height(font) / 2.0;
    let placed = PlacedAtom {x: vertex.0, y: vertex.1, origin_x, origin_y, label};
    work.figure.atom.push(placed);
    let here = work.figure.atom.len() - 1;
    close_hook(work, node, here, style)?;

    // 環を抱えている原子は、その環の最初の頂点である。外は入ってきた向きの反対になる
    let away = match (outward, &node.ring) {
        (Some(found), _) => Some(found),
        (None, Some(_)) => Some(incoming + 180.0),
        (None, None) => None,
    };

    for (bond, child) in &node.branch {
        let angle = match bond.angle {
            Angle::Default => away.unwrap_or(0.0),
            Angle::Absolute(value) => value,
            Angle::Relative(value) => incoming + value,
        };
        let goal = advance(vertex, angle, style.bond_length * bond.length);
        let there = walk(work, child, goal, angle, None, font, style)?;
        connect(work, here, there, bond, Placement::Middle, style)?;
    }

    if let Some(found) = &node.ring {
        place_ring(work, found, here, incoming, font, style)?;
    }

    Ok(here)
}


/// 環を閉じる印を突き合わせる。同じ名前が 2 度目に出たら、そこで結合を引く
fn close_hook(work: &mut Work, node: &Node, here: usize, style: &Style) -> Result<(), Error> {
    for hook in &node.atom.hook {
        match work.hook.remove(&hook.name) {
            None => {
                work.hook.insert(hook.name.clone(), (here, hook.position, hook.kind));
            }
            Some((there, _, opened)) => {
                let kind = hook.kind.or(opened).unwrap_or(Kind::Single);
                let bond = Bond::new(kind, hook.position);
                connect(work, there, here, &bond, Placement::Middle, style)?;
            }
        }
    }

    Ok(())
}


/// 環を置く。最初の頂点は here の原子で、残りを時計回りに並べる
fn place_ring(
    work: &mut Work,
    found: &Ring,
    here: usize,
    incoming: f64,
    font: &Font,
    style: &Style,
) -> Result<(), Error> {
    let size = found.size;
    let span = style.bond_length;
    let vertex = (work.figure.atom[here].x, work.figure.atom[here].y);
    let center = ring::center(size, span, vertex, incoming);
    let mut angle = ring::first_angle(size, incoming);
    let mut point = vertex;
    let mut order = Vec::with_capacity(size);
    order.push(here);

    for member in &found.member {
        point = advance(point, angle, span);
        let away = Some(bearing(center, point));
        order.push(walk(work, member, point, angle, away, font, style)?);
        angle += ring::turn(size);
    }

    let inside = Placement::Toward {
        x: center.0,
        y: center.1,
        inset: ring::inset(size, style.double_gap),
    };

    for (step, bond) in found.bond.iter().enumerate() {
        connect(work, order[step], order[(step + 1) % size], bond, inside, style)?;
    }

    if found.circle {
        work.figure.circle.push(Circle {
            x: center.0,
            y: center.1,
            radius: ring::inradius(size, span) * style.ring_circle,
        });
    }

    Ok(())
}


/// 2 つの原子のあいだに結合を引く
fn connect(
    work: &mut Work,
    here: usize,
    there: usize,
    bond: &Bond,
    placement: Placement,
    style: &Style,
) -> Result<(), Error> {
    let start = (work.figure.atom[here].x, work.figure.atom[here].y);
    let end = (work.figure.atom[there].x, work.figure.atom[there].y);
    let span = bond::length(start, end);

    if span <= 0.0 {
        return Ok(());
    }

    let step = ((end.0 - start.0) / span, (end.1 - start.1) / span);
    let from = clip(&work.figure.atom[here], bond.from, bond.position, style)?;
    let to = clip(&work.figure.atom[there], bond.to, bond.position, style)?;
    let head = exit(&from, step);
    let tail = exit(&to, (-step.0, -step.1));

    if head + tail >= span {
        return Ok(());
    }

    let drawn = bond::draw(
        bond.kind,
        (start.0 + step.0 * head, start.1 + step.1 * head),
        (end.0 - step.0 * tail, end.1 - step.1 * tail),
        placement,
        style,
    );
    work.figure.line.extend(drawn.line);
    work.figure.wedge.extend(drawn.wedge);

    Ok(())
}


/// 点から angle の向きへ span だけ進んだ点
fn advance(from: (f64, f64), angle: f64, span: f64) -> (f64, f64) {
    let radian = angle.to_radians();

    (from.0 + radian.cos() * span, from.1 - radian.sin() * span)
}


/// from から to を見た向き(度)
fn bearing(from: (f64, f64), to: (f64, f64)) -> f64 {
    (-(to.1 - from.1)).atan2(to.0 - from.0).to_degrees()
}


/// 結合を切り詰める箱。結び目を原点とする座標に直し、空きのぶんだけ広げる
fn clip(
    atom: &PlacedAtom,
    number: Option<usize>,
    at: usize,
    style: &Style,
) -> Result<Extent, Error> {
    let Some(found) = label::clip_extent(&atom.label, number) else {
        let reason = format!("{} 番の部分はありません", number.unwrap_or(0));

        return Err(Error::new(reason, at));
    };

    if found.is_blank() {
        return Ok(Extent::default());
    }

    Ok(local(atom, &found).grow(style.bond_offset))
}


/// ラベルの座標での範囲を、結び目を原点とする座標に直す
fn local(atom: &PlacedAtom, extent: &Extent) -> Extent {
    Extent {
        left: atom.origin_x + extent.left - atom.x,
        right: atom.origin_x + extent.right - atom.x,
        top: atom.origin_y + extent.top - atom.y,
        bottom: atom.origin_y + extent.bottom - atom.y,
    }
}


/// 原点から step の向きへ進んだとき、箱を出るまでの長さ
///
/// 箱が原点を含まないこともある。部分の番号を書いたときに起きる。
/// そのときは箱を貫く向きなら向こう側で出るまで、当たらない向きなら 0 になる。
fn exit(extent: &Extent, step: (f64, f64)) -> f64 {
    if extent.is_blank() {
        return 0.0;
    }

    let mut near = f64::NEG_INFINITY;
    let mut far = f64::INFINITY;

    for (direction, low, high) in [
        (step.0, extent.left, extent.right),
        (step.1, extent.top, extent.bottom),
    ] {
        if direction.abs() < 1e-12 {
            if low > 0.0 || high < 0.0 {
                return 0.0;
            }

            continue;
        }

        let first = low / direction;
        let second = high / direction;
        near = near.max(first.min(second));
        far = far.min(first.max(second));
    }

    if near > far {
        return 0.0;
    }

    far.max(0.0)
}


/// 図の下に名前を置く
///
/// 横は図の真ん中にそろえ、縦は図の下端から空きのぶんだけ下へ置く。
/// 置いたあとで図全体の範囲を測り直す
pub fn place_name(figure: &mut Figure, text: &str, font: &Font, style: &Style) {
    let label = label::build(text, font, style);

    if label.extent.is_blank() {
        return;
    }

    let middle = (figure.extent.left + figure.extent.right) / 2.0;
    let origin_x = middle - (label.extent.left + label.extent.right) / 2.0;
    let origin_y = figure.extent.bottom + style.name_gap - label.extent.top;
    figure.name = Some(PlacedAtom {
        x: middle,
        y: origin_y,
        origin_x,
        origin_y,
        label,
    });
    figure.extent = measure(figure, style);
}


/// 図全体を包む範囲
fn measure(figure: &Figure, style: &Style) -> Extent {
    let mut found = Extent::default();

    for atom in figure.atom.iter().chain(figure.name.iter()) {
        if !atom.label.extent.is_blank() {
            found = found.union(&ink(atom).grow(style.darken / 2.0));
        }
    }

    for line in &figure.line {
        let around = Extent {
            left: line.x1.min(line.x2),
            right: line.x1.max(line.x2),
            top: line.y1.min(line.y2),
            bottom: line.y1.max(line.y2),
        };
        found = found.union(&around.grow(style.line_width / 2.0));
    }

    for wedge in &figure.wedge {
        let point = [wedge.tip, wedge.base[0], wedge.base[1]];
        let around = Extent {
            left: point.iter().map(|one| one.0).fold(f64::INFINITY, f64::min),
            right: point.iter().map(|one| one.0).fold(f64::NEG_INFINITY, f64::max),
            top: point.iter().map(|one| one.1).fold(f64::INFINITY, f64::min),
            bottom: point.iter().map(|one| one.1).fold(f64::NEG_INFINITY, f64::max),
        };
        found = found.union(&around.grow(style.line_width / 2.0));
    }

    for circle in &figure.circle {
        let around = Extent {
            left: circle.x - circle.radius,
            right: circle.x + circle.radius,
            top: circle.y - circle.radius,
            bottom: circle.y + circle.radius,
        };
        found = found.union(&around.grow(style.line_width / 2.0));
    }

    found
}


/// 原子の墨が図の座標で占める範囲
pub fn ink(atom: &PlacedAtom) -> Extent {
    Extent {
        left: atom.origin_x + atom.label.extent.left,
        right: atom.origin_x + atom.label.extent.right,
        top: atom.origin_y + atom.label.extent.top,
        bottom: atom.origin_y + atom.label.extent.bottom,
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    use crate::font::sample;
    use crate::parse::parse;

    fn build(source: &str) -> Figure {
        layout(&parse(source).unwrap(), &sample(), &Style::default()).unwrap()
    }

    fn apart(first: &PlacedAtom, second: &PlacedAtom) -> f64 {
        bond::length((first.x, first.y), (second.x, second.y))
    }

    #[test]
    fn the_first_atom_sits_at_the_origin() {
        let figure = build("H-H");
        assert_eq!(figure.atom[0].x, 0.0);
        assert_eq!(figure.atom[0].y, 0.0);
    }

    #[test]
    fn a_plain_bond_goes_east_by_the_bond_length() {
        let figure = build("H-H");
        let style = Style::default();
        assert!((figure.atom[1].x - style.bond_length).abs() < 1e-9);
        assert!(figure.atom[1].y.abs() < 1e-9);
    }

    #[test]
    fn the_bond_is_cut_back_at_both_atoms() {
        let figure = build("H-H");
        let style = Style::default();
        assert_eq!(figure.line.len(), 1);
        let line = figure.line[0];
        assert!(line.y1.abs() < 1e-9 && line.y2.abs() < 1e-9, "結合が水平ではありません");
        assert!(line.x1 > 0.0, "出る側が切り詰められていません");
        assert!(line.x2 < style.bond_length, "入る側が切り詰められていません");
        assert!(line.x2 - line.x1 > 1.0, "切り詰めすぎです");
    }

    #[test]
    fn a_knot_with_no_mark_takes_the_whole_length() {
        let figure = build("-");
        let style = Style::default();
        assert_eq!(figure.line.len(), 1);
        assert_eq!(figure.line[0].x1, 0.0);
        assert!((figure.line[0].x2 - style.bond_length).abs() < 1e-9);
    }

    #[test]
    fn the_bond_passes_through_the_middle_of_the_capitals() {
        let figure = build("H-H");
        let atom = &figure.atom[0];
        let top = atom.origin_y + atom.label.extent.top;
        let bottom = atom.origin_y + atom.label.extent.bottom;
        assert!(top < 0.0 && bottom > 0.0, "結合が字の外を通っています");
    }

    #[test]
    fn an_upward_bond_goes_up_on_the_screen() {
        let figure = build("A-[:90]B");
        assert!(figure.atom[1].y < 0.0);
        assert!(figure.atom[1].x.abs() < 1e-9);
    }

    #[test]
    fn a_relative_angle_adds_to_the_bond_before_it() {
        let figure = build("A-[:30]B-[::60]C");
        let style = Style::default();
        assert!((figure.atom[2].x - figure.atom[1].x).abs() < 1e-9);
        assert!((figure.atom[1].y - figure.atom[2].y - style.bond_length).abs() < 1e-9);
    }

    #[test]
    fn the_numbered_angles_are_forty_five_degrees_apart() {
        assert!(build("A-[2]B").atom[1].x.abs() < 1e-9, "[2] は真上のはずです");
    }

    #[test]
    fn the_length_can_be_scaled() {
        let figure = build("A-[,2]B");
        assert!((figure.atom[1].x - Style::default().bond_length * 2.0).abs() < 1e-9);
    }

    #[test]
    fn a_double_bond_draws_two_lines() {
        let figure = build("O=C=O");
        assert_eq!(figure.atom.len(), 3);
        assert_eq!(figure.line.len(), 4);
    }

    #[test]
    fn a_triple_bond_draws_three_lines() {
        assert_eq!(build("N~N").line.len(), 3);
    }

    #[test]
    fn a_branch_hangs_off_the_atom_it_follows() {
        let figure = build("A-B(-[:90]C)-D");
        let style = Style::default();
        assert_eq!(figure.atom.len(), 4);
        assert!((figure.atom[2].x - style.bond_length).abs() < 1e-9);
        assert!(figure.atom[2].y < 0.0);
        assert!((figure.atom[3].x - style.bond_length * 2.0).abs() < 1e-9);
        assert!(figure.atom[3].y.abs() < 1e-9);
    }

    #[test]
    fn the_box_of_the_figure_holds_everything() {
        let figure = build("H_3C-CH_3");
        let extent = figure.extent;

        for atom in &figure.atom {
            let one = ink(atom);
            assert!(one.left >= extent.left - 1e-9 && one.right <= extent.right + 1e-9);
            assert!(one.top >= extent.top - 1e-9 && one.bottom <= extent.bottom + 1e-9);
        }

        for line in &figure.line {
            assert!(line.x1 >= extent.left && line.x2 <= extent.right);
        }
    }

    #[test]
    fn a_long_label_leaves_a_shorter_bond() {
        let short = build("C-C");
        let long = build("H_3C-CH_3");
        let short_span = short.line[0].x2 - short.line[0].x1;
        let long_span = long.line[0].x2 - long.line[0].x1;
        assert!(long_span < short_span, "ラベルが伸びても結合が短くなっていません");
        assert!(long_span > 0.0);
    }

    #[test]
    fn a_part_number_out_of_range_is_reported() {
        let node = parse("C-[,,5]C").unwrap();
        let found = layout(&node, &sample(), &Style::default()).unwrap_err();
        assert!(found.message.contains("部分"));
    }

    #[test]
    fn a_part_number_picks_that_part() {
        let font = sample();
        let style = Style::default();
        let whole = layout(&parse("CH_3-C").unwrap(), &font, &style).unwrap();
        let first = layout(&parse("CH_3-[,,1]C").unwrap(), &font, &style).unwrap();
        assert!(first.line[0].x1 < whole.line[0].x1, "1 番の部分で切れていません");
    }

    #[test]
    fn a_ring_places_as_many_atoms_as_it_has_vertices() {
        let figure = build("*6(------)");
        assert_eq!(figure.atom.len(), 6);
        assert_eq!(figure.line.len(), 6);
    }

    #[test]
    fn every_vertex_of_a_ring_is_the_same_distance_from_the_center() {
        let figure = build("*6(------)");
        let style = Style::default();
        let center = ring::center(6, style.bond_length, (0.0, 0.0), 0.0);
        let radius = ring::radius(6, style.bond_length);

        for atom in &figure.atom {
            let span = bond::length((atom.x, atom.y), center);
            assert!((span - radius).abs() < 1e-9, "頂点が円周に乗っていません");
        }
    }

    #[test]
    fn every_side_of_a_ring_is_the_bond_length() {
        let figure = build("*5(-----)");
        let style = Style::default();

        for step in 0..5 {
            let span = apart(&figure.atom[step], &figure.atom[(step + 1) % 5]);
            assert!((span - style.bond_length).abs() < 1e-9);
        }
    }

    #[test]
    fn a_plain_hexagon_stands_on_a_flat_side() {
        let figure = build("*6(------)");
        // 向かいの頂点は同じ高さ、上下の辺は水平になる
        assert!((figure.atom[0].y - figure.atom[3].y).abs() < 1e-9);
        assert!((figure.atom[1].y - figure.atom[2].y).abs() < 1e-9);
        assert!(figure.atom[1].y < figure.atom[0].y, "1 番目の頂点が上にありません");
    }

    #[test]
    fn a_ring_grows_along_the_bond_that_reached_it() {
        let figure = build("A-*6(------)");
        let style = Style::default();
        // 環の最初の頂点は A から結合長ぶん右。中心はさらに半径ぶん右
        let first = &figure.atom[1];
        assert!((first.x - style.bond_length).abs() < 1e-9);

        for atom in &figure.atom[1..] {
            assert!(atom.x >= first.x - 1e-9, "環が入ってきた向きの手前へ回り込んでいます");
        }
    }

    #[test]
    fn a_double_bond_in_a_ring_leans_inward() {
        let figure = build("*6(=-----)");
        let style = Style::default();
        let center = ring::center(6, style.bond_length, (0.0, 0.0), 0.0);
        // 内側の線は、辺の上の線より中心に近い
        let outer = (figure.line[0].x1 + figure.line[0].x2) / 2.0;
        let inner = (figure.line[1].x1 + figure.line[1].x2) / 2.0;
        assert!((inner - center.0).abs() < (outer - center.0).abs());
    }

    #[test]
    fn an_aromatic_ring_gets_a_circle_inside() {
        let figure = build("**6(------)");
        assert_eq!(figure.circle.len(), 1);
        let circle = figure.circle[0];
        let style = Style::default();
        assert!(circle.radius < ring::inradius(6, style.bond_length));
        assert!(circle.radius > 0.0);

        for atom in &figure.atom {
            let span = bond::length((atom.x, atom.y), (circle.x, circle.y));
            assert!(span > circle.radius, "丸が頂点の外へ出ています");
        }
    }

    #[test]
    fn a_ring_can_carry_a_branch_that_points_outward() {
        let figure = build("*6(-(-OH)-----)");
        assert_eq!(figure.atom.len(), 7);
        assert_eq!(figure.atom[1].label.width, 0.0, "枝の付け根にラベルは要りません");
        assert!(figure.atom[2].label.width > 0.0, "枝の先の OH がありません");

        // 枝は環の外を向く。中心より遠いところに置かれる
        let style = Style::default();
        let center = ring::center(6, style.bond_length, (0.0, 0.0), 0.0);
        let root = bond::length((figure.atom[1].x, figure.atom[1].y), center);
        let tip = bond::length((figure.atom[2].x, figure.atom[2].y), center);
        assert!(tip > root, "枝が環の内側へ入っています");
    }

    #[test]
    fn a_wedge_comes_out_as_a_triangle() {
        let figure = build("C>H");
        assert_eq!(figure.wedge.len(), 1);
        assert!(figure.wedge[0].filled);
        assert!(figure.line.is_empty());
    }

    #[test]
    fn a_hashed_wedge_comes_out_as_lines() {
        let figure = build("C>:H");
        assert!(figure.wedge.is_empty());
        assert!(figure.line.len() >= 2);
    }

    #[test]
    fn a_hook_closes_a_ring_by_hand() {
        // 3 本の結合で四角を書き、最後の 1 辺を印で閉じる
        let figure = build("?[a]-[:0]-[:-90]-[:180]?[a]");
        assert_eq!(figure.atom.len(), 4);
        assert_eq!(figure.line.len(), 4);

        let span = apart(&figure.atom[0], &figure.atom[3]);
        assert!((span - Style::default().bond_length).abs() < 1e-9);
    }

    #[test]
    fn the_hook_draws_the_bond_that_was_written() {
        // 三角形にして、閉じる辺が鎖に重ならないようにする
        let triangle = "A?[a{}]-[:60]B-[:-60]C?[a{}]";
        let plain = triangle.replace("{}", "");
        assert_eq!(build(&plain).line.len(), 3);
        assert_eq!(build(&plain.replacen("?[a]", "?[a,=]", 1)).line.len(), 4);
        assert_eq!(build(&plain.replacen("?[a]", "?[a,~]", 1)).line.len(), 5);
    }

    #[test]
    fn the_hook_bond_is_written_where_it_was_asked_for() {
        let figure = build("A?[a,=]-[:60]B-[:-60]C?[a]");
        assert_eq!(figure.atom.len(), 3);
        // A と C は水平に並び、閉じた辺の 2 本はその上下へ振り分けられる
        assert!((figure.atom[0].y - figure.atom[2].y).abs() < 1e-9);
        let gap = Style::default().double_gap;
        assert!((figure.line[0].y1 - figure.line[1].y1).abs() - gap < 1e-9);
    }

    #[test]
    fn a_hook_left_open_is_reported() {
        let node = parse("A?[a]-B").unwrap();
        let found = layout(&node, &sample(), &Style::default()).unwrap_err();
        assert!(found.message.contains("環を閉じる印"));
        assert_eq!(found.position, 1);
    }

    #[test]
    fn the_same_name_can_be_used_again() {
        let figure = build("?[a]-[:90]?[a]-[:0]?[a]-[:-90]?[a]");
        assert_eq!(figure.atom.len(), 4);
        // 鎖が 3 本、閉じる辺が 2 本
        assert_eq!(figure.line.len(), 5);
    }

    #[test]
    fn a_name_sits_under_the_figure_and_in_the_middle() {
        let font = sample();
        let style = Style::default();
        let mut figure = layout(&parse("H-O-H").unwrap(), &font, &style).unwrap();
        let before = figure.extent;
        place_name(&mut figure, "water", &font, &style);

        let name = figure.name.as_ref().expect("名前が置かれていません");
        let box_of_name = ink(name);
        assert!(box_of_name.top > before.bottom, "名前が図に重なっています");

        let middle = (before.left + before.right) / 2.0;
        let center = (box_of_name.left + box_of_name.right) / 2.0;
        assert!((center - middle).abs() < 1e-9, "名前が真ん中にありません");
        assert!(figure.extent.bottom > before.bottom, "図の箱が名前を含んでいません");
        assert!((figure.extent.top - before.top).abs() < 1e-9, "上へ伸びています");
    }

    #[test]
    fn an_empty_name_places_nothing() {
        let font = sample();
        let style = Style::default();
        let mut figure = layout(&parse("H-O-H").unwrap(), &font, &style).unwrap();
        let before = figure.extent;
        place_name(&mut figure, "", &font, &style);
        assert!(figure.name.is_none());
        assert_eq!(figure.extent, before);
    }

    #[test]
    fn the_box_holds_the_wedges_and_the_circle() {
        let figure = build("**6(------)");
        let extent = figure.extent;
        let circle = figure.circle[0];
        assert!(circle.x - circle.radius >= extent.left);
        assert!(circle.x + circle.radius <= extent.right);

        let figure = build("C>H");
        let wedge = figure.wedge[0];

        for point in [wedge.tip, wedge.base[0], wedge.base[1]] {
            assert!(point.0 >= figure.extent.left && point.0 <= figure.extent.right);
            assert!(point.1 >= figure.extent.top && point.1 <= figure.extent.bottom);
        }
    }
}