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