The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

Glyph.vala in libbirdfont

This file is a part of the Birdfont project.

Contributing

Send patches or pull requests to johan.mattsson.m@gmail.com.
Clone this repository: git clone https://github.com/johanmattssonm/birdfont.git

Revisions

View the latest version of libbirdfont/Glyph.vala.
New bezier drawing tool
1 /* 2 Copyright (C) 2012 2013 2014 2015 Johan Mattsson 3 4 This library is free software; you can redistribute it and/or modify 5 it under the terms of the GNU Lesser General Public License as 6 published by the Free Software Foundation; either version 3 of the 7 License, or (at your option) any later version. 8 9 This library is distributed in the hope that it will be useful, but 10 WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 Lesser General Public License for more details. 13 */ 14 15 using Cairo; 16 using Math; 17 using Gee; 18 19 namespace BirdFont { 20 21 public class Glyph : FontDisplay { 22 // Background image 23 BackgroundImage? background_image = null; 24 bool background_image_visible = true; 25 26 // Glyph zoom level 27 public double view_zoom = 0.1; 28 29 public double view_offset_x = 0; 30 public double view_offset_y = 0; 31 Gee.ArrayList<ZoomView> zoom_list = new Gee.ArrayList<ZoomView> (); 32 int zoom_list_index = 0; 33 34 // Paths 35 public Gee.ArrayList<Path> path_list = new Gee.ArrayList<Path> (); 36 public Gee.ArrayList<Path> active_paths = new Gee.ArrayList<Path> (); 37 38 // The point where edit event begun 39 double pointer_begin_x = 0; 40 double pointer_begin_y = 0; 41 42 // Tap last tap event position in pixels 43 int last_tap0_y = 0; 44 int last_tap0_x = 0; 45 int last_tap1_y = 0; 46 int last_tap1_x = 0; 47 double zoom_distance = 0; 48 bool change_view; 49 50 bool ignore_input = false; 51 52 // Current pointer position 53 public double motion_x = 0; 54 public double motion_y = 0; 55 56 // Zoom area 57 double zoom_x1 = 0; 58 double zoom_y1 = 0; 59 double zoom_x2 = 0; 60 double zoom_y2 = 0; 61 bool zoom_area_is_visible = false; 62 63 bool view_is_moving = false; 64 public double move_offset_x = 0; 65 public double move_offset_y = 0; 66 bool move_canvas = false; 67 68 public WidgetAllocation allocation = new WidgetAllocation (); 69 70 public unichar unichar_code = 0; // FIXME: name and unichar should be moved to to glyph collection 71 public string name; 72 73 public double left_limit; 74 public double right_limit; 75 76 // x-height, lsb, etc. 77 public Gee.ArrayList<Line> vertical_help_lines = new Gee.ArrayList<Line> (); 78 public Gee.ArrayList<Line> horizontal_help_lines = new Gee.ArrayList<Line> (); 79 bool show_help_lines = true; 80 bool xheight_lines_visible = false; 81 bool margin_boundaries_visible = false; 82 string new_guide_name = ""; 83 84 Gee.ArrayList<Glyph> undo_list = new Gee.ArrayList<Glyph> (); 85 Gee.ArrayList<Glyph> redo_list = new Gee.ArrayList<Glyph> (); 86 87 string glyph_sequence = ""; 88 bool open = true; 89 90 public static Glyph? background_glyph = null; 91 92 bool empty = false; 93 94 /** Id in the version list. */ 95 public int version_id = 1; 96 97 /** Cache quadratic form on export. */ 98 GlyfData? ttf_data = null; 99 100 Line left_line; 101 Line right_line; 102 103 /** Cache for Cairo rendering */ 104 HashMap<string, Surface> glyph_cache = new HashMap<string, Surface> (); 105 106 public const double CANVAS_MIN = -10000; 107 public const double CANVAS_MAX = 10000; 108 109 public static bool show_orientation_arrow = false; 110 public static double orientation_arrow_opacity = 1; 111 112 public Glyph (string name, unichar unichar_code = 0) { 113 this.name = name; 114 this.unichar_code = unichar_code; 115 116 path_list.add (new Path ()); 117 118 add_help_lines (); 119 120 left_limit = -28; 121 right_limit = 28; 122 } 123 124 public Glyph.no_lines (string name, unichar unichar_code = 0) { 125 this.name = name; 126 this.unichar_code = unichar_code; 127 128 path_list.add (new Path ()); 129 } 130 131 public GlyfData get_ttf_data () { 132 if (ttf_data == null) { 133 134 ttf_data = new GlyfData (this); 135 } 136 137 return (!) ttf_data; 138 } 139 140 public PathList get_quadratic_paths () { 141 PointConverter pc; 142 PathList pl; 143 PathList stroke; 144 145 pl = new PathList (); 146 foreach (Path p in path_list) { 147 if (p.stroke > 0) { 148 stroke = p.get_stroke_fast (); 149 foreach (Path stroke_part in stroke.paths) { 150 pc = new PointConverter (stroke_part); 151 pl.add (pc.get_quadratic_path ()); 152 } 153 } else { 154 pc = new PointConverter (p); 155 pl.add (pc.get_quadratic_path ()); 156 } 157 } 158 159 return pl; 160 } 161 162 public override void close () { 163 undo_list.clear (); 164 redo_list.clear (); 165 } 166 167 public void set_empty_ttf (bool e) { 168 empty = e; 169 } 170 171 public bool is_empty_ttf () { 172 return empty; 173 } 174 175 public void clear_active_paths () { 176 active_paths.clear (); 177 } 178 179 public void add_active_path (Path? p) { 180 Path path; 181 if (p != null) { 182 path = (!) p; 183 184 if (Toolbox.get_move_tool ().is_selected ()) { 185 if (path.stroke > 0) { 186 Toolbox.set_object_stroke (path.stroke); 187 } 188 } 189 190 if (!active_paths.contains (path)) { 191 active_paths.add (path); 192 } 193 PenTool.active_path = path; 194 } 195 } 196 197 public void delete_background () { 198 store_undo_state (); 199 background_image = null; 200 GlyphCanvas.redraw (); 201 } 202 203 public Path? get_active_path () { 204 return_val_if_fail (active_paths.size > 0, null); 205 return active_paths.get (active_paths.size - 1); 206 } 207 208 public bool boundaries (out double x1, out double y1, out double x2, out double y2) { 209 if (path_list.size == 0) { 210 x1 = 0; 211 y1 = 0; 212 x2 = 0; 213 y2 = 0; 214 return false; 215 } 216 217 x1 = CANVAS_MAX; 218 x2 = CANVAS_MIN; 219 y1 = CANVAS_MAX; 220 y2 = CANVAS_MIN; 221 222 foreach (Path p in path_list) { 223 p.update_region_boundaries (); 224 225 if (p.points.size > 1) { 226 if (p.xmin < x1) { 227 x1 = p.xmin; 228 } 229 230 if (p.xmax > x2) { 231 x2 = p.xmax; 232 } 233 234 if (p.ymin < y1) { 235 y1 = p.ymin; 236 } 237 238 if (p.ymax > y2) { 239 y2 = p.ymax; 240 } 241 } 242 } 243 244 return x1 != double.MAX; 245 } 246 247 public void selection_boundaries (out double x, out double y, out double w, out double h) { 248 double px, py, px2, py2; 249 250 px = 10000; 251 py = 10000; 252 px2 = -10000; 253 py2 = -10000; 254 255 foreach (Path p in active_paths) { 256 if (p.xmin < px) { 257 px = p.xmin; 258 } 259 260 if (p.ymin < py) { 261 py = p.ymin; 262 } 263 264 if (p.xmax > px2) { 265 px2 = p.xmax; 266 } 267 268 if (p.ymax > py2) { 269 py2 = p.ymax; 270 } 271 } 272 273 if (px2 == -10000 || px == 10000) { 274 warning (@"No box for selected paths. ($(active_paths.size))"); 275 px = 0; 276 py = 0; 277 px2 = 0; 278 py2 = 0; 279 } 280 281 x = px; 282 y = py2; 283 w = px2 - px; 284 h = py2 - py; 285 } 286 287 /** @return centrum pixel for x coordinates. */ 288 public static double xc () { 289 double c = MainWindow.get_current_glyph ().allocation.width / 2.0; 290 return c; 291 } 292 293 /** @return centrum pixel for y coordinates. */ 294 public static double yc () { 295 double c = MainWindow.get_current_glyph ().allocation.height / 2.0; 296 return c; 297 } 298 299 /** @return 1/view_zoom */ 300 public static double ivz () { 301 return 1 / MainWindow.get_current_glyph ().view_zoom; 302 } 303 304 public void resized (WidgetAllocation alloc) { 305 double a, b, c, d; 306 307 a = Glyph.path_coordinate_x (0); 308 b = Glyph.path_coordinate_y (0); 309 310 this.allocation = alloc; 311 312 c = Glyph.path_coordinate_x (0); 313 d = Glyph.path_coordinate_y (0); 314 315 view_offset_x -= c - a; 316 view_offset_y -= b - d; 317 } 318 319 public void set_background_image (BackgroundImage? b) { 320 BackgroundImage bg; 321 322 if (b == null) { 323 background_image = null; 324 } else { 325 bg = (!) b; 326 background_image = bg; 327 } 328 329 BirdFont.get_current_font ().touch (); 330 } 331 332 public BackgroundImage? get_background_image () { 333 return background_image; 334 } 335 336 public override void scroll_wheel_up (double x, double y) { 337 if (KeyBindings.has_alt ()) { 338 zoom_in_at_point (x, y); 339 } else if (KeyBindings.has_ctrl ()) { 340 view_offset_x -= 10 / view_zoom; 341 } else { 342 view_offset_y -= 10 / view_zoom; 343 } 344 345 redraw_area (0, 0, allocation.width, allocation.height); 346 } 347 348 public override void scroll_wheel_down (double x, double y) { 349 if (KeyBindings.has_alt ()) { 350 zoom_out_at_point (x, y); 351 } else if (KeyBindings.has_ctrl ()) { 352 view_offset_x += 10 / view_zoom; 353 } else { 354 view_offset_y += 10 / view_zoom; 355 } 356 357 redraw_area (0, 0, allocation.width, allocation.height); 358 } 359 360 public void add_path (Path p) { 361 path_list.add (p); 362 } 363 364 public override void selected_canvas () { 365 TimeoutSource input_delay; 366 367 ttf_data = null; // recreate quatradic path on export 368 369 ignore_input = true; // make sure that tripple clicks in overview are ignored 370 371 input_delay = new TimeoutSource (250); 372 input_delay.set_callback(() => { 373 ignore_input = false; 374 return false; 375 }); 376 input_delay.attach (null); 377 378 add_help_lines (); 379 KeyBindings.set_require_modifier (false); 380 glyph_sequence = Preferences.get ("glyph_sequence"); 381 382 GridTool.update_lines (); 383 384 if (!is_null (MainWindow.native_window)) { 385 MainWindow.native_window.set_scrollbar_size (0); 386 } 387 388 update_zoom_bar (); 389 } 390 391 void update_zoom_bar () { 392 if (!is_null (Toolbox.drawing_tools) 393 && !is_null (Toolbox.drawing_tools.zoom_bar)) { 394 Toolbox.drawing_tools.zoom_bar.set_zoom ((view_zoom - 1) / 20); 395 } 396 } 397 398 public void remove_lines () { 399 vertical_help_lines.clear (); 400 horizontal_help_lines.clear (); 401 } 402 403 public void add_help_lines () { 404 remove_lines (); 405 406 return_if_fail (!is_null (BirdFont.get_current_font ())); 407 408 double bgt = BirdFont.get_current_font ().top_limit; 409 Line top_margin_line = new Line ("top margin", bgt, false); 410 top_margin_line.set_color_theme ("Guide 2"); 411 top_margin_line.position_updated.connect ((pos) => { 412 BirdFont.get_current_font ().top_limit = pos; 413 }); 414 415 double thp = BirdFont.get_current_font ().top_position; 416 Line top_line = new Line ("top", thp, false); 417 top_line.position_updated.connect ((pos) => { 418 Font f = BirdFont.get_current_font (); 419 f.top_position = pos; 420 }); 421 422 double xhp = BirdFont.get_current_font ().xheight_position; 423 Line xheight_line = new Line ("x-height", xhp, false); 424 xheight_line.set_color_theme ("Guide 3"); 425 xheight_line.dashed = true; 426 xheight_line.position_updated.connect ((pos) => { 427 Font f = BirdFont.get_current_font (); 428 f.xheight_position = pos; 429 }); 430 431 double xbl = BirdFont.get_current_font ().base_line; 432 Line base_line = new Line ("baseline", xbl, false); 433 base_line.position_updated.connect ((pos) => { 434 Font f = BirdFont.get_current_font (); 435 f.base_line = pos; 436 }); 437 438 double bp = BirdFont.get_current_font ().bottom_position; 439 Line bottom_line = new Line ("bottom", bp, false); 440 bottom_line.position_updated.connect ((pos) => { 441 BirdFont.get_current_font ().bottom_position = pos; 442 }); 443 444 double bgb = BirdFont.get_current_font ().bottom_limit; 445 Line bottom_margin_line = new Line ("bottom margin", bgb, false); 446 bottom_margin_line.set_color_theme ("Guide 2"); 447 bottom_margin_line.position_updated.connect ((pos) => { 448 BirdFont.get_current_font ().bottom_limit = pos; 449 }); 450 451 left_line = new Line ("left", left_limit, true); 452 left_line.lsb = true; 453 left_line.position_updated.connect ((pos) => { 454 left_limit = pos; 455 update_other_spacing_classes (); 456 457 left_line.set_metrics (get_left_side_bearing ()); 458 }); 459 left_line.position_updated (left_limit); 460 461 right_line = new Line ("right", right_limit, true); 462 right_line.rsb = true; 463 right_line.position_updated.connect ((pos) => { 464 right_limit = pos; 465 update_other_spacing_classes (); 466 467 right_line.set_metrics (get_right_side_bearing ()); 468 }); 469 right_line.position_updated (right_limit); 470 471 // lists of lines are sorted and lines are added only if 472 // they are relevant for a particular glyph. 473 474 // left to right 475 add_line (left_line); 476 add_line (right_line); 477 478 bool glyph_has_top = has_top_line (); 479 480 // top to bottom 481 add_line (top_margin_line); 482 top_margin_line.set_visible (margin_boundaries_visible); 483 484 add_line (top_line); 485 top_line.set_visible (glyph_has_top || xheight_lines_visible); 486 487 add_line (xheight_line); 488 xheight_line.set_visible (!glyph_has_top || xheight_lines_visible); 489 490 add_line (base_line); 491 492 add_line (bottom_line); 493 bottom_line.set_visible (CharDatabase.has_descender (unichar_code) || xheight_lines_visible); 494 495 add_line (bottom_margin_line); 496 bottom_margin_line.set_visible (margin_boundaries_visible); 497 498 foreach (Line guide in BirdFont.get_current_font ().custom_guides) { 499 add_line (guide); 500 } 501 } 502 503 public double get_left_side_bearing () { 504 double x1, y1, x2, y2; 505 506 if (get_boundaries (out x1, out y1, out x2, out y2)) { 507 return x1 - left_limit; 508 } else { 509 return right_limit - left_limit; 510 } 511 } 512 513 public double get_right_side_bearing () { 514 double x1, y1, x2, y2; 515 516 if (get_boundaries (out x1, out y1, out x2, out y2)) { 517 return right_limit - x2; 518 } else { 519 return right_limit - left_limit; 520 } 521 } 522 523 public bool get_boundaries (out double x1, out double y1, 524 out double x2, out double y2) { 525 526 double max_x, min_x, max_y, min_y; 527 528 if (path_list.size == 0) { 529 x1 = 0; 530 y1 = 0; 531 x2 = 0; 532 y2 = 0; 533 return false; 534 } 535 536 max_x = CANVAS_MIN; 537 min_x = CANVAS_MAX; 538 max_y = CANVAS_MIN; 539 min_y = CANVAS_MAX; 540 541 // FIXME: optimize 542 foreach (Path p in path_list) { 543 p.all_of_path ((x, y, t) => { 544 if (x > max_x) { 545 max_x = x; 546 } 547 548 if (y > max_y) { 549 max_y = y; 550 } 551 552 if (x < min_x) { 553 min_x = x; 554 } 555 556 if (y < min_y) { 557 min_y = y; 558 } 559 560 return true; 561 }); 562 } 563 564 x1 = min_x; 565 y1 = max_y; 566 x2 = max_x; 567 y2 = min_y; 568 569 return max_x != CANVAS_MIN; 570 } 571 572 bool has_top_line () { 573 return !unichar_code.islower () || CharDatabase.has_ascender (unichar_code); 574 } 575 576 public bool get_show_help_lines () { 577 return show_help_lines; 578 } 579 580 /** Show both x-height and top lines. */ 581 public bool get_xheight_lines_visible () { 582 return xheight_lines_visible; 583 } 584 585 /** Show both x-height and top lines. */ 586 public void set_xheight_lines_visible (bool x) { 587 xheight_lines_visible = x; 588 add_help_lines (); 589 } 590 591 public void set_margin_lines_visible (bool m) { 592 margin_boundaries_visible = m; 593 add_help_lines (); 594 } 595 596 public bool get_margin_lines_visible () { 597 return margin_boundaries_visible; 598 } 599 600 public void remove_empty_paths () { 601 foreach (Path p in path_list) { 602 if (p.points.size < 2) { 603 delete_path (p); 604 remove_empty_paths (); 605 return; 606 } 607 } 608 } 609 610 public void delete_path (Path p) requires (path_list.size > 0) { 611 path_list.remove (p); 612 } 613 614 public string get_svg_data () { 615 return Svg.to_svg_glyph (this); 616 } 617 618 public int get_height () { 619 Font f = BirdFont.get_current_font (); 620 return (int) Math.fabs (f.top_limit - f.bottom_limit); 621 } 622 623 public double get_width () { 624 return Math.fabs (right_limit - left_limit); 625 } 626 627 public unichar get_unichar () { 628 return unichar_code; 629 } 630 631 public string get_unichar_string () { 632 string? s = (!)get_unichar ().to_string (); 633 634 if (unlikely (s == null)) { 635 warning ("Invalid character."); 636 return "".dup (); 637 } 638 639 return (!) s; 640 } 641 642 public void redraw_help_lines () { 643 redraw_area (0, 0, allocation.width, allocation.height); 644 } 645 646 public void set_show_help_lines (bool hl) { 647 show_help_lines = hl; 648 } 649 650 private void add_line (Line line) { 651 if (line.is_vertical ()) { 652 vertical_help_lines.add (line); 653 } else { 654 horizontal_help_lines.add (line); 655 } 656 657 sort_help_lines (); 658 659 line.queue_draw_area.connect ((x, y, w, h) => { 660 this.redraw_area (x, y, w, h); 661 }); 662 } 663 664 public void sort_help_lines () { 665 vertical_help_lines.sort ((a, b) => { 666 Line first, next; 667 first = (Line) a; 668 next = (Line) b; 669 return (int) (first.get_pos () - next.get_pos ()); 670 }); 671 672 horizontal_help_lines.sort ((a, b) => { 673 Line first, next; 674 first = (Line) a; 675 next = (Line) b; 676 return (int) (first.get_pos () - next.get_pos ()); 677 }); 678 } 679 680 public override string get_name () { 681 return name; 682 } 683 684 public override string get_label () { 685 return name; 686 } 687 688 private void help_line_event (int x, int y) { 689 bool m = false; 690 691 foreach (Line line in vertical_help_lines) { 692 if (!m && line.event_move_to (x, y, allocation)) { 693 m = true; 694 } 695 } 696 697 foreach (Line line in horizontal_help_lines) { 698 if (!m && line.event_move_to (x, y, allocation)) { 699 m = true; 700 } 701 } 702 } 703 704 public override void key_release (uint keyval) { 705 Tool t; 706 t = MainWindow.get_toolbox ().get_current_tool (); 707 t.key_release_action (t, keyval); 708 709 if (keyval == (uint)' ') { 710 move_canvas = false; 711 } 712 } 713 714 public override void key_press (uint keyval) { 715 Tool t = MainWindow.get_toolbox ().get_current_tool (); 716 t.key_press_action (t, keyval); 717 718 if (keyval == (uint)' ') { 719 move_canvas = true; 720 } 721 722 switch (keyval) { 723 case Key.NUM_PLUS: 724 zoom_in (); 725 break; 726 case Key.NUM_MINUS: 727 zoom_out (); 728 break; 729 } 730 } 731 732 /** Delete edit point from path. 733 * @return false if no points was deleted 734 */ 735 public bool process_deleted () { 736 Gee.ArrayList<Path> deleted_paths = new Gee.ArrayList<Path> (); 737 foreach (Path p in path_list) { 738 if (p.points.size > 0) { 739 if (process_deleted_points_in_path (p)) { 740 return true; 741 } 742 } else { 743 deleted_paths.add (p); 744 } 745 } 746 747 foreach (Path p in deleted_paths) { 748 delete_path (p); 749 } 750 751 return false; 752 } 753 754 private bool process_deleted_points_in_path (Path p) { 755 PathList remaining_points; 756 remaining_points = p.process_deleted_points (); 757 foreach (Path path in remaining_points.paths) { 758 add_path (path); 759 path.reopen (); 760 path.create_list (); 761 762 add_active_path (path); 763 } 764 765 if (remaining_points.paths.size > 0) { 766 delete_path (p); 767 return true; 768 } 769 770 return false; 771 } 772 773 public override void motion_notify (double x, double y) { 774 Tool t = MainWindow.get_toolbox ().get_current_tool (); 775 776 if (view_is_moving) { 777 move_view_offset (x, y); 778 return; 779 } 780 781 help_line_event ((int) x, (int) y); 782 t.move_action (t, (int) x, (int) y); 783 784 motion_x = x * ivz () - xc () + view_offset_x; 785 motion_y = yc () - y * ivz () - view_offset_y; 786 } 787 788 public override void button_release (int button, double ex, double ey) { 789 bool line_moving = false; 790 view_is_moving = false; 791 792 foreach (Line line in get_all_help_lines ()) { 793 if (!line.set_move (false)) { 794 line_moving = true; 795 } 796 } 797 798 if (!line_moving) { 799 Tool t = MainWindow.get_toolbox ().get_current_tool (); 800 t.release_action (t, (int) button, (int) ex, (int) ey); 801 } 802 803 update_view (); 804 } 805 806 private Gee.ArrayList<Line> get_all_help_lines () { 807 Gee.ArrayList<Line> all_lines = new Gee.ArrayList<Line> (); 808 809 foreach (Line l in vertical_help_lines) { 810 all_lines.add (l); 811 } 812 813 foreach (Line l in horizontal_help_lines) { 814 all_lines.add (l); 815 } 816 817 if (GridTool.is_visible ()) { 818 foreach (Line l in GridTool.get_vertical_lines ()) { 819 all_lines.add (l); 820 } 821 822 foreach (Line l in GridTool.get_horizontal_lines ()) { 823 all_lines.add (l); 824 } 825 } 826 827 return all_lines; 828 } 829 830 public void update_view () { 831 GlyphCanvas.redraw (); 832 } 833 834 public override void double_click (uint button, double ex, double ey) { 835 Tool t = MainWindow.get_toolbox ().get_current_tool (); 836 t.double_click_action (t, (int) button, (int) ex, (int) ey); 837 } 838 839 public override void button_press (uint button, double ex, double ey) { 840 bool moving_lines = false; 841 842 pointer_begin_x = ex; 843 pointer_begin_y = ey; 844 845 foreach (Line line in horizontal_help_lines) { 846 if (!moving_lines && line.is_visible () && line.button_press (button)) { 847 moving_lines = true; 848 } 849 } 850 851 foreach (Line line in vertical_help_lines) { 852 if (!moving_lines && line.is_visible () && line.button_press (button)) { 853 moving_lines = true; 854 } 855 } 856 857 if (moving_lines) { 858 return; 859 } 860 861 if (move_canvas || DrawingTools.move_canvas.is_selected ()) { 862 view_is_moving = true; 863 move_offset_x = view_offset_x; 864 move_offset_y = view_offset_y; 865 } else { 866 Tool t = MainWindow.get_toolbox ().get_current_tool (); 867 t.press_action (t, (int) button, (int) ex, (int) ey); 868 } 869 } 870 871 /** Add new points to this path. */ 872 public void set_active_path (Path p) { 873 path_list.remove (p); 874 path_list.add (p); 875 p.reopen (); 876 clear_active_paths (); 877 add_active_path (p); 878 } 879 880 /** Insert new edit point for current path on the appropriate zoom 881 * level. 882 */ 883 public PointSelection add_new_edit_point (int x, int y) { 884 return insert_edit_point (x, y); 885 } 886 887 /** Move view port centrum to this coordinate. */ 888 public void set_center (double x, double y) { 889 x -= allocation.width / 2.0; 890 y -= allocation.height / 2.0; 891 892 view_offset_x += x / view_zoom; 893 view_offset_y += y / view_zoom; 894 } 895 896 public void set_zoom_from_area () { 897 double x = Math.fmin (zoom_x1, zoom_x2); 898 double y = Math.fmin (zoom_y1, zoom_y2); 899 900 double w = Math.fabs (zoom_x1 - zoom_x2); 901 double h = Math.fabs (zoom_y1 - zoom_y2); 902 903 double view_zoom_x, view_zoom_y; 904 double ivz, off; 905 906 if (move_canvas) { 907 return; 908 } 909 910 if (Path.distance (x, x + w, y, y + h) < 7) { 911 zoom_in (); 912 } else { 913 view_offset_x += x / view_zoom; 914 view_offset_y += y / view_zoom; 915 916 if (unlikely (allocation.width == 0 || allocation.height == 0)) { 917 return; 918 } 919 920 view_zoom_x = allocation.width * view_zoom / w; 921 view_zoom_y = allocation.height * view_zoom / h; 922 923 // TODO: there is a max zoom level 924 925 if (view_zoom_x * allocation.width < view_zoom_y * allocation.height) { 926 view_zoom = view_zoom_x; 927 ivz = 1 / view_zoom; 928 929 off = (view_zoom / view_zoom_y) * allocation.height / view_zoom; 930 off = allocation.height/view_zoom - off; 931 off /= 2; 932 933 view_offset_y -= off; 934 935 } else { 936 view_zoom = view_zoom_y; 937 ivz = 1 / view_zoom; 938 939 off = (view_zoom / view_zoom_x) * allocation.width / view_zoom; 940 off = allocation.width / view_zoom - off; 941 off /= 2; 942 943 view_offset_x -= off; 944 } 945 946 zoom_area_is_visible = false; 947 store_current_view (); 948 } 949 950 update_zoom_bar (); 951 } 952 953 public void show_zoom_area (int sx, int sy, int nx, int ny) { 954 double x, y, w, h; 955 956 set_zoom_area (sx, sy, nx, ny); 957 958 zoom_area_is_visible = true; 959 960 x = Math.fmin (zoom_x1, zoom_x2) - 50; 961 y = Math.fmin (zoom_y1, zoom_y2) - 50; 962 963 w = Math.fabs (zoom_x1 - zoom_x2) + 100; 964 h = Math.fabs (zoom_y1 - zoom_y2) + 100; 965 966 redraw_area ((int)x, (int)y, (int)w, (int)h); 967 } 968 969 public void set_zoom_area (int sx, int sy, int nx, int ny) { 970 zoom_x1 = sx; 971 zoom_y1 = sy; 972 zoom_x2 = nx; 973 zoom_y2 = ny; 974 } 975 976 public static void validate_zoom () { 977 Glyph g = MainWindow.get_current_glyph (); 978 if (unlikely (g.view_zoom == 0)) { 979 warning ("Zoom is zero."); 980 g.reset_zoom (); 981 982 if (g.view_zoom == 0) { 983 g.view_zoom = 1; 984 g.view_offset_x = 0; 985 g.view_offset_y = 0; 986 } 987 } 988 } 989 990 public static double path_coordinate_x (double x) { 991 Glyph g = MainWindow.get_current_glyph (); 992 validate_zoom (); 993 return x * ivz () - xc () + g.view_offset_x; 994 } 995 996 public static int reverse_path_coordinate_x (double x) { 997 Glyph g = MainWindow.get_current_glyph (); 998 validate_zoom (); 999 return (int) Math.rint ((x - g.view_offset_x + xc ()) * g.view_zoom); 1000 } 1001 1002 public static double precise_reverse_path_coordinate_x (double x) { 1003 Glyph g = MainWindow.get_current_glyph (); 1004 validate_zoom (); 1005 return (x - g.view_offset_x + xc ()) * g.view_zoom; 1006 } 1007 1008 public static double path_coordinate_y (double y) { 1009 Glyph g = MainWindow.get_current_glyph (); 1010 validate_zoom (); 1011 return yc () - y * ivz () - g.view_offset_y; 1012 } 1013 1014 public static int reverse_path_coordinate_y (double y) { 1015 Glyph g = MainWindow.get_current_glyph (); 1016 validate_zoom (); 1017 y = Math.rint ((y + g.view_offset_y - yc ()) * g.view_zoom); 1018 return (int) (-y); 1019 } 1020 1021 public static double precise_reverse_path_coordinate_y (double y) { 1022 Glyph g = MainWindow.get_current_glyph (); 1023 validate_zoom (); 1024 y = (y + g.view_offset_y - yc ()) * g.view_zoom; 1025 return -y; 1026 } 1027 1028 public bool select_path (double x, double y) { 1029 Path? p = null; 1030 bool found = false; 1031 1032 foreach (Path pt in path_list) { 1033 if (pt.is_over (x, y)) { 1034 p = pt; 1035 found = true; 1036 } 1037 } 1038 1039 if (!KeyBindings.has_shift ()) { 1040 clear_active_paths (); 1041 } 1042 1043 add_active_path (p); 1044 1045 return found; 1046 } 1047 1048 public bool is_over_selected_path (double x, double y) { 1049 foreach (Path pt in active_paths) { 1050 if (pt.is_over (x, y)) { 1051 return true; 1052 } 1053 } 1054 return false; 1055 } 1056 1057 public void queue_redraw_path (Path path) { 1058 redraw_path (path.xmin, path.ymin, path.xmax, path.ymax); 1059 } 1060 1061 private void redraw_path (double xmin, double ymin, double xmax, double ymax) { 1062 int yc = (int)(allocation.height / 2.0); 1063 1064 double yta = yc - ymin - view_offset_y; 1065 double ytb = yc - ymax - view_offset_y; 1066 1067 double xta = -view_offset_x - xmin; 1068 double xtb = -view_offset_x - xmax; 1069 1070 redraw_area ((int)xtb - 10, (int)yta - 10, (int)(xtb - xta) + 10, (int) (yta - ytb) + 10); 1071 } 1072 1073 public Path get_closeset_path (double x, double y) { 1074 double d; 1075 EditPoint ep = new EditPoint (); 1076 1077 Path min_point = new Path (); 1078 double min_distance = double.MAX; 1079 1080 double xt = path_coordinate_x (x); 1081 double yt = path_coordinate_y (y); 1082 1083 foreach (Path p in path_list) { 1084 if (p.is_over (xt, yt)) { 1085 return p; 1086 } 1087 } 1088 1089 foreach (Path p in path_list) { 1090 if (p.points.size == 0) continue; 1091 1092 p.get_closest_point_on_path (ep, xt, yt); 1093 d = Math.pow (ep.x - xt, 2) + Math.pow (ep.y - yt, 2); 1094 1095 if (d < min_distance) { 1096 min_distance = d; 1097 min_point = p; 1098 } 1099 1100 } 1101 1102 // a path without any editpoints 1103 if (path_list.size > 0) { 1104 return path_list.get (0); 1105 } 1106 1107 if (unlikely (min_distance == double.MAX)) { 1108 warning (@"No path found in path_list. Length: $(path_list.size)"); 1109 } 1110 1111 return min_point; 1112 } 1113 1114 private PointSelection insert_edit_point (double x, double y) { 1115 double xt, yt; 1116 Path np; 1117 EditPoint inserted; 1118 bool stroke = StrokeTool.add_stroke; 1119 1120 if (active_paths.size == 0) { 1121 np = new Path (); 1122 path_list.add (np); 1123 np.stroke = stroke ? StrokeTool.stroke_width : 0; 1124 add_active_path (np); 1125 PenTool.active_path = np; 1126 } 1127 1128 xt = path_coordinate_x (x); 1129 yt = path_coordinate_y (y); 1130 1131 return_val_if_fail (active_paths.size > 0, new PointSelection.empty ()); 1132 1133 if (PenTool.active_path.is_open ()) { 1134 np = PenTool.active_path; 1135 np.add (xt, yt); 1136 } else { 1137 np = new Path (); 1138 np.stroke = stroke ? StrokeTool.stroke_width : 0; 1139 path_list.add (np); 1140 np.add (xt, yt); 1141 1142 if (DrawingTools.pen_tool.is_selected ()) { 1143 np.set_stroke (PenTool.path_stroke_width); 1144 } 1145 1146 PenTool.active_path = np; 1147 } 1148 1149 clear_active_paths (); 1150 add_active_path (np); 1151 PenTool.active_path = np; 1152 1153 inserted = np.points.get (np.points.size - 1); 1154 1155 return new PointSelection (inserted, np); 1156 } 1157 1158 public void move_selected_edit_point_coordinates (EditPoint selected_point, double xt, double yt) { 1159 double x, y; 1160 1161 BirdFont.get_current_font ().touch (); 1162 1163 x = reverse_path_coordinate_x (xt); 1164 y = reverse_path_coordinate_y (yt); 1165 1166 // redraw control point 1167 redraw_area ((int)(x - 4*view_zoom), (int)(y - 4*view_zoom), (int)(x + 3*view_zoom), (int)(y + 3*view_zoom)); 1168 1169 // update position of selected point 1170 selected_point.set_position (xt, yt); 1171 1172 if (view_zoom >= 2) { 1173 redraw_area (0, 0, allocation.width, allocation.height); 1174 } else { 1175 redraw_last_stroke (x, y); 1176 } 1177 } 1178 1179 public void move_selected_edit_point (EditPoint selected_point, double x, double y) { 1180 double xt = path_coordinate_x (x); 1181 double yt = path_coordinate_y (y); 1182 move_selected_edit_point_coordinates (selected_point, xt, yt); 1183 } 1184 1185 public void redraw_segment (EditPoint a, EditPoint b) { 1186 double margin = 10; 1187 double x = Math.fmin (reverse_path_coordinate_x (a.x), reverse_path_coordinate_x (b.x)) - margin; 1188 double y = Math.fmin (reverse_path_coordinate_y (a.y), reverse_path_coordinate_y(b.y)) - margin; 1189 double w = Math.fabs (reverse_path_coordinate_x (a.x) - reverse_path_coordinate_x(b.x)) + 2 * margin; 1190 double h = Math.fabs (reverse_path_coordinate_y (a.y) - reverse_path_coordinate_y (b.y)) + 2 * margin; 1191 1192 redraw_area ((int)x, (int)y, (int)w, (int)h); 1193 } 1194 1195 private void redraw_last_stroke (double x, double y) { 1196 // redraw line, if we have more than one new point on path 1197 double px = 0; 1198 double py = 0; 1199 int tw = 0; 1200 int th = 0; 1201 1202 double xc = (allocation.width / 2.0); 1203 1204 if (active_paths.size == 0) { 1205 return; 1206 } 1207 1208 foreach (Path path in active_paths) { 1209 EditPoint p; 1210 EditPoint pl = path.get_last_point (); 1211 1212 if (pl.prev != null) { 1213 p = pl.get_prev (); 1214 1215 px = p.x + xc; 1216 py = p.y - xc; 1217 1218 tw = (px > x) ? (int) (px - x) : (int) (x - px); 1219 th = (py > y) ? (int) (py - y) : (int) (y - py); 1220 1221 if (px > x) px -= tw + 60; 1222 if (py > y) py -= th + 60; 1223 1224 } else { 1225 px = x - 60; 1226 py = y - 60; 1227 tw = 0; 1228 th = 0; 1229 } 1230 } 1231 1232 redraw_area ((int)px - 20, (int)py - 20, tw + 120, th + 120); 1233 } 1234 1235 public Path? get_last_path () 1236 ensures (result != null) 1237 { 1238 return_val_if_fail (path_list.size > 0, null); 1239 return path_list.get (path_list.size - 1); 1240 } 1241 1242 public bool has_active_path () { 1243 return active_paths.size > 0; 1244 } 1245 1246 public bool is_open () { 1247 return open; 1248 } 1249 1250 /** Close all editable paths and return false if no path have been closed. */ 1251 public bool close_path () { 1252 bool r = false; 1253 1254 foreach (Path p in path_list) { 1255 if (p.is_editable ()) { 1256 r = true; 1257 p.set_editable (false); 1258 } 1259 1260 if (p.is_open ()) { 1261 p.convert_path_ending_to_line (); 1262 } 1263 } 1264 1265 open = false; 1266 clear_active_paths (); 1267 GlyphCanvas.redraw (); 1268 1269 return r; 1270 } 1271 1272 public void open_path () { 1273 foreach (Path p in path_list) { 1274 p.set_editable (true); 1275 p.recalculate_linear_handles (); 1276 1277 if (p.is_open () && p.points.size > 0) { 1278 p.get_first_point ().set_reflective_handles (false); 1279 p.get_first_point ().set_tie_handle (false); 1280 p.get_last_point ().set_reflective_handles (false); 1281 p.get_last_point ().set_tie_handle (false); 1282 } 1283 } 1284 1285 open = true; 1286 redraw_area (0, 0, allocation.width, allocation.height); 1287 } 1288 1289 public void redraw_path_region (Path p) { 1290 int x, y, w, h; 1291 1292 p.update_region_boundaries (); 1293 1294 x = reverse_path_coordinate_x (p.xmin); 1295 y = reverse_path_coordinate_x (p.xmin); 1296 w = reverse_path_coordinate_x (p.xmax) - x; 1297 h = reverse_path_coordinate_x (p.ymax) - y; // FIXME: redraw path 1298 1299 redraw_area (x, y, w, h); 1300 } 1301 1302 public Line get_line (string name) { 1303 foreach (Line line in vertical_help_lines) { 1304 if (likely (line.get_label () == name)) { 1305 return line; 1306 } 1307 } 1308 1309 foreach (Line line in horizontal_help_lines) { 1310 if (likely (line.get_label () == name)) { 1311 return line; 1312 } 1313 } 1314 1315 warning (@"No line with label $name found"); 1316 return new Line ("Err"); 1317 } 1318 1319 public override void zoom_in () { 1320 if (move_canvas) { 1321 return; 1322 } 1323 1324 set_zoom_area (10, 10, allocation.width - 10, allocation.height - 10); 1325 set_zoom_from_area (); 1326 update_view (); 1327 update_zoom_bar (); 1328 } 1329 1330 public override void zoom_out () { 1331 double w = allocation.width; 1332 int n = (int) (10 * ((w - 10) / allocation.width)); 1333 set_zoom_area (-n, -n, allocation.width + n, allocation.height + n); 1334 set_zoom_from_area (); 1335 update_view (); 1336 update_zoom_bar (); 1337 } 1338 1339 public override void zoom_max () { 1340 default_zoom (); 1341 update_zoom_bar (); 1342 } 1343 1344 public override void zoom_min () { 1345 double ax = 1000; 1346 double ay = 1000; 1347 double bx = -1000; 1348 double by = -1000; 1349 1350 int iax, iay, ibx, iby; 1351 1352 reset_zoom (); 1353 1354 foreach (var p in path_list) { 1355 p.update_region_boundaries (); 1356 1357 if (p.points.size > 2) { 1358 if (p.xmin < ax) ax = p.xmin; 1359 if (p.ymin < ay) ay = p.ymin; 1360 if (p.xmax > bx) bx = p.xmax; 1361 if (p.ymax > by) by = p.ymax; 1362 } 1363 } 1364 1365 if (ax == 1000) return; // empty page 1366 1367 iax = (int) ((ax + view_offset_x + allocation.width / 2.0) * view_zoom); 1368 iay = (int) ((-ay + view_offset_y + allocation.height / 2.0) * view_zoom); 1369 ibx = (int) ((bx + view_offset_x + allocation.width / 2.0) * view_zoom); 1370 iby = (int) ((-by + view_offset_y + allocation.height / 2.0) * view_zoom); 1371 1372 show_zoom_area (iax, iay, ibx, iby); // set this later on button release 1373 set_zoom_from_area (); 1374 zoom_out (); // add some margin 1375 1376 redraw_area (0, 0, allocation.width, allocation.height); 1377 update_zoom_bar (); 1378 } 1379 1380 public override void store_current_view () { 1381 ZoomView n; 1382 1383 if (zoom_list_index + 1 < zoom_list.size) { 1384 n = zoom_list.get (zoom_list_index); 1385 while (n != zoom_list.get (zoom_list.size - 1)) { 1386 zoom_list.remove_at (zoom_list.size - 1); 1387 } 1388 } 1389 1390 zoom_list.add (new ZoomView (view_offset_x, view_offset_y, view_zoom, allocation)); 1391 zoom_list_index = (int) zoom_list.size - 1; 1392 1393 if (zoom_list.size > 50) { 1394 zoom_list.remove_at (0); 1395 } 1396 } 1397 1398 public override void restore_last_view () { 1399 if (zoom_list.size == 0 || zoom_list_index - 1 < 0 || zoom_list.size == 0) { 1400 return; 1401 } 1402 1403 zoom_list_index--; 1404 1405 ZoomView z = zoom_list.get (zoom_list_index); 1406 1407 view_offset_x = z.x; 1408 view_offset_y = z.y; 1409 view_zoom = z.zoom; 1410 allocation = z.allocation; 1411 1412 update_zoom_bar (); 1413 } 1414 1415 public override void next_view () { 1416 ZoomView z; 1417 1418 if (zoom_list.size == 0 || zoom_list_index + 1 >= zoom_list.size) { 1419 return; 1420 } 1421 1422 zoom_list_index++; 1423 1424 z = zoom_list.get (zoom_list_index); 1425 1426 view_offset_x = z.x; 1427 view_offset_y = z.y; 1428 view_zoom = z.zoom; 1429 allocation = z.allocation; 1430 1431 update_zoom_bar (); 1432 } 1433 1434 public override void reset_zoom () { 1435 view_offset_x = 0; 1436 view_offset_y = 0; 1437 1438 set_zoom (1); 1439 1440 store_current_view (); 1441 update_zoom_bar (); 1442 } 1443 1444 /** Get x-height or top line. */ 1445 public Line get_upper_line () { 1446 if (has_top_line () || xheight_lines_visible) { 1447 return get_line ("top"); 1448 } 1449 1450 return get_line ("x-height"); 1451 } 1452 1453 /** Get base line. */ 1454 public Line get_lower_line () { 1455 return get_line ("baseline"); 1456 } 1457 1458 /** Set default zoom. See default_zoom. */ 1459 public void set_default_zoom () { 1460 int bottom = 0; 1461 int top = 0; 1462 int left = 0; 1463 int right = 0; 1464 1465 return_if_fail (vertical_help_lines.size != 0); 1466 return_if_fail (horizontal_help_lines.size != 0); 1467 1468 reset_zoom (); 1469 1470 bottom = get_lower_line ().get_position_pixel (); 1471 top = get_upper_line ().get_position_pixel (); 1472 1473 left = vertical_help_lines.get (vertical_help_lines.size - 1).get_position_pixel (); 1474 right = vertical_help_lines.get (0).get_position_pixel (); 1475 1476 set_zoom_area (left + 10, top - 10, right - 10, bottom + 10); 1477 set_zoom_from_area (); 1478 } 1479 1480 /** Set default zoom and redraw canvas. */ 1481 public void default_zoom () { 1482 set_default_zoom (); 1483 update_view (); 1484 } 1485 1486 public bool is_empty () { 1487 foreach (Path p in path_list) { 1488 if (p.points.size > 0) { 1489 return false; 1490 } 1491 } 1492 1493 return true; 1494 } 1495 1496 public void set_zoom (double z) 1497 requires (z > 0) 1498 { 1499 view_zoom = z; 1500 } 1501 1502 public void set_background_visible (bool visibility) { 1503 background_image_visible = visibility; 1504 } 1505 1506 public bool get_background_visible () { 1507 return background_image_visible; 1508 } 1509 1510 private void draw_coordinate (Context cr) { 1511 Theme.color (cr, "Table Border"); 1512 cr.set_font_size (12); 1513 cr.move_to (0, 10); 1514 cr.show_text (@"($motion_x, $motion_y)"); 1515 cr.stroke (); 1516 } 1517 1518 /** Draw filled paths. */ 1519 public void draw_paths (Context cr) { 1520 PathList stroke; 1521 1522 cr.save (); 1523 cr.new_path (); 1524 foreach (Path p in path_list) { 1525 if (p.stroke > 0) { 1526 stroke = p.get_stroke_fast (); 1527 draw_path_list (stroke, cr, Color.black ()); 1528 } else { 1529 p.draw_path (cr, this, Color.black ()); 1530 } 1531 } 1532 cr.fill (); 1533 cr.restore (); 1534 } 1535 1536 public void draw_path (Context cr) { 1537 PathList stroke; 1538 Color color; 1539 1540 cr.save (); 1541 cr.new_path (); 1542 foreach (Path p in path_list) { 1543 if (p.stroke > 0) { 1544 stroke = p.get_stroke_fast (); 1545 1546 if (p.is_editable ()) { 1547 color = Theme.get_color ("Filled Stroke"); 1548 color.a = 0.8; 1549 } else { 1550 color = Color.black (); 1551 } 1552 1553 draw_path_list (stroke, cr, color); 1554 } 1555 } 1556 cr.fill (); 1557 cr.restore (); 1558 1559 if (!(MainWindow.get_toolbox ().get_current_tool () is PenTool) 1560 && !(MainWindow.get_toolbox ().get_current_tool () is PointTool) 1561 && !(MainWindow.get_toolbox ().get_current_tool () is TrackTool) 1562 && !(MainWindow.get_toolbox ().get_current_tool () is ForesightTool)) { 1563 cr.save (); 1564 cr.new_path (); 1565 foreach (Path p in active_paths) { 1566 if (p.stroke > 0) { 1567 stroke = p.get_stroke_fast (); 1568 color = Theme.get_color ("Selected Objects"); 1569 draw_path_list (stroke, cr, color); 1570 } 1571 } 1572 cr.fill (); 1573 cr.restore (); 1574 } 1575 1576 if (is_open () && Path.fill_open_path) { 1577 cr.save (); 1578 cr.new_path (); 1579 foreach (Path p in path_list) { 1580 if (p.stroke == 0) { 1581 p.draw_path (cr, this, get_path_fill_color ()); 1582 } 1583 } 1584 cr.fill (); 1585 cr.restore (); 1586 } 1587 1588 if (is_open ()) { 1589 cr.save (); 1590 cr.new_path (); 1591 foreach (Path p in path_list) { 1592 p.draw_outline (cr); 1593 p.draw_edit_points (cr); 1594 } 1595 cr.restore (); 1596 } 1597 1598 if (!is_open ()) { 1599 // This was good for testing but it is way too slow: 1600 // Svg.draw_svg_path (cr, get_svg_data (), Glyph.xc () + left, Glyph.yc () - baseline); 1601 1602 1603 cr.save (); 1604 cr.new_path (); 1605 foreach (Path p in path_list) { 1606 if (p.stroke == 0) { 1607 p.draw_path (cr, this, Color.black ()); 1608 } 1609 } 1610 cr.close_path (); 1611 cr.fill (); 1612 cr.restore (); 1613 1614 foreach (Path p in active_paths) { 1615 cr.save (); 1616 cr.new_path (); 1617 if (p.stroke == 0) { 1618 p.draw_path (cr, this); 1619 } 1620 cr.close_path (); 1621 cr.fill (); 1622 cr.restore (); 1623 } 1624 } 1625 1626 if (show_orientation_arrow) { 1627 foreach (Path p in path_list) { 1628 if (p.stroke > 0) { 1629 stroke = p.get_stroke_fast (); 1630 foreach (Path ps in stroke.paths) { 1631 ps.draw_orientation_arrow (cr, orientation_arrow_opacity); 1632 } 1633 } else { 1634 p.draw_orientation_arrow (cr, orientation_arrow_opacity); 1635 } 1636 } 1637 } 1638 } 1639 1640 private Color get_path_fill_color () { 1641 return Theme.get_color ("Fill Color"); 1642 } 1643 1644 private void draw_outline_for_paths (PathList pl, Context cr) { 1645 foreach (Path p in pl.paths) { 1646 p.draw_outline (cr); 1647 } 1648 } 1649 1650 private void draw_path_list (PathList pl, Context cr, Color? c = null) { 1651 foreach (Path p in pl.paths) { 1652 p.draw_path (cr, this, c); 1653 } 1654 } 1655 1656 private void draw_zoom_area(Context cr) { 1657 cr.save (); 1658 cr.set_line_width (2.0); 1659 Theme.color (cr, "Selection Border"); 1660 cr.rectangle (Math.fmin (zoom_x1, zoom_x2), Math.fmin (zoom_y1, zoom_y2), Math.fabs (zoom_x1 - zoom_x2), Math.fabs (zoom_y1 - zoom_y2)); 1661 cr.stroke (); 1662 cr.restore (); 1663 } 1664 1665 private void draw_background_color (Context cr, double opacity) { 1666 if (opacity > 0) { 1667 cr.save (); 1668 cr.rectangle (0, 0, allocation.width, allocation.height); 1669 Theme.color (cr, "Canvas Background"); 1670 cr.fill (); 1671 cr.restore (); 1672 } 1673 } 1674 1675 private void draw_help_lines (Context cr) { 1676 foreach (Line line in get_all_help_lines ()) { 1677 cr.save (); 1678 line.draw (cr, allocation); 1679 cr.restore (); 1680 } 1681 } 1682 1683 public void set_allocation (WidgetAllocation a) { 1684 allocation = a; 1685 } 1686 1687 public override void draw (WidgetAllocation allocation, Context cr) { 1688 Tool tool; 1689 1690 this.allocation = allocation; 1691 1692 ImageSurface ps = new ImageSurface (Format.ARGB32, allocation.width, allocation.height); 1693 Context cmp = new Context (ps); 1694 1695 cr.save (); 1696 draw_background_color (cr, 1); 1697 cr.restore (); 1698 1699 if (background_image != null && background_image_visible) { 1700 ((!)background_image).draw (cr, allocation, view_offset_x, view_offset_y, view_zoom); 1701 } 1702 1703 if (unlikely (Preferences.draw_boundaries)) { 1704 foreach (Path p in path_list) { 1705 p.draw_boundaries (cr); 1706 } 1707 } 1708 1709 draw_background_glyph (allocation, cr); 1710 juxtapose (allocation, cr); 1711 1712 if (BirdFont.show_coordinates) { 1713 draw_coordinate (cmp); 1714 } 1715 1716 if (show_help_lines) { 1717 cmp.save (); 1718 draw_help_lines (cmp); 1719 cmp.restore (); 1720 } 1721 1722 if (zoom_area_is_visible) { 1723 cmp.save (); 1724 draw_zoom_area (cmp); 1725 cmp.restore (); 1726 } 1727 1728 if (!is_empty ()) { 1729 cmp.save (); 1730 cmp.scale (view_zoom, view_zoom); 1731 cmp.translate (-view_offset_x, -view_offset_y); 1732 draw_path (cmp); 1733 cmp.restore (); 1734 } 1735 1736 cmp.save (); 1737 tool = MainWindow.get_toolbox ().get_current_tool (); 1738 tool.draw_action (tool, cmp, this); 1739 cmp.restore (); 1740 1741 cr.save (); 1742 cr.set_source_surface (ps, 0, 0); 1743 cr.paint (); 1744 cr.restore (); 1745 } 1746 1747 private void zoom_in_at_point (double x, double y) { 1748 int n = -10; 1749 zoom_at_point (x, y, n); 1750 } 1751 1752 private void zoom_out_at_point (double x, double y) { 1753 int n = (int) (10.0 * ((allocation.width - 10.0) / allocation.width)); 1754 zoom_at_point (x, y, n); 1755 } 1756 1757 public void zoom_tap (double distance) { 1758 int w = (int) (distance); 1759 if (distance != 0) { 1760 show_zoom_area (-w , -w, allocation.width + w, allocation.height + w); 1761 set_zoom_from_area (); 1762 } 1763 } 1764 1765 /** Zoom in @param n pixels. */ 1766 private void zoom_at_point (double x, double y, int n) { 1767 double w = allocation.width; 1768 double h = allocation.height; 1769 1770 double rx = Math.fabs (w / 2 - x) / (w / 2); 1771 double ry = Math.fabs (h / 2 - y) / (h / 2); 1772 1773 int xd = (x < w / 2) ? (int) (n * rx) : (int) (-n * rx); 1774 int yd = (y < h / 2) ? (int) (n * ry) : (int) (-n * ry); 1775 1776 show_zoom_area (-n + xd, -n + yd, allocation.width + n + xd, allocation.height + n + yd); 1777 set_zoom_from_area (); 1778 } 1779 1780 private void move_view_offset (double x, double y) { 1781 view_offset_x = move_offset_x + (pointer_begin_x - x) * (1/view_zoom); 1782 view_offset_y = move_offset_y + (pointer_begin_y - y) * (1/view_zoom); 1783 redraw_area (0, 0, allocation.width, allocation.height); 1784 } 1785 1786 public void store_undo_state (bool clear_redo = false) { 1787 Glyph g = copy (); 1788 undo_list.add (g); 1789 1790 if (clear_redo) { 1791 redo_list.clear (); 1792 } 1793 } 1794 1795 public void store_redo_state () { 1796 Glyph g = copy (); 1797 redo_list.add (g); 1798 } 1799 1800 public Glyph copy () { 1801 Glyph g = new Glyph.no_lines (name, unichar_code); 1802 1803 g.left_limit = left_limit; 1804 g.right_limit = right_limit; 1805 1806 g.remove_lines (); 1807 1808 foreach (Line line in get_all_help_lines ()) { 1809 g.add_line (line.copy ()); 1810 } 1811 1812 foreach (Path p in path_list) { 1813 g.add_path (p.copy ()); 1814 } 1815 1816 foreach (Path p in active_paths) { 1817 g.active_paths.add (p); 1818 } 1819 1820 if (background_image != null) { 1821 g.background_image = ((!) background_image).copy (); 1822 } 1823 1824 g.empty = empty; 1825 g.open = open; 1826 1827 return g; 1828 } 1829 1830 public void reload () { 1831 Font f = BirdFont.get_current_font (); 1832 1833 if (f.has_glyph (name)) { 1834 set_glyph_data ((!) f.get_glyph (name)); 1835 } 1836 } 1837 1838 public override void undo () { 1839 Glyph g; 1840 Tool tool; 1841 1842 if (undo_list.size == 0) { 1843 return; 1844 } 1845 1846 tool = MainWindow.get_toolbox ().get_current_tool (); 1847 tool.before_undo (); 1848 1849 g = undo_list.get (undo_list.size - 1); 1850 1851 store_redo_state (); 1852 set_glyph_data (g); 1853 1854 undo_list.remove_at (undo_list.size - 1); 1855 1856 PenTool.update_selected_points (); 1857 1858 clear_active_paths (); 1859 1860 tool.after_undo (); 1861 } 1862 1863 public override void redo () { 1864 Glyph g; 1865 1866 if (redo_list.size == 0) { 1867 return; 1868 } 1869 1870 g = redo_list.get (redo_list.size - 1); 1871 1872 store_undo_state (false); 1873 set_glyph_data (g); 1874 1875 redo_list.remove_at (redo_list.size - 1); 1876 1877 PenTool.update_selected_points (); 1878 1879 clear_active_paths (); 1880 } 1881 1882 void set_glyph_data (Glyph g) { 1883 path_list.clear (); 1884 1885 foreach (Path p in g.path_list) { 1886 add_path (p); 1887 p.update_region_boundaries (); 1888 } 1889 1890 remove_lines (); 1891 foreach (Line line in g.get_all_help_lines ()) { 1892 add_line (line.copy ()); 1893 } 1894 1895 add_help_lines (); 1896 1897 if (g.background_image != null) { 1898 background_image = ((!) g.background_image).copy (); 1899 } 1900 1901 clear_active_paths (); 1902 foreach (Path p in g.active_paths) { 1903 add_active_path (p); 1904 } 1905 1906 redraw_area (0, 0, allocation.width, allocation.height); 1907 } 1908 1909 /** Split curve in two parts and add a new point in between. 1910 * @return the new point 1911 */ 1912 public void insert_new_point_on_path (double x, double y) { 1913 double min, distance; 1914 Path? p = null; 1915 Path path; 1916 EditPoint? np = null; 1917 EditPoint lep; 1918 1919 double xt; 1920 double yt; 1921 1922 xt = x * ivz () - xc () + view_offset_x; 1923 yt = yc () - y * ivz () - view_offset_y; 1924 1925 min = double.MAX; 1926 1927 foreach (Path pp in path_list) { 1928 lep = new EditPoint (); 1929 pp.get_closest_point_on_path (lep, xt, yt); 1930 distance = Math.sqrt (Math.pow (Math.fabs (xt - lep.x), 2) + Math.pow (Math.fabs (yt - lep.y), 2)); 1931 1932 if (distance < min) { 1933 min = distance; 1934 p = pp; 1935 np = lep; 1936 } 1937 } 1938 1939 if (p == null) { 1940 return; 1941 } 1942 1943 path = (!) p; 1944 1945 lep = new EditPoint (); 1946 path.get_closest_point_on_path (lep, xt, yt); 1947 path.insert_new_point_on_path (lep); 1948 } 1949 1950 static bool in_range (double offset_x, double coordinate_x1, double coordinate_x2) { 1951 return coordinate_x1 <= offset_x <= coordinate_x2; 1952 } 1953 1954 public void juxtapose (WidgetAllocation allocation, Context cr) { 1955 string glyph_sequence = Preferences.get ("glyph_sequence"); 1956 unichar c; 1957 Font font = BirdFont.get_current_font (); 1958 Glyph glyph = MainWindow.get_current_glyph (); 1959 Glyph juxtaposed; 1960 StringBuilder current = new StringBuilder (); 1961 int pos; 1962 string name; 1963 double x, kern; 1964 double left, baseline; 1965 string last_name; 1966 1967 double box_x1, box_x2, box_y1, box_y2; 1968 double marker_x, marker_y; 1969 1970 KerningClasses classes = font.get_kerning_classes (); 1971 1972 x = 0; 1973 1974 box_x1 = path_coordinate_x (0); 1975 box_y1 = path_coordinate_y (0); 1976 box_x2 = path_coordinate_x (allocation.width); 1977 box_y2 = path_coordinate_y (allocation.height); 1978 1979 current.append_unichar (glyph.unichar_code); 1980 pos = glyph_sequence.index_of (current.str); 1981 1982 baseline = font.base_line;; 1983 left = glyph.get_line ("left").pos; 1984 1985 x = glyph.get_width (); 1986 last_name = glyph.name; 1987 for (int i = pos + 1; i < glyph_sequence.char_count (); i++) { 1988 c = glyph_sequence.get_char (i); 1989 name = font.get_name_for_character (c); 1990 juxtaposed = (font.has_glyph (name)) ? (!) font.get_glyph (name) : font.get_space ().get_current (); 1991 1992 if (font.has_glyph (last_name) && font.has_glyph (name)) { 1993 kern = classes.get_kerning (last_name, name); 1994 } else { 1995 kern = 0; 1996 } 1997 1998 if (!juxtaposed.is_empty () 1999 && (in_range (left + x + kern, box_x1, box_x2) // the letter is visible 2000 || in_range (left + x + kern + juxtaposed.get_width (), box_x1, box_x2))) { 2001 2002 marker_x = Glyph.xc () + left + x + kern - glyph.view_offset_x; 2003 marker_y = Glyph.yc () - baseline - glyph.view_offset_y; 2004 2005 cr.save (); 2006 cr.scale (glyph.view_zoom, glyph.view_zoom); 2007 Theme.color (cr, "Foreground 1"); 2008 2009 Svg.draw_svg_path (cr, juxtaposed.get_svg_data (), marker_x, marker_y); 2010 cr.restore (); 2011 } 2012 2013 x += juxtaposed.get_width () + kern; 2014 2015 last_name = name; 2016 } 2017 2018 x = 0; 2019 last_name = glyph.name; 2020 for (int i = pos - 1; i >= 0; i--) { 2021 c = glyph_sequence.get_char (i); 2022 name = font.get_name_for_character (c); 2023 juxtaposed = (font.has_glyph (name)) ? (!) font.get_glyph (name) : font.get_space ().get_current (); 2024 2025 if (font.has_glyph (last_name) && font.has_glyph (name)) { 2026 kern = classes.get_kerning (name, last_name); 2027 } else { 2028 kern = 0; 2029 } 2030 2031 x -= juxtaposed.get_width (); 2032 x -= kern; 2033 2034 marker_x = Glyph.xc () + left + x; 2035 marker_y = Glyph.yc () - baseline; 2036 if (!juxtaposed.is_empty () 2037 &&(in_range (left + x, box_x1, box_x2) 2038 || in_range (left + x + juxtaposed.get_width (), box_x1, box_x2))) { 2039 cr.save (); 2040 cr.scale (glyph.view_zoom, glyph.view_zoom); 2041 cr.translate (-glyph.view_offset_x, -glyph.view_offset_y); 2042 Theme.color (cr, "Foreground 1"); 2043 Svg.draw_svg_path (cr, juxtaposed.get_svg_data (), marker_x, marker_y); 2044 cr.restore (); 2045 } 2046 2047 last_name = name; 2048 } 2049 } 2050 2051 /** @return left side bearing */ 2052 public double get_lsb () { 2053 return get_line ("left").pos; 2054 } 2055 2056 /** @return bottom line */ 2057 public double get_baseline () { 2058 Font font = BirdFont.get_current_font (); 2059 return font.base_line; 2060 } 2061 2062 void draw_background_glyph (WidgetAllocation allocation, Context cr) { 2063 double left, baseline, current_left; 2064 Glyph g; 2065 Font font = BirdFont.get_current_font (); 2066 2067 current_left = get_line ("left").pos; 2068 2069 if (background_glyph != null) { 2070 g = (!) background_glyph; 2071 baseline = font.base_line; 2072 left = g.get_line ("left").pos; 2073 cr.save (); 2074 cr.scale (view_zoom, view_zoom); 2075 cr.translate (-view_offset_x, -view_offset_y); 2076 Theme.color (cr, "Background Glyph"); 2077 2078 Svg.draw_svg_path (cr, g.get_svg_data (), 2079 Glyph.xc () + left - (left - current_left) , 2080 Glyph.yc () - baseline); 2081 cr.restore (); 2082 } 2083 2084 } 2085 2086 public string get_hex () { 2087 return Font.to_hex_code (unichar_code); 2088 } 2089 2090 public override void move_view (double x, double y) { 2091 view_offset_x += x / view_zoom; 2092 view_offset_y += y / view_zoom; 2093 GlyphCanvas.redraw (); 2094 } 2095 2096 /** Scroll or zoom from tap events. */ 2097 public void change_view_event (int finger, int x, int y) { 2098 double dx, dy; 2099 double last_distance, new_distance; 2100 2101 dx = 0; 2102 dy = 0; 2103 2104 new_distance = 0; 2105 2106 if (last_tap0_y == -1 || last_tap0_x == -1 || last_tap1_y == -1 || last_tap1_x == -1) { 2107 return; 2108 } 2109 2110 if (finger == 0) { 2111 dx = last_tap0_x - x; 2112 dy = last_tap0_y - y; 2113 new_distance = Path.distance (last_tap1_x, x, last_tap1_y, y); 2114 } 2115 2116 if (finger == 1) { 2117 dx = last_tap1_x - x; 2118 dy = last_tap1_y - y; 2119 new_distance = Path.distance (last_tap0_x, x, last_tap0_y, y); 2120 } 2121 2122 last_distance = Path.distance (last_tap0_x, last_tap1_x, last_tap0_y, last_tap1_y); 2123 2124 if (zoom_distance != 0) { 2125 zoom_tap (zoom_distance - new_distance); 2126 } 2127 2128 if (finger == 1) { 2129 warning (@"dx $dx dy $dy last_tap1_x $last_tap1_x last_tap1_y $last_tap1_y x $x y $y"); 2130 move_view (dx, dy); 2131 } 2132 2133 zoom_distance = new_distance; 2134 } 2135 2136 public override void tap_down (int finger, int x, int y) { 2137 TimeoutSource delay; 2138 2139 if (finger == 0) { 2140 delay = new TimeoutSource (400); // wait for second finger 2141 delay.set_callback(() => { 2142 if (!change_view && !ignore_input) { 2143 button_press (1, x, y); 2144 } 2145 return false; 2146 }); 2147 delay.attach (null); 2148 2149 last_tap0_x = x; 2150 last_tap0_y = y; 2151 } 2152 2153 if (finger == 1) { 2154 change_view = true; 2155 last_tap1_x = x; 2156 last_tap1_y = y; 2157 } 2158 } 2159 2160 public override void tap_up (int finger, int x, int y) { 2161 if (finger == 0) { 2162 button_release (1, x, y); 2163 2164 last_tap0_x = -1; 2165 last_tap0_y = -1; 2166 } 2167 2168 if (finger == 1) { 2169 last_tap1_x = -1; 2170 last_tap1_y = -1; 2171 2172 change_view = false; 2173 zoom_distance = 0; 2174 } 2175 } 2176 2177 public override void tap_move (int finger, int x, int y) { 2178 if (!change_view) { 2179 motion_notify (x, y); 2180 } else { 2181 change_view_event (finger, x, y); 2182 } 2183 2184 if (finger == 0) { 2185 last_tap0_x = x; 2186 last_tap0_y = y; 2187 } 2188 2189 if (finger == 1) { 2190 last_tap1_x = x; 2191 last_tap1_y = y; 2192 } 2193 } 2194 2195 public void update_spacing_class () { 2196 Font font = BirdFont.get_current_font (); 2197 GlyphCollection? g; 2198 GlyphCollection gc; 2199 Glyph glyph; 2200 2201 foreach (string l in font.get_spacing () 2202 .get_all_connections ((!) unichar_code.to_string ())) { 2203 if (l != (!) unichar_code.to_string ()) { 2204 g = font.get_glyph_collection (l); 2205 if (g != null) { 2206 gc = (!) g; 2207 glyph = gc.get_current (); 2208 left_limit = glyph.left_limit; 2209 right_limit = glyph.right_limit; 2210 break; 2211 } 2212 } 2213 } 2214 2215 add_help_lines (); 2216 } 2217 2218 public void update_other_spacing_classes () { 2219 Font font = BirdFont.get_current_font (); 2220 GlyphCollection? g; 2221 GlyphCollection gc; 2222 Glyph glyph; 2223 2224 foreach (string l in font.get_spacing () 2225 .get_all_connections ((!) unichar_code.to_string ())) { 2226 if (l != (!) unichar_code.to_string ()) { 2227 g = font.get_glyph_collection (l); 2228 if (g != null) { 2229 gc = (!) g; 2230 glyph = gc.get_current (); 2231 glyph.left_limit = left_limit; 2232 glyph.right_limit = right_limit; 2233 // FIXME: DELETE glyph.add_help_lines (); 2234 } 2235 } 2236 } 2237 } 2238 2239 public void set_cache (string key, Surface cache) { 2240 glyph_cache.set (key, cache); 2241 } 2242 2243 public bool has_cache (string key) { 2244 return glyph_cache.has_key (key); 2245 } 2246 2247 public Surface get_cache (string key) { 2248 if (unlikely (!has_cache (key))) { 2249 warning ("No cache for glyph."); 2250 return new ImageSurface (Cairo.Format.ARGB32, 1, 1); 2251 } 2252 2253 return glyph_cache.get (key); 2254 } 2255 2256 public void add_custom_guide () { 2257 TextListener listener; 2258 2259 listener = new TextListener (t_("Guide"), "", t_("Add")); 2260 2261 listener.signal_text_input.connect ((text) => { 2262 new_guide_name = text; 2263 }); 2264 2265 listener.signal_submit.connect (() => { 2266 Line guide; 2267 double position; 2268 2269 position = path_coordinate_y (allocation.height / 2.0); 2270 guide = new Line (new_guide_name, position); 2271 horizontal_help_lines.add (guide); 2272 2273 BirdFont.get_current_font ().custom_guides.add (guide); 2274 2275 TabContent.hide_text_input (); 2276 GlyphCanvas.redraw (); 2277 }); 2278 2279 TabContent.show_text_input (listener); 2280 } 2281 } 2282 2283 } 2284