sketchingpy.sketch2dapp

Pygame-based renderer for Sketchingpy.

License:

BSD

   1"""Pygame-based renderer for Sketchingpy.
   2
   3License:
   4    BSD
   5"""
   6
   7import contextlib
   8import copy
   9import math
  10import typing
  11
  12import PIL.Image
  13import PIL.ImageFont
  14
  15with contextlib.redirect_stdout(None):
  16    import pygame
  17    import pygame.draw
  18    import pygame.image
  19    import pygame.key
  20    import pygame.locals
  21    import pygame.mouse
  22    import pygame.time
  23
  24
  25ui_available = False
  26try:
  27    import pygame_gui  # type: ignore
  28    import pygame_gui.windows  # type: ignore
  29    import sketchingpy.pygame_prompt  # type: ignore
  30    ui_available = True
  31except:
  32    pass
  33
  34import sketchingpy.abstracted
  35import sketchingpy.const
  36import sketchingpy.control_struct
  37import sketchingpy.data_struct
  38import sketchingpy.local_data_struct
  39import sketchingpy.pillow_util
  40import sketchingpy.sketch2d_keymap
  41import sketchingpy.state_struct
  42import sketchingpy.transform
  43
  44DEFAULT_FPS = 20
  45MANUAL_OFFSET = True
  46OPTIONAL_SKETCH_CALLBACK = typing.Optional[typing.Callable[[sketchingpy.abstracted.Sketch], None]]
  47
  48
  49class Sketch2DApp(sketchingpy.abstracted.Sketch):
  50    """Create a new Pygame-based Sketch."""
  51
  52    def __init__(self, width: int, height: int, title: typing.Optional[str] = None,
  53        loading_src: typing.Optional[str] = None):
  54        """Create a enw Pygame-based sketch.
  55
  56        Args:
  57            width: The width of the sketch in pixels. This will be used for window width.
  58            height: The height of the sketch in pixels. This will be used for window height.
  59            title: Starting title for the application.
  60            loading_src: ID for loading screen. Ignored, reserved for future use.
  61        """
  62        super().__init__()
  63
  64        # System params
  65        self._width = width
  66        self._height = height
  67
  68        # Callbacks
  69        self._callback_step: OPTIONAL_SKETCH_CALLBACK = None
  70        self._callback_quit: OPTIONAL_SKETCH_CALLBACK = None
  71
  72        # User configurable state
  73        self._state_frame_rate = DEFAULT_FPS
  74
  75        # Buffers
  76        self._internal_surface = None
  77        self._output_surface = None
  78        self._buffers: typing.Dict[str, pygame.Surface] = {}
  79
  80        # Internal state
  81        self._internal_pre_show_actions: typing.List[typing.Callable] = []
  82        self._internal_quit_requested = False
  83        self._internal_clock = pygame.time.Clock()
  84        self._transformer = sketchingpy.transform.Transformer()
  85        self._transformer_stack: typing.List[sketchingpy.transform.Transformer] = []
  86        self._dialog_layer: typing.Optional['AppDialogLayer'] = None
  87
  88        # Inputs
  89        self._mouse = PygameMouse()
  90        self._keyboard = PygameKeyboard()
  91
  92        # Internal struct
  93        self._struct_event_handlers = {
  94            pygame.KEYDOWN: lambda x: self._process_key_down(x),
  95            pygame.KEYUP: lambda x: self._process_key_up(x),
  96            pygame.MOUSEBUTTONDOWN: lambda x: self._process_mouse_down(x),
  97            pygame.MOUSEBUTTONUP: lambda x: self._process_mouse_up(x),
  98            pygame.locals.QUIT: lambda x: self._process_quit(x)
  99        }
 100
 101        # Default window properties
 102        self.set_title('Sketchingpy Sketch' if title is None else title)
 103
 104    ##########
 105    # Buffer #
 106    ##########
 107
 108    def create_buffer(self, name: str, width: int, height: int,
 109        background: typing.Optional[str] = None):
 110        def execute():
 111            has_alpha = self._get_is_color_transparent(background)
 112            self._buffers[name] = self._make_shape_surface(
 113                pygame.Rect(0, 0, width, height),
 114                0,
 115                has_alpha=has_alpha
 116            )
 117            if not has_alpha:
 118                self._buffers[name].fill(pygame.Color(background))
 119
 120        if self._internal_surface is None:
 121            self._internal_pre_show_actions.append(execute)
 122        else:
 123            execute()
 124
 125    def enter_buffer(self, name: str):
 126        def execute():
 127            self._internal_surface = self._buffers[name]
 128
 129        if self._internal_surface is None:
 130            self._internal_pre_show_actions.append(execute)
 131        else:
 132            execute()
 133
 134    def exit_buffer(self):
 135        def execute():
 136            self._internal_surface = self._output_surface
 137
 138        if self._internal_surface is None:
 139            self._internal_pre_show_actions.append(execute)
 140        else:
 141            execute()
 142
 143    def draw_buffer(self, x: float, y: float, name: str):
 144        def execute():
 145            target_surface = self._buffers[name]
 146
 147            original_rect = target_surface.get_rect()
 148            rect = pygame.Rect(
 149                original_rect.x,
 150                original_rect.y,
 151                original_rect.width,
 152                original_rect.height
 153            )
 154            rect.left = x
 155            rect.top = y
 156
 157            self._blit_with_transform(
 158                target_surface,
 159                rect.centerx,
 160                rect.centery,
 161                self._transformer.quick_copy()
 162            )
 163
 164        if self._internal_surface is None:
 165            self._internal_pre_show_actions.append(execute)
 166        else:
 167            execute()
 168
 169    ############
 170    # Controls #
 171    ############
 172
 173    def get_keyboard(self) -> typing.Optional[sketchingpy.control_struct.Keyboard]:
 174        return self._keyboard
 175
 176    def get_mouse(self) -> typing.Optional[sketchingpy.control_struct.Mouse]:
 177        return self._mouse
 178
 179    ########
 180    # Data #
 181    ########
 182
 183    def get_data_layer(self) -> typing.Optional[sketchingpy.data_struct.DataLayer]:
 184        return sketchingpy.local_data_struct.LocalDataLayer()
 185
 186    ###########
 187    # Dialogs #
 188    ###########
 189
 190    def get_dialog_layer(self) -> typing.Optional[sketchingpy.dialog_struct.DialogLayer]:
 191        if not ui_available:
 192            return None
 193
 194        if self._dialog_layer is None:
 195            self._dialog_layer = AppDialogLayer(self)
 196
 197        return self._dialog_layer
 198
 199    ###########
 200    # Drawing #
 201    ###########
 202
 203    def clear(self, color_hex: str):
 204        if self._internal_surface is None:
 205            self._internal_pre_show_actions.append(lambda: self.clear(color_hex))
 206            return
 207
 208        self._internal_surface.fill(pygame.Color(color_hex))
 209
 210    def draw_arc(self, x1: float, y1: float, x2: float, y2: float, a1: float,
 211        a2: float):
 212        state_machine = self._get_current_state_machine()
 213
 214        stroke_enabled = state_machine.get_stroke_enabled()
 215        fill_enabled = state_machine.get_fill_enabled()
 216        stroke_native = state_machine.get_stroke_native()
 217        fill_native = state_machine.get_fill_native()
 218        stroke_weight = state_machine.get_stroke_weight()
 219
 220        mode_native = state_machine.get_arc_mode_native()
 221        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
 222
 223        a1_rad = self._convert_to_radians(a1)
 224        a2_rad = self._convert_to_radians(a2)
 225
 226        transformer = self._transformer.quick_copy()
 227
 228        def execute_draw():
 229            pillow_util_image = sketchingpy.pillow_util.make_arc_image(
 230                rect.x,
 231                rect.y,
 232                rect.w,
 233                rect.h,
 234                a1_rad,
 235                a2_rad,
 236                stroke_enabled,
 237                fill_enabled,
 238                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
 239                self._to_pillow_rgba(fill_native) if fill_enabled else None,
 240                stroke_weight
 241            )
 242
 243            native_image = self._convert_pillow_image(pillow_util_image.get_image())
 244
 245            self._blit_with_transform(
 246                native_image,
 247                rect.centerx,
 248                rect.centery,
 249                transformer
 250            )
 251
 252        if self._internal_surface is None:
 253            self._internal_pre_show_actions.append(execute_draw)
 254        else:
 255            execute_draw()
 256
 257    def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
 258        state_machine = self._get_current_state_machine()
 259
 260        stroke_enabled = state_machine.get_stroke_enabled()
 261        fill_enabled = state_machine.get_fill_enabled()
 262        stroke_native = state_machine.get_stroke_native()
 263        fill_native = state_machine.get_fill_native()
 264        stroke_weight = state_machine.get_stroke_weight()
 265
 266        mode_native = state_machine.get_ellipse_mode_native()
 267        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
 268
 269        transformer = self._transformer.quick_copy()
 270
 271        def execute_draw():
 272            pillow_util_image = sketchingpy.pillow_util.make_ellipse_image(
 273                rect.x,
 274                rect.y,
 275                rect.w,
 276                rect.h,
 277                stroke_enabled,
 278                fill_enabled,
 279                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
 280                self._to_pillow_rgba(fill_native) if fill_enabled else None,
 281                stroke_weight
 282            )
 283
 284            native_image = self._convert_pillow_image(pillow_util_image.get_image())
 285
 286            self._blit_with_transform(
 287                native_image,
 288                rect.centerx,
 289                rect.centery,
 290                transformer
 291            )
 292
 293        if self._internal_surface is None:
 294            self._internal_pre_show_actions.append(execute_draw)
 295        else:
 296            execute_draw()
 297
 298    def draw_line(self, x1: float, y1: float, x2: float, y2: float):
 299        state_machine = self._get_current_state_machine()
 300        if not state_machine.get_stroke_enabled():
 301            return
 302
 303        stroke_color = state_machine.get_stroke_native()
 304        stroke_weight = state_machine.get_stroke_weight_native()
 305
 306        transformer = self._transformer.quick_copy()
 307
 308        def execute_draw():
 309            min_x = min([x1, x2])
 310            max_x = max([x1, x2])
 311            width = max_x - min_x + 2 * stroke_weight
 312
 313            min_y = min([y1, y2])
 314            max_y = max([y1, y2])
 315            height = max_y - min_y + 2 * stroke_weight
 316
 317            rect = pygame.Rect(0, 0, width, height)
 318            target_surface = self._make_shape_surface(rect, stroke_weight)
 319
 320            def adjust(target):
 321                return (
 322                    target[0] - min_x + stroke_weight - 1,
 323                    target[1] - min_y + stroke_weight - 1,
 324                )
 325
 326            pygame.draw.line(
 327                target_surface,
 328                stroke_color,
 329                adjust((x1, y1)),
 330                adjust((x2, y2)),
 331                width=stroke_weight
 332            )
 333
 334            center_x = (max_x + min_x) / 2
 335            center_y = (max_y + min_y) / 2
 336            self._blit_with_transform(target_surface, center_x, center_y, transformer)
 337
 338        if self._internal_surface is None:
 339            self._internal_pre_show_actions.append(execute_draw)
 340        else:
 341            execute_draw()
 342
 343    def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
 344        state_machine = self._get_current_state_machine()
 345
 346        stroke_enabled = state_machine.get_stroke_enabled()
 347        fill_enabled = state_machine.get_fill_enabled()
 348        stroke_native = state_machine.get_stroke_native()
 349        fill_native = state_machine.get_fill_native()
 350        stroke_weight = state_machine.get_stroke_weight()
 351
 352        mode_native = state_machine.get_rect_mode_native()
 353        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
 354
 355        transformer = self._transformer.quick_copy()
 356
 357        def execute_draw():
 358            pillow_util_image = sketchingpy.pillow_util.make_rect_image(
 359                rect.x,
 360                rect.y,
 361                rect.w,
 362                rect.h,
 363                stroke_enabled,
 364                fill_enabled,
 365                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
 366                self._to_pillow_rgba(fill_native) if fill_enabled else None,
 367                stroke_weight
 368            )
 369
 370            native_image = self._convert_pillow_image(pillow_util_image.get_image())
 371
 372            self._blit_with_transform(
 373                native_image,
 374                rect.centerx,
 375                rect.centery,
 376                transformer
 377            )
 378
 379        if self._internal_surface is None:
 380            self._internal_pre_show_actions.append(execute_draw)
 381        else:
 382            execute_draw()
 383
 384    def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
 385        if not shape.get_is_finished():
 386            raise RuntimeError('Finish your shape before drawing.')
 387
 388        state_machine = self._get_current_state_machine()
 389
 390        stroke_enabled = state_machine.get_stroke_enabled()
 391        fill_enabled = state_machine.get_fill_enabled()
 392        stroke_native = state_machine.get_stroke_native()
 393        fill_native = state_machine.get_fill_native()
 394        stroke_weight = state_machine.get_stroke_weight()
 395
 396        transformer = self._transformer.quick_copy()
 397
 398        def execute_draw():
 399            pillow_util_image = sketchingpy.pillow_util.make_shape_image(
 400                shape,
 401                stroke_enabled,
 402                fill_enabled,
 403                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
 404                self._to_pillow_rgba(fill_native) if fill_enabled else None,
 405                stroke_weight
 406            )
 407
 408            native_image = self._convert_pillow_image(pillow_util_image.get_image())
 409
 410            min_x = shape.get_min_x()
 411            max_x = shape.get_max_x()
 412            center_x = (max_x + min_x) / 2
 413
 414            min_y = shape.get_min_y()
 415            max_y = shape.get_max_y()
 416            center_y = (max_y + min_y) / 2
 417
 418            self._blit_with_transform(
 419                native_image,
 420                center_x,
 421                center_y,
 422                transformer
 423            )
 424
 425        if self._internal_surface is None:
 426            self._internal_pre_show_actions.append(execute_draw)
 427        else:
 428            execute_draw()
 429
 430    def draw_text(self, x: float, y: float, content: str):
 431        content = str(content)
 432        state_machine = self._get_current_state_machine()
 433
 434        stroke_enabled = state_machine.get_stroke_enabled()
 435        fill_enabled = state_machine.get_fill_enabled()
 436        stroke_native = state_machine.get_stroke_native()
 437        fill_native = state_machine.get_fill_native()
 438        stroke_weight = state_machine.get_stroke_weight()
 439
 440        text_font = state_machine.get_text_font_native()
 441        fill_pillow = self._to_pillow_rgba(fill_native)
 442        stroke_pillow = self._to_pillow_rgba(stroke_native)
 443
 444        align_info = state_machine.get_text_align_native()
 445        anchor_str = align_info.get_horizontal_align() + align_info.get_vertical_align()
 446
 447        transformer = self._transformer.quick_copy()
 448
 449        def execute_draw():
 450            pillow_util_image = sketchingpy.pillow_util.make_text_image(
 451                x,
 452                y,
 453                content,
 454                text_font,
 455                stroke_enabled,
 456                fill_enabled,
 457                stroke_pillow,
 458                fill_pillow,
 459                stroke_weight,
 460                anchor_str
 461            )
 462
 463            native_image = self._convert_pillow_image(pillow_util_image.get_image())
 464
 465            self._blit_with_transform(
 466                native_image,
 467                pillow_util_image.get_x() + pillow_util_image.get_width() / 2,
 468                pillow_util_image.get_y() + pillow_util_image.get_height() / 2,
 469                transformer
 470            )
 471
 472        if self._internal_surface is None:
 473            self._internal_pre_show_actions.append(execute_draw)
 474        else:
 475            execute_draw()
 476
 477    ##########
 478    # Events #
 479    ##########
 480
 481    def on_step(self, callback: sketchingpy.abstracted.StepCallback):
 482        self._callback_step = callback
 483
 484    def on_quit(self, callback: sketchingpy.abstracted.QuitCallback):
 485        self._callback_quit = callback
 486
 487    #########
 488    # Image #
 489    #########
 490
 491    def get_image(self, src: str) -> sketchingpy.abstracted.Image:
 492        return PygameImage(src)
 493
 494    def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
 495        if not image.get_is_loaded():
 496            return
 497
 498        transformer = self._transformer.quick_copy()
 499
 500        image_mode_native = self._get_current_state_machine().get_image_mode_native()
 501
 502        def execute_draw():
 503            rect = self._build_rect_with_mode(
 504                x,
 505                y,
 506                image.get_width(),
 507                image.get_height(),
 508                image_mode_native
 509            )
 510
 511            surface = image.get_native()
 512            self._blit_with_transform(surface, rect.centerx, rect.centery, transformer)
 513
 514        if self._internal_surface is None:
 515            self._internal_pre_show_actions.append(execute_draw)
 516        else:
 517            execute_draw()
 518
 519    def save_image(self, path: str):
 520        def execute_save():
 521            pygame.image.save(self._internal_surface, path)
 522
 523        if self._internal_surface is None:
 524            self._internal_pre_show_actions.append(execute_save)
 525            self.show_and_quit()
 526        else:
 527            execute_save()
 528
 529    #########
 530    # State #
 531    #########
 532
 533    def push_transform(self):
 534        self._transformer_stack.append(copy.deepcopy(self._transformer))
 535
 536    def pop_transform(self):
 537        if len(self._transformer_stack) == 0:
 538            raise RuntimeError('Transformation stack empty.')
 539
 540        self._transformer = self._transformer_stack.pop()
 541
 542    ##########
 543    # System #
 544    ##########
 545
 546    def get_native(self):
 547        if self._internal_surface is None:
 548            raise RuntimeError('Need to show sketch first before surface is available.')
 549
 550        return self._internal_surface
 551
 552    def set_fps(self, rate: int):
 553        self._state_frame_rate = rate
 554
 555    def set_title(self, title: str):
 556        def execute():
 557            pygame.display.set_caption(title)
 558
 559        if self._internal_surface is None:
 560            self._internal_pre_show_actions.append(execute)
 561        else:
 562            execute()
 563
 564    def quit(self):
 565        self._internal_quit_requested = True
 566
 567    def show(self, ax=None):
 568        self._show_internal(ax=ax, quit_immediately=False)
 569
 570    def show_and_quit(self, ax=None):
 571        self._show_internal(ax=ax, quit_immediately=True)
 572
 573    #############
 574    # Transform #
 575    #############
 576
 577    def translate(self, x: float, y: float):
 578        self._transformer.translate(x, y)
 579
 580    def rotate(self, angle_mirror: float):
 581        angle = -1 * angle_mirror
 582        angle_rad = self._convert_to_radians(angle)
 583        self._transformer.rotate(angle_rad)
 584
 585    def scale(self, scale: float):
 586        self._transformer.scale(scale)
 587
 588    ###########
 589    # Support #
 590    ###########
 591
 592    def _get_window_size(self) -> typing.Tuple[int, int]:
 593        return (self._width, self._height)
 594
 595    def _show_internal(self, ax=None, quit_immediately=False):
 596        self._snapshot_time()
 597        pygame.init()
 598        self._internal_surface = pygame.display.set_mode((self._width, self._height))
 599        self._output_surface = self._internal_surface
 600
 601        for action in self._internal_pre_show_actions:
 602            action()
 603
 604        self._inner_loop(quit_immediately=quit_immediately)
 605
 606    def _inner_loop(self, quit_immediately=False):
 607        clock = pygame.time.Clock()
 608
 609        while not self._internal_quit_requested:
 610            time_delta = clock.tick(60) / 1000.0
 611
 612            for event in pygame.event.get():
 613                self._process_event(event)
 614                if self._dialog_layer:
 615                    self._dialog_layer.get_manager().process_events(event)
 616                    dialog = self._dialog_layer.get_dialog()
 617                    try:
 618                        if dialog is not None and event.ui_element == dialog:
 619                            self._dialog_layer.report_close(event)
 620                    except AttributeError:
 621                        pass
 622
 623            if self._dialog_layer:
 624                self._dialog_layer.get_manager().update(time_delta)
 625
 626            if self._callback_step is not None:
 627                self._callback_step(self)
 628
 629            if self._dialog_layer:
 630                self._dialog_layer.get_manager().draw_ui(self._internal_surface)
 631
 632            pygame.display.update()
 633            self._internal_clock.tick(self._state_frame_rate)
 634
 635            if quit_immediately:
 636                self._internal_quit_requested = True
 637
 638        if self._callback_quit is not None:
 639            self._callback_quit(self)
 640
 641    def _process_event(self, event):
 642        if event.type not in self._struct_event_handlers:
 643            return
 644
 645        self._struct_event_handlers[event.type](event)
 646
 647    def _process_quit(self, event):
 648        self._internal_quit_requested = True
 649
 650    def _process_mouse_down(self, event):
 651        self._mouse.report_mouse_down(event)
 652
 653    def _process_mouse_up(self, event):
 654        self._mouse.report_mouse_up(event)
 655
 656    def _process_key_down(self, event):
 657        self._keyboard.report_key_down(event)
 658
 659    def _process_key_up(self, event):
 660        self._keyboard.report_key_up(event)
 661
 662    def _create_state_machine(self) -> sketchingpy.state_struct.SketchStateMachine:
 663        return PygameSketchStateMachine()
 664
 665    def _make_shape_surface(self, rect: pygame.Rect, stroke_weight: float,
 666        has_alpha: bool = True) -> pygame.Surface:
 667        if has_alpha:
 668            return pygame.Surface((rect.w + stroke_weight, rect.h + stroke_weight), pygame.SRCALPHA)
 669        else:
 670            return pygame.Surface((rect.w + stroke_weight, rect.h + stroke_weight))
 671
 672    def _zero_rect(self, rect: pygame.Rect) -> pygame.Rect:
 673        return pygame.Rect(0, 0, rect.w, rect.h)
 674
 675    def _build_rect_with_mode(self, x1: float, y1: float, x2: float, y2: float,
 676        native_mode: int) -> pygame.Rect:
 677        if native_mode == sketchingpy.const.CENTER:
 678            start_x = x1 - math.floor(x2 / 2)
 679            start_y = y1 - math.floor(y2 / 2)
 680            width = x2
 681            height = y2
 682        elif native_mode == sketchingpy.const.RADIUS:
 683            start_x = x1 - x2
 684            start_y = y1 - y2
 685            width = x2 * 2
 686            height = y2 * 2
 687        elif native_mode == sketchingpy.const.CORNER:
 688            start_x = x1
 689            start_y = y1
 690            width = x2
 691            height = y2
 692        elif native_mode == sketchingpy.const.CORNERS:
 693            (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
 694            start_x = x1
 695            start_y = y1
 696            width = x2 - x1
 697            height = y2 - y1
 698        else:
 699            raise RuntimeError('Unknown mode: ' + str(native_mode))
 700
 701        return pygame.Rect(start_x, start_y, width, height)
 702
 703    def _draw_primitive(self, x1: float, y1: float, x2: float, y2: float,
 704        mode: str, native_mode, draw_method):
 705        state_machine = self._get_current_state_machine()
 706        has_fill = state_machine.get_fill_enabled()
 707        fill_color = state_machine.get_fill_native()
 708        has_stroke = state_machine.get_stroke_enabled()
 709        stroke_color = state_machine.get_stroke_native()
 710        rect = self._build_rect_with_mode(x1, y1, x2, y2, native_mode)
 711        stroke_weight = state_machine.get_stroke_weight_native()
 712
 713        transformer = self._transformer.quick_copy()
 714
 715        def execute_draw_piece(color, strategy):
 716            target_surface = self._make_shape_surface(rect, stroke_weight)
 717            rect_adj = self._zero_rect(rect)
 718
 719            strategy(target_surface, rect_adj)
 720
 721            self._blit_with_transform(
 722                target_surface,
 723                rect.centerx,
 724                rect.centery,
 725                transformer
 726            )
 727
 728        def execute_draw():
 729            if has_fill:
 730                execute_draw_piece(
 731                    fill_color,
 732                    lambda surface, rect: draw_method(
 733                        surface,
 734                        fill_color,
 735                        self._offset_fill_weight(rect, stroke_weight),
 736                        0
 737                    )
 738                )
 739
 740            if has_stroke:
 741                execute_draw_piece(
 742                    stroke_color,
 743                    lambda surface, rect: draw_method(
 744                        surface,
 745                        stroke_color,
 746                        self._offset_stroke_weight(rect, stroke_weight),
 747                        stroke_weight
 748                    )
 749                )
 750
 751        if self._internal_surface is None:
 752            self._internal_pre_show_actions.append(execute_draw)
 753            return
 754        else:
 755            execute_draw()
 756
 757    def _to_pillow_rgba(self, target: pygame.Color):
 758        return (target.r, target.g, target.b, target.a)
 759
 760    def _convert_pillow_image(self, target: PIL.Image.Image) -> pygame.Surface:
 761        return pygame.image.fromstring(
 762            target.tobytes(),
 763            target.size,
 764            target.mode  # type: ignore
 765        ).convert_alpha()
 766
 767    def _blit_with_transform(self, surface: pygame.Surface, x: float, y: float,
 768        transformer: sketchingpy.transform.Transformer):
 769        start_rect = surface.get_rect()
 770        start_rect.centerx = x  # type: ignore
 771        start_rect.centery = y  # type: ignore
 772
 773        transformed_center = transformer.transform(
 774            start_rect.centerx,
 775            start_rect.centery
 776        )
 777
 778        has_scale = transformed_center.get_scale() != 1
 779        has_rotation = transformed_center.get_rotation() != 0
 780        has_content_transform = has_scale or has_rotation
 781        if has_content_transform:
 782            angle = transformed_center.get_rotation()
 783            angle_transform = math.degrees(angle)
 784            scale = transformed_center.get_scale()
 785            surface = pygame.transform.rotozoom(surface, angle_transform, scale)
 786            end_rect = surface.get_rect()
 787        else:
 788            end_rect = start_rect
 789
 790        end_rect.centerx = transformed_center.get_x()  # type: ignore
 791        end_rect.centery = transformed_center.get_y()  # type: ignore
 792
 793        assert self._internal_surface is not None
 794        self._internal_surface.blit(surface, (end_rect.x, end_rect.y))
 795
 796
 797class PygameSketchStateMachine(sketchingpy.state_struct.SketchStateMachine):
 798    """Implementation of SketchStateMachine for Pygame types."""
 799
 800    def __init__(self):
 801        """Create a new state machine for Pygame-based sketches."""
 802        super().__init__()
 803        self._fill_native = pygame.Color(super().get_fill())
 804        self._stroke_native = pygame.Color(super().get_stroke())
 805        self._font_cache = {}
 806        self._text_align_native = self._transform_text_align(super().get_text_align_native())
 807
 808    def set_fill(self, fill: str):
 809        super().set_fill(fill)
 810        self._fill_native = pygame.Color(super().get_fill())
 811
 812    def get_fill_native(self):
 813        return self._fill_native
 814
 815    def set_stroke(self, stroke: str):
 816        super().set_stroke(stroke)
 817        self._stroke_native = pygame.Color(super().get_stroke())
 818
 819    def get_stroke_native(self):
 820        return self._stroke_native
 821
 822    def get_text_font_native(self):
 823        font = self.get_text_font()
 824        key = '%s.%d' % (font.get_identifier(), font.get_size())
 825
 826        if key not in self._font_cache:
 827            new_font = PIL.ImageFont.truetype(font.get_identifier(), font.get_size())
 828            self._font_cache[key] = new_font
 829
 830        return self._font_cache[key]
 831
 832    def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
 833        super().set_text_align(text_align)
 834        self._text_align_native = self._transform_text_align(super().get_text_align_native())
 835
 836    def get_text_align_native(self):
 837        return self._text_align_native
 838
 839    def _transform_text_align(self,
 840        text_align: sketchingpy.state_struct.TextAlign) -> sketchingpy.state_struct.TextAlign:
 841
 842        HORIZONTAL_ALIGNS = {
 843            sketchingpy.const.LEFT: 'l',
 844            sketchingpy.const.CENTER: 'm',
 845            sketchingpy.const.RIGHT: 'r'
 846        }
 847
 848        VERTICAL_ALIGNS = {
 849            sketchingpy.const.TOP: 't',
 850            sketchingpy.const.CENTER: 'm',
 851            sketchingpy.const.BASELINE: 's',
 852            sketchingpy.const.BOTTOM: 'b'
 853        }
 854
 855        return sketchingpy.state_struct.TextAlign(
 856            HORIZONTAL_ALIGNS[text_align.get_horizontal_align()],
 857            VERTICAL_ALIGNS[text_align.get_vertical_align()]
 858        )
 859
 860
 861class PygameImage(sketchingpy.abstracted.Image):
 862    """Strategy implementation for Pygame images."""
 863
 864    def __init__(self, src: str):
 865        """Create a new image.
 866
 867        Args:
 868            src: Path to the image.
 869        """
 870        super().__init__(src)
 871        self._native = pygame.image.load(self.get_src())
 872        self._converted = False
 873
 874    def get_width(self) -> float:
 875        return self._native.get_rect().width
 876
 877    def get_height(self) -> float:
 878        return self._native.get_rect().height
 879
 880    def resize(self, width: float, height: float):
 881        self._native = pygame.transform.scale(self._native, (width, height))
 882
 883    def get_native(self):
 884        if not self._converted:
 885            self._native.convert_alpha()
 886
 887        return self._native
 888
 889    def get_is_loaded(self):
 890        return True
 891
 892
 893class PygameMouse(sketchingpy.control_struct.Mouse):
 894    """Strategy implementation for Pygame-based mouse access."""
 895
 896    def __init__(self):
 897        """Create a new mouse strategy using Pygame."""
 898        super().__init__()
 899        self._press_callback = None
 900        self._release_callback = None
 901
 902    def get_pointer_x(self):
 903        return pygame.mouse.get_pos()[0]
 904
 905    def get_pointer_y(self):
 906        return pygame.mouse.get_pos()[1]
 907
 908    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
 909        is_left_pressed = pygame.mouse.get_pressed()[0]
 910        is_right_pressed = pygame.mouse.get_pressed()[2]
 911        buttons_clicked = []
 912
 913        if is_left_pressed:
 914            buttons_clicked.append(sketchingpy.const.MOUSE_LEFT_BUTTON)
 915
 916        if is_right_pressed:
 917            buttons_clicked.append(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 918
 919        return map(lambda x: sketchingpy.control_struct.Button(x), buttons_clicked)
 920
 921    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
 922        self._press_callback = callback
 923
 924    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
 925        self._release_callback = callback
 926
 927    def report_mouse_down(self, event):
 928        if self._press_callback is None:
 929            return
 930
 931        if event.button == 1:
 932            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
 933            self._press_callback(button)
 934        elif event.button == 3:
 935            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 936            self._press_callback(button)
 937
 938    def report_mouse_up(self, event):
 939        if self._release_callback is None:
 940            return
 941
 942        if event.button == 1:
 943            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
 944            self._release_callback(button)
 945        elif event.button == 3:
 946            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 947            self._release_callback(button)
 948
 949
 950class PygameKeyboard(sketchingpy.control_struct.Keyboard):
 951    """Strategy implementation for Pygame-based keyboard access."""
 952
 953    def __init__(self):
 954        """Create a new keyboard strategy using Pygame."""
 955        super().__init__()
 956        self._pressed = set()
 957        self._press_callback = None
 958        self._release_callback = None
 959
 960    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
 961        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)
 962
 963    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
 964        self._press_callback = callback
 965
 966    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
 967        self._release_callback = callback
 968
 969    def report_key_down(self, event):
 970        mapped = sketchingpy.sketch2d_keymap.KEY_MAP.get(event.key, None)
 971
 972        if mapped is None:
 973            return
 974
 975        self._pressed.add(mapped)
 976
 977        if self._press_callback is not None:
 978            button = sketchingpy.control_struct.Button(mapped)
 979            self._press_callback(button)
 980
 981    def report_key_up(self, event):
 982        mapped = sketchingpy.sketch2d_keymap.KEY_MAP.get(event.key, None)
 983
 984        if mapped is None:
 985            return
 986
 987        self._pressed.remove(mapped)
 988
 989        if self._release_callback is not None:
 990            button = sketchingpy.control_struct.Button(mapped)
 991            self._release_callback(button)
 992
 993
 994class AppDialogLayer(sketchingpy.dialog_struct.DialogLayer):
 995    """Dialog / simple UI layer for local apps."""
 996
 997    def __init__(self, sketch: Sketch2DApp):
 998        """"Initialize tkinter but hide the root window."""
 999        self._sketch = sketch
1000        self._sketch_size = self._sketch._get_window_size()
1001        self._manager = pygame_gui.UIManager(self._sketch_size)
1002        self._callback = None  # type: ignore
1003        self._dialog = None  # type: ignore
1004
1005    def get_manager(self):
1006        return self._manager
1007
1008    def get_dialog(self):
1009        return self._dialog
1010
1011    def report_close(self, event):
1012        if self._callback:
1013            self._callback(event)
1014
1015    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1016        self._dispose_dialog()
1017        self._set_dialog(pygame_gui.windows.UIMessageWindow(
1018            rect=pygame.Rect(
1019                self._sketch_size[0] / 2 - 150,
1020                self._sketch_size[1] / 2 - 150,
1021                300,
1022                300
1023            ),
1024            html_message=message,
1025            manager=self._manager
1026        ))
1027
1028        def outer_callback(event):
1029            if event.type == pygame_gui._constants.UI_BUTTON_PRESSED and callback:
1030                callback()
1031
1032        self._callback = outer_callback  # type: ignore
1033
1034    def show_prompt(self, message: str,
1035        callback: typing.Optional[typing.Callable[[str], None]] = None):
1036        self._set_dialog(sketchingpy.pygame_prompt.PygameGuiPrompt(  # type: ignore
1037            rect=pygame.Rect(
1038                self._sketch_size[0] / 2 - 150,
1039                self._sketch_size[1] / 2 - 150,
1040                300,
1041                300
1042            ),
1043            action_long_desc=message,
1044            manager=self._manager,
1045            window_title='Prompt'
1046        ))
1047
1048        def outer_callback(event):
1049            if event.type == pygame_gui._constants.UI_CONFIRMATION_DIALOG_CONFIRMED:
1050                callback(str(self._dialog.get_text()))
1051
1052        self._callback = outer_callback  # type: ignore
1053
1054    def get_file_save_location(self,
1055        callback: typing.Optional[typing.Callable[[str], None]] = None):
1056        self._dispose_dialog()
1057        self._set_dialog(pygame_gui.windows.ui_file_dialog.UIFileDialog(
1058            rect=pygame.Rect(
1059                self._sketch_size[0] / 2 - 150,
1060                self._sketch_size[1] / 2 - 150,
1061                300,
1062                300
1063            ),
1064            manager=self._manager,
1065            allow_existing_files_only=False,
1066            window_title='Save'
1067        ))
1068        self._callback = self._make_file_dialog_callback(callback)  # type: ignore
1069
1070    def get_file_load_location(self,
1071        callback: typing.Optional[typing.Callable[[str], None]] = None):
1072        self._dispose_dialog()
1073        self._set_dialog(pygame_gui.windows.ui_file_dialog.UIFileDialog(
1074            rect=pygame.Rect(
1075                self._sketch_size[0] / 2 - 150,
1076                self._sketch_size[1] / 2 - 150,
1077                300,
1078                300
1079            ),
1080            manager=self._manager,
1081            allow_existing_files_only=False,
1082            window_title='Load'
1083        ))
1084        self._callback = self._make_file_dialog_callback(callback)  # type: ignore
1085
1086    def _make_file_dialog_callback(self, inner_callback):
1087        def callback(event):
1088            if event.type == pygame_gui.UI_FILE_DIALOG_PATH_PICKED:
1089                inner_callback(str(self._dialog.current_file_path))
1090
1091        return callback
1092
1093    def _dispose_dialog(self):
1094        if self._dialog:
1095            self._dialog.kill()
1096
1097    def _set_dialog(self, new_dialog):
1098        self._dialog = new_dialog  # type: ignore
ui_available = True
DEFAULT_FPS = 20
MANUAL_OFFSET = True
OPTIONAL_SKETCH_CALLBACK = typing.Optional[typing.Callable[[sketchingpy.abstracted.Sketch], NoneType]]
class Sketch2DApp(sketchingpy.abstracted.Sketch):
 50class Sketch2DApp(sketchingpy.abstracted.Sketch):
 51    """Create a new Pygame-based Sketch."""
 52
 53    def __init__(self, width: int, height: int, title: typing.Optional[str] = None,
 54        loading_src: typing.Optional[str] = None):
 55        """Create a enw Pygame-based sketch.
 56
 57        Args:
 58            width: The width of the sketch in pixels. This will be used for window width.
 59            height: The height of the sketch in pixels. This will be used for window height.
 60            title: Starting title for the application.
 61            loading_src: ID for loading screen. Ignored, reserved for future use.
 62        """
 63        super().__init__()
 64
 65        # System params
 66        self._width = width
 67        self._height = height
 68
 69        # Callbacks
 70        self._callback_step: OPTIONAL_SKETCH_CALLBACK = None
 71        self._callback_quit: OPTIONAL_SKETCH_CALLBACK = None
 72
 73        # User configurable state
 74        self._state_frame_rate = DEFAULT_FPS
 75
 76        # Buffers
 77        self._internal_surface = None
 78        self._output_surface = None
 79        self._buffers: typing.Dict[str, pygame.Surface] = {}
 80
 81        # Internal state
 82        self._internal_pre_show_actions: typing.List[typing.Callable] = []
 83        self._internal_quit_requested = False
 84        self._internal_clock = pygame.time.Clock()
 85        self._transformer = sketchingpy.transform.Transformer()
 86        self._transformer_stack: typing.List[sketchingpy.transform.Transformer] = []
 87        self._dialog_layer: typing.Optional['AppDialogLayer'] = None
 88
 89        # Inputs
 90        self._mouse = PygameMouse()
 91        self._keyboard = PygameKeyboard()
 92
 93        # Internal struct
 94        self._struct_event_handlers = {
 95            pygame.KEYDOWN: lambda x: self._process_key_down(x),
 96            pygame.KEYUP: lambda x: self._process_key_up(x),
 97            pygame.MOUSEBUTTONDOWN: lambda x: self._process_mouse_down(x),
 98            pygame.MOUSEBUTTONUP: lambda x: self._process_mouse_up(x),
 99            pygame.locals.QUIT: lambda x: self._process_quit(x)
100        }
101
102        # Default window properties
103        self.set_title('Sketchingpy Sketch' if title is None else title)
104
105    ##########
106    # Buffer #
107    ##########
108
109    def create_buffer(self, name: str, width: int, height: int,
110        background: typing.Optional[str] = None):
111        def execute():
112            has_alpha = self._get_is_color_transparent(background)
113            self._buffers[name] = self._make_shape_surface(
114                pygame.Rect(0, 0, width, height),
115                0,
116                has_alpha=has_alpha
117            )
118            if not has_alpha:
119                self._buffers[name].fill(pygame.Color(background))
120
121        if self._internal_surface is None:
122            self._internal_pre_show_actions.append(execute)
123        else:
124            execute()
125
126    def enter_buffer(self, name: str):
127        def execute():
128            self._internal_surface = self._buffers[name]
129
130        if self._internal_surface is None:
131            self._internal_pre_show_actions.append(execute)
132        else:
133            execute()
134
135    def exit_buffer(self):
136        def execute():
137            self._internal_surface = self._output_surface
138
139        if self._internal_surface is None:
140            self._internal_pre_show_actions.append(execute)
141        else:
142            execute()
143
144    def draw_buffer(self, x: float, y: float, name: str):
145        def execute():
146            target_surface = self._buffers[name]
147
148            original_rect = target_surface.get_rect()
149            rect = pygame.Rect(
150                original_rect.x,
151                original_rect.y,
152                original_rect.width,
153                original_rect.height
154            )
155            rect.left = x
156            rect.top = y
157
158            self._blit_with_transform(
159                target_surface,
160                rect.centerx,
161                rect.centery,
162                self._transformer.quick_copy()
163            )
164
165        if self._internal_surface is None:
166            self._internal_pre_show_actions.append(execute)
167        else:
168            execute()
169
170    ############
171    # Controls #
172    ############
173
174    def get_keyboard(self) -> typing.Optional[sketchingpy.control_struct.Keyboard]:
175        return self._keyboard
176
177    def get_mouse(self) -> typing.Optional[sketchingpy.control_struct.Mouse]:
178        return self._mouse
179
180    ########
181    # Data #
182    ########
183
184    def get_data_layer(self) -> typing.Optional[sketchingpy.data_struct.DataLayer]:
185        return sketchingpy.local_data_struct.LocalDataLayer()
186
187    ###########
188    # Dialogs #
189    ###########
190
191    def get_dialog_layer(self) -> typing.Optional[sketchingpy.dialog_struct.DialogLayer]:
192        if not ui_available:
193            return None
194
195        if self._dialog_layer is None:
196            self._dialog_layer = AppDialogLayer(self)
197
198        return self._dialog_layer
199
200    ###########
201    # Drawing #
202    ###########
203
204    def clear(self, color_hex: str):
205        if self._internal_surface is None:
206            self._internal_pre_show_actions.append(lambda: self.clear(color_hex))
207            return
208
209        self._internal_surface.fill(pygame.Color(color_hex))
210
211    def draw_arc(self, x1: float, y1: float, x2: float, y2: float, a1: float,
212        a2: float):
213        state_machine = self._get_current_state_machine()
214
215        stroke_enabled = state_machine.get_stroke_enabled()
216        fill_enabled = state_machine.get_fill_enabled()
217        stroke_native = state_machine.get_stroke_native()
218        fill_native = state_machine.get_fill_native()
219        stroke_weight = state_machine.get_stroke_weight()
220
221        mode_native = state_machine.get_arc_mode_native()
222        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
223
224        a1_rad = self._convert_to_radians(a1)
225        a2_rad = self._convert_to_radians(a2)
226
227        transformer = self._transformer.quick_copy()
228
229        def execute_draw():
230            pillow_util_image = sketchingpy.pillow_util.make_arc_image(
231                rect.x,
232                rect.y,
233                rect.w,
234                rect.h,
235                a1_rad,
236                a2_rad,
237                stroke_enabled,
238                fill_enabled,
239                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
240                self._to_pillow_rgba(fill_native) if fill_enabled else None,
241                stroke_weight
242            )
243
244            native_image = self._convert_pillow_image(pillow_util_image.get_image())
245
246            self._blit_with_transform(
247                native_image,
248                rect.centerx,
249                rect.centery,
250                transformer
251            )
252
253        if self._internal_surface is None:
254            self._internal_pre_show_actions.append(execute_draw)
255        else:
256            execute_draw()
257
258    def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
259        state_machine = self._get_current_state_machine()
260
261        stroke_enabled = state_machine.get_stroke_enabled()
262        fill_enabled = state_machine.get_fill_enabled()
263        stroke_native = state_machine.get_stroke_native()
264        fill_native = state_machine.get_fill_native()
265        stroke_weight = state_machine.get_stroke_weight()
266
267        mode_native = state_machine.get_ellipse_mode_native()
268        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
269
270        transformer = self._transformer.quick_copy()
271
272        def execute_draw():
273            pillow_util_image = sketchingpy.pillow_util.make_ellipse_image(
274                rect.x,
275                rect.y,
276                rect.w,
277                rect.h,
278                stroke_enabled,
279                fill_enabled,
280                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
281                self._to_pillow_rgba(fill_native) if fill_enabled else None,
282                stroke_weight
283            )
284
285            native_image = self._convert_pillow_image(pillow_util_image.get_image())
286
287            self._blit_with_transform(
288                native_image,
289                rect.centerx,
290                rect.centery,
291                transformer
292            )
293
294        if self._internal_surface is None:
295            self._internal_pre_show_actions.append(execute_draw)
296        else:
297            execute_draw()
298
299    def draw_line(self, x1: float, y1: float, x2: float, y2: float):
300        state_machine = self._get_current_state_machine()
301        if not state_machine.get_stroke_enabled():
302            return
303
304        stroke_color = state_machine.get_stroke_native()
305        stroke_weight = state_machine.get_stroke_weight_native()
306
307        transformer = self._transformer.quick_copy()
308
309        def execute_draw():
310            min_x = min([x1, x2])
311            max_x = max([x1, x2])
312            width = max_x - min_x + 2 * stroke_weight
313
314            min_y = min([y1, y2])
315            max_y = max([y1, y2])
316            height = max_y - min_y + 2 * stroke_weight
317
318            rect = pygame.Rect(0, 0, width, height)
319            target_surface = self._make_shape_surface(rect, stroke_weight)
320
321            def adjust(target):
322                return (
323                    target[0] - min_x + stroke_weight - 1,
324                    target[1] - min_y + stroke_weight - 1,
325                )
326
327            pygame.draw.line(
328                target_surface,
329                stroke_color,
330                adjust((x1, y1)),
331                adjust((x2, y2)),
332                width=stroke_weight
333            )
334
335            center_x = (max_x + min_x) / 2
336            center_y = (max_y + min_y) / 2
337            self._blit_with_transform(target_surface, center_x, center_y, transformer)
338
339        if self._internal_surface is None:
340            self._internal_pre_show_actions.append(execute_draw)
341        else:
342            execute_draw()
343
344    def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
345        state_machine = self._get_current_state_machine()
346
347        stroke_enabled = state_machine.get_stroke_enabled()
348        fill_enabled = state_machine.get_fill_enabled()
349        stroke_native = state_machine.get_stroke_native()
350        fill_native = state_machine.get_fill_native()
351        stroke_weight = state_machine.get_stroke_weight()
352
353        mode_native = state_machine.get_rect_mode_native()
354        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
355
356        transformer = self._transformer.quick_copy()
357
358        def execute_draw():
359            pillow_util_image = sketchingpy.pillow_util.make_rect_image(
360                rect.x,
361                rect.y,
362                rect.w,
363                rect.h,
364                stroke_enabled,
365                fill_enabled,
366                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
367                self._to_pillow_rgba(fill_native) if fill_enabled else None,
368                stroke_weight
369            )
370
371            native_image = self._convert_pillow_image(pillow_util_image.get_image())
372
373            self._blit_with_transform(
374                native_image,
375                rect.centerx,
376                rect.centery,
377                transformer
378            )
379
380        if self._internal_surface is None:
381            self._internal_pre_show_actions.append(execute_draw)
382        else:
383            execute_draw()
384
385    def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
386        if not shape.get_is_finished():
387            raise RuntimeError('Finish your shape before drawing.')
388
389        state_machine = self._get_current_state_machine()
390
391        stroke_enabled = state_machine.get_stroke_enabled()
392        fill_enabled = state_machine.get_fill_enabled()
393        stroke_native = state_machine.get_stroke_native()
394        fill_native = state_machine.get_fill_native()
395        stroke_weight = state_machine.get_stroke_weight()
396
397        transformer = self._transformer.quick_copy()
398
399        def execute_draw():
400            pillow_util_image = sketchingpy.pillow_util.make_shape_image(
401                shape,
402                stroke_enabled,
403                fill_enabled,
404                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
405                self._to_pillow_rgba(fill_native) if fill_enabled else None,
406                stroke_weight
407            )
408
409            native_image = self._convert_pillow_image(pillow_util_image.get_image())
410
411            min_x = shape.get_min_x()
412            max_x = shape.get_max_x()
413            center_x = (max_x + min_x) / 2
414
415            min_y = shape.get_min_y()
416            max_y = shape.get_max_y()
417            center_y = (max_y + min_y) / 2
418
419            self._blit_with_transform(
420                native_image,
421                center_x,
422                center_y,
423                transformer
424            )
425
426        if self._internal_surface is None:
427            self._internal_pre_show_actions.append(execute_draw)
428        else:
429            execute_draw()
430
431    def draw_text(self, x: float, y: float, content: str):
432        content = str(content)
433        state_machine = self._get_current_state_machine()
434
435        stroke_enabled = state_machine.get_stroke_enabled()
436        fill_enabled = state_machine.get_fill_enabled()
437        stroke_native = state_machine.get_stroke_native()
438        fill_native = state_machine.get_fill_native()
439        stroke_weight = state_machine.get_stroke_weight()
440
441        text_font = state_machine.get_text_font_native()
442        fill_pillow = self._to_pillow_rgba(fill_native)
443        stroke_pillow = self._to_pillow_rgba(stroke_native)
444
445        align_info = state_machine.get_text_align_native()
446        anchor_str = align_info.get_horizontal_align() + align_info.get_vertical_align()
447
448        transformer = self._transformer.quick_copy()
449
450        def execute_draw():
451            pillow_util_image = sketchingpy.pillow_util.make_text_image(
452                x,
453                y,
454                content,
455                text_font,
456                stroke_enabled,
457                fill_enabled,
458                stroke_pillow,
459                fill_pillow,
460                stroke_weight,
461                anchor_str
462            )
463
464            native_image = self._convert_pillow_image(pillow_util_image.get_image())
465
466            self._blit_with_transform(
467                native_image,
468                pillow_util_image.get_x() + pillow_util_image.get_width() / 2,
469                pillow_util_image.get_y() + pillow_util_image.get_height() / 2,
470                transformer
471            )
472
473        if self._internal_surface is None:
474            self._internal_pre_show_actions.append(execute_draw)
475        else:
476            execute_draw()
477
478    ##########
479    # Events #
480    ##########
481
482    def on_step(self, callback: sketchingpy.abstracted.StepCallback):
483        self._callback_step = callback
484
485    def on_quit(self, callback: sketchingpy.abstracted.QuitCallback):
486        self._callback_quit = callback
487
488    #########
489    # Image #
490    #########
491
492    def get_image(self, src: str) -> sketchingpy.abstracted.Image:
493        return PygameImage(src)
494
495    def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
496        if not image.get_is_loaded():
497            return
498
499        transformer = self._transformer.quick_copy()
500
501        image_mode_native = self._get_current_state_machine().get_image_mode_native()
502
503        def execute_draw():
504            rect = self._build_rect_with_mode(
505                x,
506                y,
507                image.get_width(),
508                image.get_height(),
509                image_mode_native
510            )
511
512            surface = image.get_native()
513            self._blit_with_transform(surface, rect.centerx, rect.centery, transformer)
514
515        if self._internal_surface is None:
516            self._internal_pre_show_actions.append(execute_draw)
517        else:
518            execute_draw()
519
520    def save_image(self, path: str):
521        def execute_save():
522            pygame.image.save(self._internal_surface, path)
523
524        if self._internal_surface is None:
525            self._internal_pre_show_actions.append(execute_save)
526            self.show_and_quit()
527        else:
528            execute_save()
529
530    #########
531    # State #
532    #########
533
534    def push_transform(self):
535        self._transformer_stack.append(copy.deepcopy(self._transformer))
536
537    def pop_transform(self):
538        if len(self._transformer_stack) == 0:
539            raise RuntimeError('Transformation stack empty.')
540
541        self._transformer = self._transformer_stack.pop()
542
543    ##########
544    # System #
545    ##########
546
547    def get_native(self):
548        if self._internal_surface is None:
549            raise RuntimeError('Need to show sketch first before surface is available.')
550
551        return self._internal_surface
552
553    def set_fps(self, rate: int):
554        self._state_frame_rate = rate
555
556    def set_title(self, title: str):
557        def execute():
558            pygame.display.set_caption(title)
559
560        if self._internal_surface is None:
561            self._internal_pre_show_actions.append(execute)
562        else:
563            execute()
564
565    def quit(self):
566        self._internal_quit_requested = True
567
568    def show(self, ax=None):
569        self._show_internal(ax=ax, quit_immediately=False)
570
571    def show_and_quit(self, ax=None):
572        self._show_internal(ax=ax, quit_immediately=True)
573
574    #############
575    # Transform #
576    #############
577
578    def translate(self, x: float, y: float):
579        self._transformer.translate(x, y)
580
581    def rotate(self, angle_mirror: float):
582        angle = -1 * angle_mirror
583        angle_rad = self._convert_to_radians(angle)
584        self._transformer.rotate(angle_rad)
585
586    def scale(self, scale: float):
587        self._transformer.scale(scale)
588
589    ###########
590    # Support #
591    ###########
592
593    def _get_window_size(self) -> typing.Tuple[int, int]:
594        return (self._width, self._height)
595
596    def _show_internal(self, ax=None, quit_immediately=False):
597        self._snapshot_time()
598        pygame.init()
599        self._internal_surface = pygame.display.set_mode((self._width, self._height))
600        self._output_surface = self._internal_surface
601
602        for action in self._internal_pre_show_actions:
603            action()
604
605        self._inner_loop(quit_immediately=quit_immediately)
606
607    def _inner_loop(self, quit_immediately=False):
608        clock = pygame.time.Clock()
609
610        while not self._internal_quit_requested:
611            time_delta = clock.tick(60) / 1000.0
612
613            for event in pygame.event.get():
614                self._process_event(event)
615                if self._dialog_layer:
616                    self._dialog_layer.get_manager().process_events(event)
617                    dialog = self._dialog_layer.get_dialog()
618                    try:
619                        if dialog is not None and event.ui_element == dialog:
620                            self._dialog_layer.report_close(event)
621                    except AttributeError:
622                        pass
623
624            if self._dialog_layer:
625                self._dialog_layer.get_manager().update(time_delta)
626
627            if self._callback_step is not None:
628                self._callback_step(self)
629
630            if self._dialog_layer:
631                self._dialog_layer.get_manager().draw_ui(self._internal_surface)
632
633            pygame.display.update()
634            self._internal_clock.tick(self._state_frame_rate)
635
636            if quit_immediately:
637                self._internal_quit_requested = True
638
639        if self._callback_quit is not None:
640            self._callback_quit(self)
641
642    def _process_event(self, event):
643        if event.type not in self._struct_event_handlers:
644            return
645
646        self._struct_event_handlers[event.type](event)
647
648    def _process_quit(self, event):
649        self._internal_quit_requested = True
650
651    def _process_mouse_down(self, event):
652        self._mouse.report_mouse_down(event)
653
654    def _process_mouse_up(self, event):
655        self._mouse.report_mouse_up(event)
656
657    def _process_key_down(self, event):
658        self._keyboard.report_key_down(event)
659
660    def _process_key_up(self, event):
661        self._keyboard.report_key_up(event)
662
663    def _create_state_machine(self) -> sketchingpy.state_struct.SketchStateMachine:
664        return PygameSketchStateMachine()
665
666    def _make_shape_surface(self, rect: pygame.Rect, stroke_weight: float,
667        has_alpha: bool = True) -> pygame.Surface:
668        if has_alpha:
669            return pygame.Surface((rect.w + stroke_weight, rect.h + stroke_weight), pygame.SRCALPHA)
670        else:
671            return pygame.Surface((rect.w + stroke_weight, rect.h + stroke_weight))
672
673    def _zero_rect(self, rect: pygame.Rect) -> pygame.Rect:
674        return pygame.Rect(0, 0, rect.w, rect.h)
675
676    def _build_rect_with_mode(self, x1: float, y1: float, x2: float, y2: float,
677        native_mode: int) -> pygame.Rect:
678        if native_mode == sketchingpy.const.CENTER:
679            start_x = x1 - math.floor(x2 / 2)
680            start_y = y1 - math.floor(y2 / 2)
681            width = x2
682            height = y2
683        elif native_mode == sketchingpy.const.RADIUS:
684            start_x = x1 - x2
685            start_y = y1 - y2
686            width = x2 * 2
687            height = y2 * 2
688        elif native_mode == sketchingpy.const.CORNER:
689            start_x = x1
690            start_y = y1
691            width = x2
692            height = y2
693        elif native_mode == sketchingpy.const.CORNERS:
694            (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
695            start_x = x1
696            start_y = y1
697            width = x2 - x1
698            height = y2 - y1
699        else:
700            raise RuntimeError('Unknown mode: ' + str(native_mode))
701
702        return pygame.Rect(start_x, start_y, width, height)
703
704    def _draw_primitive(self, x1: float, y1: float, x2: float, y2: float,
705        mode: str, native_mode, draw_method):
706        state_machine = self._get_current_state_machine()
707        has_fill = state_machine.get_fill_enabled()
708        fill_color = state_machine.get_fill_native()
709        has_stroke = state_machine.get_stroke_enabled()
710        stroke_color = state_machine.get_stroke_native()
711        rect = self._build_rect_with_mode(x1, y1, x2, y2, native_mode)
712        stroke_weight = state_machine.get_stroke_weight_native()
713
714        transformer = self._transformer.quick_copy()
715
716        def execute_draw_piece(color, strategy):
717            target_surface = self._make_shape_surface(rect, stroke_weight)
718            rect_adj = self._zero_rect(rect)
719
720            strategy(target_surface, rect_adj)
721
722            self._blit_with_transform(
723                target_surface,
724                rect.centerx,
725                rect.centery,
726                transformer
727            )
728
729        def execute_draw():
730            if has_fill:
731                execute_draw_piece(
732                    fill_color,
733                    lambda surface, rect: draw_method(
734                        surface,
735                        fill_color,
736                        self._offset_fill_weight(rect, stroke_weight),
737                        0
738                    )
739                )
740
741            if has_stroke:
742                execute_draw_piece(
743                    stroke_color,
744                    lambda surface, rect: draw_method(
745                        surface,
746                        stroke_color,
747                        self._offset_stroke_weight(rect, stroke_weight),
748                        stroke_weight
749                    )
750                )
751
752        if self._internal_surface is None:
753            self._internal_pre_show_actions.append(execute_draw)
754            return
755        else:
756            execute_draw()
757
758    def _to_pillow_rgba(self, target: pygame.Color):
759        return (target.r, target.g, target.b, target.a)
760
761    def _convert_pillow_image(self, target: PIL.Image.Image) -> pygame.Surface:
762        return pygame.image.fromstring(
763            target.tobytes(),
764            target.size,
765            target.mode  # type: ignore
766        ).convert_alpha()
767
768    def _blit_with_transform(self, surface: pygame.Surface, x: float, y: float,
769        transformer: sketchingpy.transform.Transformer):
770        start_rect = surface.get_rect()
771        start_rect.centerx = x  # type: ignore
772        start_rect.centery = y  # type: ignore
773
774        transformed_center = transformer.transform(
775            start_rect.centerx,
776            start_rect.centery
777        )
778
779        has_scale = transformed_center.get_scale() != 1
780        has_rotation = transformed_center.get_rotation() != 0
781        has_content_transform = has_scale or has_rotation
782        if has_content_transform:
783            angle = transformed_center.get_rotation()
784            angle_transform = math.degrees(angle)
785            scale = transformed_center.get_scale()
786            surface = pygame.transform.rotozoom(surface, angle_transform, scale)
787            end_rect = surface.get_rect()
788        else:
789            end_rect = start_rect
790
791        end_rect.centerx = transformed_center.get_x()  # type: ignore
792        end_rect.centery = transformed_center.get_y()  # type: ignore
793
794        assert self._internal_surface is not None
795        self._internal_surface.blit(surface, (end_rect.x, end_rect.y))

Create a new Pygame-based Sketch.

Sketch2DApp( width: int, height: int, title: Optional[str] = None, loading_src: Optional[str] = None)
 53    def __init__(self, width: int, height: int, title: typing.Optional[str] = None,
 54        loading_src: typing.Optional[str] = None):
 55        """Create a enw Pygame-based sketch.
 56
 57        Args:
 58            width: The width of the sketch in pixels. This will be used for window width.
 59            height: The height of the sketch in pixels. This will be used for window height.
 60            title: Starting title for the application.
 61            loading_src: ID for loading screen. Ignored, reserved for future use.
 62        """
 63        super().__init__()
 64
 65        # System params
 66        self._width = width
 67        self._height = height
 68
 69        # Callbacks
 70        self._callback_step: OPTIONAL_SKETCH_CALLBACK = None
 71        self._callback_quit: OPTIONAL_SKETCH_CALLBACK = None
 72
 73        # User configurable state
 74        self._state_frame_rate = DEFAULT_FPS
 75
 76        # Buffers
 77        self._internal_surface = None
 78        self._output_surface = None
 79        self._buffers: typing.Dict[str, pygame.Surface] = {}
 80
 81        # Internal state
 82        self._internal_pre_show_actions: typing.List[typing.Callable] = []
 83        self._internal_quit_requested = False
 84        self._internal_clock = pygame.time.Clock()
 85        self._transformer = sketchingpy.transform.Transformer()
 86        self._transformer_stack: typing.List[sketchingpy.transform.Transformer] = []
 87        self._dialog_layer: typing.Optional['AppDialogLayer'] = None
 88
 89        # Inputs
 90        self._mouse = PygameMouse()
 91        self._keyboard = PygameKeyboard()
 92
 93        # Internal struct
 94        self._struct_event_handlers = {
 95            pygame.KEYDOWN: lambda x: self._process_key_down(x),
 96            pygame.KEYUP: lambda x: self._process_key_up(x),
 97            pygame.MOUSEBUTTONDOWN: lambda x: self._process_mouse_down(x),
 98            pygame.MOUSEBUTTONUP: lambda x: self._process_mouse_up(x),
 99            pygame.locals.QUIT: lambda x: self._process_quit(x)
100        }
101
102        # Default window properties
103        self.set_title('Sketchingpy Sketch' if title is None else title)

Create a enw Pygame-based sketch.

Arguments:
  • width: The width of the sketch in pixels. This will be used for window width.
  • height: The height of the sketch in pixels. This will be used for window height.
  • title: Starting title for the application.
  • loading_src: ID for loading screen. Ignored, reserved for future use.
def create_buffer( self, name: str, width: int, height: int, background: Optional[str] = None):
109    def create_buffer(self, name: str, width: int, height: int,
110        background: typing.Optional[str] = None):
111        def execute():
112            has_alpha = self._get_is_color_transparent(background)
113            self._buffers[name] = self._make_shape_surface(
114                pygame.Rect(0, 0, width, height),
115                0,
116                has_alpha=has_alpha
117            )
118            if not has_alpha:
119                self._buffers[name].fill(pygame.Color(background))
120
121        if self._internal_surface is None:
122            self._internal_pre_show_actions.append(execute)
123        else:
124            execute()

Create a new named in-memory (or equivalent) buffer.

Arguments:
  • name: The name of the buffer. If a prior buffer of this name exists, it will be replaced.
  • width: The width of the buffer in pixels. In some renderers, the buffer will clip. In others, out of buffer values may be drawn.
  • height: The height of the buffer in pixels. In some renderers, the buffer will clip. In others, out of buffer values may be drawn.
  • background: The background to use for this buffer or None if transparent. Defaults to None.
def enter_buffer(self, name: str):
126    def enter_buffer(self, name: str):
127        def execute():
128            self._internal_surface = self._buffers[name]
129
130        if self._internal_surface is None:
131            self._internal_pre_show_actions.append(execute)
132        else:
133            execute()

Switch rendering context to a buffer, exiting current buffer if active.

Arguments:
  • name: The name of the buffer to which context should switch.
def exit_buffer(self):
135    def exit_buffer(self):
136        def execute():
137            self._internal_surface = self._output_surface
138
139        if self._internal_surface is None:
140            self._internal_pre_show_actions.append(execute)
141        else:
142            execute()

Exit the current offscreen buffer.

Exit the current offscreen buffer, returning to the actual sketch. This will act as a noop if not currently in a buffer.

def draw_buffer(self, x: float, y: float, name: str):
144    def draw_buffer(self, x: float, y: float, name: str):
145        def execute():
146            target_surface = self._buffers[name]
147
148            original_rect = target_surface.get_rect()
149            rect = pygame.Rect(
150                original_rect.x,
151                original_rect.y,
152                original_rect.width,
153                original_rect.height
154            )
155            rect.left = x
156            rect.top = y
157
158            self._blit_with_transform(
159                target_surface,
160                rect.centerx,
161                rect.centery,
162                self._transformer.quick_copy()
163            )
164
165        if self._internal_surface is None:
166            self._internal_pre_show_actions.append(execute)
167        else:
168            execute()

Draw an offscreen buffer to the current buffer or sketch.

Arguments:
  • x: The horizontal position in pixels at which the left should be drawn.
  • y: The vertical position in pixels at which the top should be drawn.
  • name: The name of the buffer to draw.
def get_keyboard(self) -> Optional[sketchingpy.control_struct.Keyboard]:
174    def get_keyboard(self) -> typing.Optional[sketchingpy.control_struct.Keyboard]:
175        return self._keyboard

Get access to the keyboard.

Get access to the keyboard currently registered with the operating system for the sketch. Different sketches running at the same time may have different keyboards depending on focus or OS configuration.

Returns:

Current keyboard or None if not found / supported.

def get_mouse(self) -> Optional[sketchingpy.control_struct.Mouse]:
177    def get_mouse(self) -> typing.Optional[sketchingpy.control_struct.Mouse]:
178        return self._mouse

Get access to the mouse.

Get access to the mouse currently registered with the operating system for the sketch. Different sketches running at the same time may have different mouse objects depending on focus or OS configuration. Note that the mouse may also be emulated if the device uses a touch screen.

Returns:

Current mouse or None if not found / supported.

def get_data_layer(self) -> Optional[sketchingpy.data_struct.DataLayer]:
184    def get_data_layer(self) -> typing.Optional[sketchingpy.data_struct.DataLayer]:
185        return sketchingpy.local_data_struct.LocalDataLayer()

Get access to reading and writing data.

Open access to the file system, network, or browser to read or write data.

Returns:

Facade for data access or None if not supported or insufficient permissions.

def get_dialog_layer(self) -> Optional[sketchingpy.dialog_struct.DialogLayer]:
191    def get_dialog_layer(self) -> typing.Optional[sketchingpy.dialog_struct.DialogLayer]:
192        if not ui_available:
193            return None
194
195        if self._dialog_layer is None:
196            self._dialog_layer = AppDialogLayer(self)
197
198        return self._dialog_layer

Get access to rendering and using simple dialogs.

Open access to a simple dialog prefabricated UI system to show alerts, prompts, and other dialog boxes.

Returns:

Facade for rendering dialogs or None if not supported or insufficient permissions.

def clear(self, color_hex: str):
204    def clear(self, color_hex: str):
205        if self._internal_surface is None:
206            self._internal_pre_show_actions.append(lambda: self.clear(color_hex))
207            return
208
209        self._internal_surface.fill(pygame.Color(color_hex))

Clear the sketch to a color.

Peform the equivalent of drawing a rectangle the size of the sketch without stroke and with the given fill color.

Arguments:
  • color: The color to use in clearing.
def draw_arc( self, x1: float, y1: float, x2: float, y2: float, a1: float, a2: float):
211    def draw_arc(self, x1: float, y1: float, x2: float, y2: float, a1: float,
212        a2: float):
213        state_machine = self._get_current_state_machine()
214
215        stroke_enabled = state_machine.get_stroke_enabled()
216        fill_enabled = state_machine.get_fill_enabled()
217        stroke_native = state_machine.get_stroke_native()
218        fill_native = state_machine.get_fill_native()
219        stroke_weight = state_machine.get_stroke_weight()
220
221        mode_native = state_machine.get_arc_mode_native()
222        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
223
224        a1_rad = self._convert_to_radians(a1)
225        a2_rad = self._convert_to_radians(a2)
226
227        transformer = self._transformer.quick_copy()
228
229        def execute_draw():
230            pillow_util_image = sketchingpy.pillow_util.make_arc_image(
231                rect.x,
232                rect.y,
233                rect.w,
234                rect.h,
235                a1_rad,
236                a2_rad,
237                stroke_enabled,
238                fill_enabled,
239                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
240                self._to_pillow_rgba(fill_native) if fill_enabled else None,
241                stroke_weight
242            )
243
244            native_image = self._convert_pillow_image(pillow_util_image.get_image())
245
246            self._blit_with_transform(
247                native_image,
248                rect.centerx,
249                rect.centery,
250                transformer
251            )
252
253        if self._internal_surface is None:
254            self._internal_pre_show_actions.append(execute_draw)
255        else:
256            execute_draw()

Draw a partial ellipse using starting and ending angles.

Using starting and ending angles, draw a partial ellipse which is either drawn outside line only (stroke) and / or filled from the center of that ellipse.

Arguments:
  • x1: The x location at which to draw the arc.
  • y1: The y location at which to draw the arc.
  • x2: Horizontal size.
  • y2: Vertical size.
  • a1: Starting angle.
  • a2: Ending angle.
def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
258    def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
259        state_machine = self._get_current_state_machine()
260
261        stroke_enabled = state_machine.get_stroke_enabled()
262        fill_enabled = state_machine.get_fill_enabled()
263        stroke_native = state_machine.get_stroke_native()
264        fill_native = state_machine.get_fill_native()
265        stroke_weight = state_machine.get_stroke_weight()
266
267        mode_native = state_machine.get_ellipse_mode_native()
268        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
269
270        transformer = self._transformer.quick_copy()
271
272        def execute_draw():
273            pillow_util_image = sketchingpy.pillow_util.make_ellipse_image(
274                rect.x,
275                rect.y,
276                rect.w,
277                rect.h,
278                stroke_enabled,
279                fill_enabled,
280                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
281                self._to_pillow_rgba(fill_native) if fill_enabled else None,
282                stroke_weight
283            )
284
285            native_image = self._convert_pillow_image(pillow_util_image.get_image())
286
287            self._blit_with_transform(
288                native_image,
289                rect.centerx,
290                rect.centery,
291                transformer
292            )
293
294        if self._internal_surface is None:
295            self._internal_pre_show_actions.append(execute_draw)
296        else:
297            execute_draw()

Draw a circle or ellipse.

Draw an ellipse or, in the case of equal width and height, a circle.

Arguments:
  • x1: The x location at which to draw the ellipse.
  • y1: The y location at which to draw the ellipse.
  • x2: Horizontal size.
  • y2: Vertical size.
def draw_line(self, x1: float, y1: float, x2: float, y2: float):
299    def draw_line(self, x1: float, y1: float, x2: float, y2: float):
300        state_machine = self._get_current_state_machine()
301        if not state_machine.get_stroke_enabled():
302            return
303
304        stroke_color = state_machine.get_stroke_native()
305        stroke_weight = state_machine.get_stroke_weight_native()
306
307        transformer = self._transformer.quick_copy()
308
309        def execute_draw():
310            min_x = min([x1, x2])
311            max_x = max([x1, x2])
312            width = max_x - min_x + 2 * stroke_weight
313
314            min_y = min([y1, y2])
315            max_y = max([y1, y2])
316            height = max_y - min_y + 2 * stroke_weight
317
318            rect = pygame.Rect(0, 0, width, height)
319            target_surface = self._make_shape_surface(rect, stroke_weight)
320
321            def adjust(target):
322                return (
323                    target[0] - min_x + stroke_weight - 1,
324                    target[1] - min_y + stroke_weight - 1,
325                )
326
327            pygame.draw.line(
328                target_surface,
329                stroke_color,
330                adjust((x1, y1)),
331                adjust((x2, y2)),
332                width=stroke_weight
333            )
334
335            center_x = (max_x + min_x) / 2
336            center_y = (max_y + min_y) / 2
337            self._blit_with_transform(target_surface, center_x, center_y, transformer)
338
339        if self._internal_surface is None:
340            self._internal_pre_show_actions.append(execute_draw)
341        else:
342            execute_draw()

Draw a simple line.

Draw a line between two points.

Arguments:
  • x1: The x coordinate from which the line should be drawn.
  • y1: The y coordinate from which the line should be drawn.
  • x2: The x coordinate to which the line should be drawn.
  • y2: The y coordinate to which the line should be drawn.
def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
344    def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
345        state_machine = self._get_current_state_machine()
346
347        stroke_enabled = state_machine.get_stroke_enabled()
348        fill_enabled = state_machine.get_fill_enabled()
349        stroke_native = state_machine.get_stroke_native()
350        fill_native = state_machine.get_fill_native()
351        stroke_weight = state_machine.get_stroke_weight()
352
353        mode_native = state_machine.get_rect_mode_native()
354        rect = self._build_rect_with_mode(x1, y1, x2, y2, mode_native)
355
356        transformer = self._transformer.quick_copy()
357
358        def execute_draw():
359            pillow_util_image = sketchingpy.pillow_util.make_rect_image(
360                rect.x,
361                rect.y,
362                rect.w,
363                rect.h,
364                stroke_enabled,
365                fill_enabled,
366                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
367                self._to_pillow_rgba(fill_native) if fill_enabled else None,
368                stroke_weight
369            )
370
371            native_image = self._convert_pillow_image(pillow_util_image.get_image())
372
373            self._blit_with_transform(
374                native_image,
375                rect.centerx,
376                rect.centery,
377                transformer
378            )
379
380        if self._internal_surface is None:
381            self._internal_pre_show_actions.append(execute_draw)
382        else:
383            execute_draw()

Draw a rectangle.

Draw a rectangle or, if width and height are the same, a square.

Arguments:
  • x1: The x location at which to draw the rectangle.
  • y1: The y location at which to draw the rectangle.
  • x2: Horizontal size.
  • y2: Vertical size.
def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
385    def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
386        if not shape.get_is_finished():
387            raise RuntimeError('Finish your shape before drawing.')
388
389        state_machine = self._get_current_state_machine()
390
391        stroke_enabled = state_machine.get_stroke_enabled()
392        fill_enabled = state_machine.get_fill_enabled()
393        stroke_native = state_machine.get_stroke_native()
394        fill_native = state_machine.get_fill_native()
395        stroke_weight = state_machine.get_stroke_weight()
396
397        transformer = self._transformer.quick_copy()
398
399        def execute_draw():
400            pillow_util_image = sketchingpy.pillow_util.make_shape_image(
401                shape,
402                stroke_enabled,
403                fill_enabled,
404                self._to_pillow_rgba(stroke_native) if stroke_enabled else None,
405                self._to_pillow_rgba(fill_native) if fill_enabled else None,
406                stroke_weight
407            )
408
409            native_image = self._convert_pillow_image(pillow_util_image.get_image())
410
411            min_x = shape.get_min_x()
412            max_x = shape.get_max_x()
413            center_x = (max_x + min_x) / 2
414
415            min_y = shape.get_min_y()
416            max_y = shape.get_max_y()
417            center_y = (max_y + min_y) / 2
418
419            self._blit_with_transform(
420                native_image,
421                center_x,
422                center_y,
423                transformer
424            )
425
426        if self._internal_surface is None:
427            self._internal_pre_show_actions.append(execute_draw)
428        else:
429            execute_draw()

Draw a shape.

Draw a shape which consists of multiple line or curve segments and which can be either open (stroke only) or closed (can be filled).

Arguments:
  • shape: The shape to draw.
def draw_text(self, x: float, y: float, content: str):
431    def draw_text(self, x: float, y: float, content: str):
432        content = str(content)
433        state_machine = self._get_current_state_machine()
434
435        stroke_enabled = state_machine.get_stroke_enabled()
436        fill_enabled = state_machine.get_fill_enabled()
437        stroke_native = state_machine.get_stroke_native()
438        fill_native = state_machine.get_fill_native()
439        stroke_weight = state_machine.get_stroke_weight()
440
441        text_font = state_machine.get_text_font_native()
442        fill_pillow = self._to_pillow_rgba(fill_native)
443        stroke_pillow = self._to_pillow_rgba(stroke_native)
444
445        align_info = state_machine.get_text_align_native()
446        anchor_str = align_info.get_horizontal_align() + align_info.get_vertical_align()
447
448        transformer = self._transformer.quick_copy()
449
450        def execute_draw():
451            pillow_util_image = sketchingpy.pillow_util.make_text_image(
452                x,
453                y,
454                content,
455                text_font,
456                stroke_enabled,
457                fill_enabled,
458                stroke_pillow,
459                fill_pillow,
460                stroke_weight,
461                anchor_str
462            )
463
464            native_image = self._convert_pillow_image(pillow_util_image.get_image())
465
466            self._blit_with_transform(
467                native_image,
468                pillow_util_image.get_x() + pillow_util_image.get_width() / 2,
469                pillow_util_image.get_y() + pillow_util_image.get_height() / 2,
470                transformer
471            )
472
473        if self._internal_surface is None:
474            self._internal_pre_show_actions.append(execute_draw)
475        else:
476            execute_draw()

Draw text using the current font.

Draw text using the current font and alignment.

Arguments:
  • x: The x coordinate at which to draw the text.
  • y: The y coordinate at which to draw the text.
  • text: The string to draw.
def on_step(self, callback: Callable[[ForwardRef('Sketch')], NoneType]):
482    def on_step(self, callback: sketchingpy.abstracted.StepCallback):
483        self._callback_step = callback

Callback for when the sketch ends execution.

Register a callback for when the sketch redraws. This function should expect a single parameter which is the sketch redrawing.

Arguments:
  • callback: The function to invoke when the sketch stops execution.
def on_quit(self, callback: Callable[[ForwardRef('Sketch')], NoneType]):
485    def on_quit(self, callback: sketchingpy.abstracted.QuitCallback):
486        self._callback_quit = callback

Callback for when the sketch ends execution.

Register a callback for when the sketch terminates.

Arguments:
  • callback: The function to invoke when the sketch stops execution.
def get_image(self, src: str) -> sketchingpy.abstracted.Image:
492    def get_image(self, src: str) -> sketchingpy.abstracted.Image:
493        return PygameImage(src)

Load an image file.

Load an image from the local file system or URL.

Arguments:
  • src: The location from which the file should be read.
def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
495    def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
496        if not image.get_is_loaded():
497            return
498
499        transformer = self._transformer.quick_copy()
500
501        image_mode_native = self._get_current_state_machine().get_image_mode_native()
502
503        def execute_draw():
504            rect = self._build_rect_with_mode(
505                x,
506                y,
507                image.get_width(),
508                image.get_height(),
509                image_mode_native
510            )
511
512            surface = image.get_native()
513            self._blit_with_transform(surface, rect.centerx, rect.centery, transformer)
514
515        if self._internal_surface is None:
516            self._internal_pre_show_actions.append(execute_draw)
517        else:
518            execute_draw()

Draw an image at a location.

Draw a previously loaded image at a specific coordinate using its current size.

Arguments:
  • x: Horizontal coordinate at which to draw the image.
  • y: Vertical coordinate at which to draw the image.
  • image: The image to draw.
def save_image(self, path: str):
520    def save_image(self, path: str):
521        def execute_save():
522            pygame.image.save(self._internal_surface, path)
523
524        if self._internal_surface is None:
525            self._internal_pre_show_actions.append(execute_save)
526            self.show_and_quit()
527        else:
528            execute_save()

Save an image file.

Save the sketch as an image file, either directly to the file system or as a download.

Arguments:
  • path: The location at which the file should be written.
def push_transform(self):
534    def push_transform(self):
535        self._transformer_stack.append(copy.deepcopy(self._transformer))

Save current transformation state.

Save current sketch transformation state to the matrix history. This works as a stack (like a stack of plates) where this puts a new plate on the top of the pile. This will leave the current transformation matrix in the sketch unchanged.

def pop_transform(self):
537    def pop_transform(self):
538        if len(self._transformer_stack) == 0:
539            raise RuntimeError('Transformation stack empty.')
540
541        self._transformer = self._transformer_stack.pop()

Restore a previously saved transformation state.

Restore the most recently transformation configuration saved in matrix history, removing that "transform matrix" from the history. This works as a stack (like a stack of plates) where the top of the pile is taken off and restored, removing it from that stack. This will overwrite the current transformation configuration in the sketch.

def get_native(self):
547    def get_native(self):
548        if self._internal_surface is None:
549            raise RuntimeError('Need to show sketch first before surface is available.')
550
551        return self._internal_surface

Get a reference to the underlying native renderer object.

Returns:

Native render object.

def set_fps(self, rate: int):
553    def set_fps(self, rate: int):
554        self._state_frame_rate = rate

Indicate how fast the sketch should redraw.

Indicate a target frames per second that the sketch will take a "step" or redraw. Note that this is a goal and, if the system fall behind, it will drop frames and cause the on_step callback to be executed fewer times than the target.

Arguments:
  • rate: The number of frames to try to draw per second.
def set_title(self, title: str):
556    def set_title(self, title: str):
557        def execute():
558            pygame.display.set_caption(title)
559
560        if self._internal_surface is None:
561            self._internal_pre_show_actions.append(execute)
562        else:
563            execute()

Indicate the title to assign the window in the operating system.

Indicate the human-readable string title to assign to the sketch window.

Arguments:
  • title: The text of the title.
def quit(self):
565    def quit(self):
566        self._internal_quit_requested = True

Finish execution of the sketch.

Cause the sketch to stop execution.

def show(self, ax=None):
568    def show(self, ax=None):
569        self._show_internal(ax=ax, quit_immediately=False)

Show the sketch.

Show the sketch to the user and, if applicable, start the draw loop specified by set_fps. For Sketch2DApp, will execute any waiting drawing instructions provided to the sketch prior to showing. This is conceptually the same as "starting" the sketch.

Arguments:
  • ax: The container into which the sketch should be shown. Currently only supported for Sketch2DStatic. Optional and ignored on most renderers.
def show_and_quit(self, ax=None):
571    def show_and_quit(self, ax=None):
572        self._show_internal(ax=ax, quit_immediately=True)

Show the sketch and quit immediatley afterwards.

Show the sketch to the user and quit immediately afterwards, a routine potentially useful for testing.

def translate(self, x: float, y: float):
578    def translate(self, x: float, y: float):
579        self._transformer.translate(x, y)

Change the location of the origin.

Change the transform matrix such that any drawing afterwards is moved by a set amount.

Arguments:
  • x: The number of pixels to offset horizontally.
  • y: The number of pixels to offset vertically.
def rotate(self, angle_mirror: float):
581    def rotate(self, angle_mirror: float):
582        angle = -1 * angle_mirror
583        angle_rad = self._convert_to_radians(angle)
584        self._transformer.rotate(angle_rad)

Rotate around the current origin.

Change the transform matrix such that any drawing afterwards is rotated around the current origin clock-wise.

Arguments:
  • angle: The angle by which to rotate.
def scale(self, scale: float):
586    def scale(self, scale: float):
587        self._transformer.scale(scale)

Scale outwards from the current origin.

Change the transform matrix such that any drawing afterwards is scaled from the current origin.

Arguments:
  • scale: The factor by which to scale where values over 1 scale up and less than 1 scale down. A value of 1 will have no effect.
class PygameSketchStateMachine(sketchingpy.state_struct.SketchStateMachine):
798class PygameSketchStateMachine(sketchingpy.state_struct.SketchStateMachine):
799    """Implementation of SketchStateMachine for Pygame types."""
800
801    def __init__(self):
802        """Create a new state machine for Pygame-based sketches."""
803        super().__init__()
804        self._fill_native = pygame.Color(super().get_fill())
805        self._stroke_native = pygame.Color(super().get_stroke())
806        self._font_cache = {}
807        self._text_align_native = self._transform_text_align(super().get_text_align_native())
808
809    def set_fill(self, fill: str):
810        super().set_fill(fill)
811        self._fill_native = pygame.Color(super().get_fill())
812
813    def get_fill_native(self):
814        return self._fill_native
815
816    def set_stroke(self, stroke: str):
817        super().set_stroke(stroke)
818        self._stroke_native = pygame.Color(super().get_stroke())
819
820    def get_stroke_native(self):
821        return self._stroke_native
822
823    def get_text_font_native(self):
824        font = self.get_text_font()
825        key = '%s.%d' % (font.get_identifier(), font.get_size())
826
827        if key not in self._font_cache:
828            new_font = PIL.ImageFont.truetype(font.get_identifier(), font.get_size())
829            self._font_cache[key] = new_font
830
831        return self._font_cache[key]
832
833    def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
834        super().set_text_align(text_align)
835        self._text_align_native = self._transform_text_align(super().get_text_align_native())
836
837    def get_text_align_native(self):
838        return self._text_align_native
839
840    def _transform_text_align(self,
841        text_align: sketchingpy.state_struct.TextAlign) -> sketchingpy.state_struct.TextAlign:
842
843        HORIZONTAL_ALIGNS = {
844            sketchingpy.const.LEFT: 'l',
845            sketchingpy.const.CENTER: 'm',
846            sketchingpy.const.RIGHT: 'r'
847        }
848
849        VERTICAL_ALIGNS = {
850            sketchingpy.const.TOP: 't',
851            sketchingpy.const.CENTER: 'm',
852            sketchingpy.const.BASELINE: 's',
853            sketchingpy.const.BOTTOM: 'b'
854        }
855
856        return sketchingpy.state_struct.TextAlign(
857            HORIZONTAL_ALIGNS[text_align.get_horizontal_align()],
858            VERTICAL_ALIGNS[text_align.get_vertical_align()]
859        )

Implementation of SketchStateMachine for Pygame types.

PygameSketchStateMachine()
801    def __init__(self):
802        """Create a new state machine for Pygame-based sketches."""
803        super().__init__()
804        self._fill_native = pygame.Color(super().get_fill())
805        self._stroke_native = pygame.Color(super().get_stroke())
806        self._font_cache = {}
807        self._text_align_native = self._transform_text_align(super().get_text_align_native())

Create a new state machine for Pygame-based sketches.

def set_fill(self, fill: str):
809    def set_fill(self, fill: str):
810        super().set_fill(fill)
811        self._fill_native = pygame.Color(super().get_fill())

Set the fill color.

Set the color to use for filling shapes and figures.

Arguments:
  • fill: Name of the color or a hex code.
def get_fill_native(self):
813    def get_fill_native(self):
814        return self._fill_native

Get the renderer-native version of the fill color.

Returns:

Renderer-specific value. Undefined if get_fill_enabled() is False.

def set_stroke(self, stroke: str):
816    def set_stroke(self, stroke: str):
817        super().set_stroke(stroke)
818        self._stroke_native = pygame.Color(super().get_stroke())

Set the stroke color.

Set the color to use for drawing outlines for shapes and figures as well as lines.

Arguments:
  • stroke: Name of the color or a hex code.
def get_stroke_native(self):
820    def get_stroke_native(self):
821        return self._stroke_native

Get the renderer-native version of the stroke color.

Returns:

Renderer-specific value. Undefined if get_stroke_enabled() is False.

def get_text_font_native(self):
823    def get_text_font_native(self):
824        font = self.get_text_font()
825        key = '%s.%d' % (font.get_identifier(), font.get_size())
826
827        if key not in self._font_cache:
828            new_font = PIL.ImageFont.truetype(font.get_identifier(), font.get_size())
829            self._font_cache[key] = new_font
830
831        return self._font_cache[key]

Get the type and size for text drawing.

Returns:

Renderer-specific value.

def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
833    def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
834        super().set_text_align(text_align)
835        self._text_align_native = self._transform_text_align(super().get_text_align_native())

Indicate the alignment to use when drawing text.

Arguments:
  • text_align: Structure describing horizontal and vertical text alignment.
def get_text_align_native(self):
837    def get_text_align_native(self):
838        return self._text_align_native

Get the alignment to use when drawing text.

Returns:

Renderer-specific value.

class PygameImage(sketchingpy.abstracted.Image):
862class PygameImage(sketchingpy.abstracted.Image):
863    """Strategy implementation for Pygame images."""
864
865    def __init__(self, src: str):
866        """Create a new image.
867
868        Args:
869            src: Path to the image.
870        """
871        super().__init__(src)
872        self._native = pygame.image.load(self.get_src())
873        self._converted = False
874
875    def get_width(self) -> float:
876        return self._native.get_rect().width
877
878    def get_height(self) -> float:
879        return self._native.get_rect().height
880
881    def resize(self, width: float, height: float):
882        self._native = pygame.transform.scale(self._native, (width, height))
883
884    def get_native(self):
885        if not self._converted:
886            self._native.convert_alpha()
887
888        return self._native
889
890    def get_is_loaded(self):
891        return True

Strategy implementation for Pygame images.

PygameImage(src: str)
865    def __init__(self, src: str):
866        """Create a new image.
867
868        Args:
869            src: Path to the image.
870        """
871        super().__init__(src)
872        self._native = pygame.image.load(self.get_src())
873        self._converted = False

Create a new image.

Arguments:
  • src: Path to the image.
def get_width(self) -> float:
875    def get_width(self) -> float:
876        return self._native.get_rect().width

Get the width of this image in pixels.

Returns:

Horizontal width of this image.

def get_height(self) -> float:
878    def get_height(self) -> float:
879        return self._native.get_rect().height

Get the height of this image in pixels.

Returns:

Vertical height of this image.

def resize(self, width: float, height: float):
881    def resize(self, width: float, height: float):
882        self._native = pygame.transform.scale(self._native, (width, height))

Resize this image by scaling.

Arguments:
  • width: The new desired width of this image in pixels.
  • height: The new desired height of this image in pixels.
def get_native(self):
884    def get_native(self):
885        if not self._converted:
886            self._native.convert_alpha()
887
888        return self._native

Access the underlying native version of this image.

Returns:

Renderer specific native version.

def get_is_loaded(self):
890    def get_is_loaded(self):
891        return True

Determine if this image has finished loading.

Returns:

True if loaded and ready to draw. False otherwise.

class PygameMouse(sketchingpy.control_struct.Mouse):
894class PygameMouse(sketchingpy.control_struct.Mouse):
895    """Strategy implementation for Pygame-based mouse access."""
896
897    def __init__(self):
898        """Create a new mouse strategy using Pygame."""
899        super().__init__()
900        self._press_callback = None
901        self._release_callback = None
902
903    def get_pointer_x(self):
904        return pygame.mouse.get_pos()[0]
905
906    def get_pointer_y(self):
907        return pygame.mouse.get_pos()[1]
908
909    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
910        is_left_pressed = pygame.mouse.get_pressed()[0]
911        is_right_pressed = pygame.mouse.get_pressed()[2]
912        buttons_clicked = []
913
914        if is_left_pressed:
915            buttons_clicked.append(sketchingpy.const.MOUSE_LEFT_BUTTON)
916
917        if is_right_pressed:
918            buttons_clicked.append(sketchingpy.const.MOUSE_RIGHT_BUTTON)
919
920        return map(lambda x: sketchingpy.control_struct.Button(x), buttons_clicked)
921
922    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
923        self._press_callback = callback
924
925    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
926        self._release_callback = callback
927
928    def report_mouse_down(self, event):
929        if self._press_callback is None:
930            return
931
932        if event.button == 1:
933            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
934            self._press_callback(button)
935        elif event.button == 3:
936            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
937            self._press_callback(button)
938
939    def report_mouse_up(self, event):
940        if self._release_callback is None:
941            return
942
943        if event.button == 1:
944            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
945            self._release_callback(button)
946        elif event.button == 3:
947            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
948            self._release_callback(button)

Strategy implementation for Pygame-based mouse access.

PygameMouse()
897    def __init__(self):
898        """Create a new mouse strategy using Pygame."""
899        super().__init__()
900        self._press_callback = None
901        self._release_callback = None

Create a new mouse strategy using Pygame.

def get_pointer_x(self):
903    def get_pointer_x(self):
904        return pygame.mouse.get_pos()[0]

Get the x coordinate of the mouse pointer.

Get the current horizontal coordinate of the mouse pointer or, in the case of a touchscreen, the point of last touch input if available. Defaults to 0 if no mouse events have been seen.

Returns:

The horizontal coordinate of the mouse pointer.

def get_pointer_y(self):
906    def get_pointer_y(self):
907        return pygame.mouse.get_pos()[1]

Get the y coordinate of the mouse pointer.

Get the current vertical coordinate of the mouse pointer or, in the case of a touchscreen, the point of last touch input if available. Defaults to 0 if no mouse events have been seen.

Returns:

The vertical coordinate of the mouse pointer.

def get_buttons_pressed(self) -> Iterable[sketchingpy.control_struct.Button]:
909    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
910        is_left_pressed = pygame.mouse.get_pressed()[0]
911        is_right_pressed = pygame.mouse.get_pressed()[2]
912        buttons_clicked = []
913
914        if is_left_pressed:
915            buttons_clicked.append(sketchingpy.const.MOUSE_LEFT_BUTTON)
916
917        if is_right_pressed:
918            buttons_clicked.append(sketchingpy.const.MOUSE_RIGHT_BUTTON)
919
920        return map(lambda x: sketchingpy.control_struct.Button(x), buttons_clicked)

Information about the mouse buttons currently pressed.

Get information about mouse buttons currently pressed.

Returns:

Collection of buttons currently pressed.

def on_button_press( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Mouse')], NoneType]):
922    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
923        self._press_callback = callback

Callback for when a mouse button is pressed.

Register a callback for when a button is pressed, calling a function with the button and mouse. Will pass two arguments to that callback function: first a Button followed by a Mouse. Will unregister prior callbacks for on_button_press.

Arguments:
  • callback: The function to invoke when a mouse button or equivalent is pressed.
def on_button_release( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Mouse')], NoneType]):
925    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
926        self._release_callback = callback

Callback for when a mouse button is released.

Register a callback for when a button is unpressed, calling a function with the button and mouse. Will pass two arguments to that callback function: first a Button followed by a Mouse. Will unregister prior callbacks for on_button_press.

Arguments:
  • callback: The function to invoke when a mouse button or equivalent is unpressed.
def report_mouse_down(self, event):
928    def report_mouse_down(self, event):
929        if self._press_callback is None:
930            return
931
932        if event.button == 1:
933            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
934            self._press_callback(button)
935        elif event.button == 3:
936            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
937            self._press_callback(button)
def report_mouse_up(self, event):
939    def report_mouse_up(self, event):
940        if self._release_callback is None:
941            return
942
943        if event.button == 1:
944            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
945            self._release_callback(button)
946        elif event.button == 3:
947            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
948            self._release_callback(button)
class PygameKeyboard(sketchingpy.control_struct.Keyboard):
951class PygameKeyboard(sketchingpy.control_struct.Keyboard):
952    """Strategy implementation for Pygame-based keyboard access."""
953
954    def __init__(self):
955        """Create a new keyboard strategy using Pygame."""
956        super().__init__()
957        self._pressed = set()
958        self._press_callback = None
959        self._release_callback = None
960
961    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
962        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)
963
964    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
965        self._press_callback = callback
966
967    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
968        self._release_callback = callback
969
970    def report_key_down(self, event):
971        mapped = sketchingpy.sketch2d_keymap.KEY_MAP.get(event.key, None)
972
973        if mapped is None:
974            return
975
976        self._pressed.add(mapped)
977
978        if self._press_callback is not None:
979            button = sketchingpy.control_struct.Button(mapped)
980            self._press_callback(button)
981
982    def report_key_up(self, event):
983        mapped = sketchingpy.sketch2d_keymap.KEY_MAP.get(event.key, None)
984
985        if mapped is None:
986            return
987
988        self._pressed.remove(mapped)
989
990        if self._release_callback is not None:
991            button = sketchingpy.control_struct.Button(mapped)
992            self._release_callback(button)

Strategy implementation for Pygame-based keyboard access.

PygameKeyboard()
954    def __init__(self):
955        """Create a new keyboard strategy using Pygame."""
956        super().__init__()
957        self._pressed = set()
958        self._press_callback = None
959        self._release_callback = None

Create a new keyboard strategy using Pygame.

def get_keys_pressed(self) -> Iterable[sketchingpy.control_struct.Button]:
961    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
962        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)

Get a list of keys currently pressed.

Get a list of keys as Buttons.

Returns:

Get list of buttons pressed.

def on_key_press( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Keyboard')], NoneType]):
964    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
965        self._press_callback = callback

Callback for when a key is pressed.

Register a callback for when a key is pressed, calling a function with the key and keyboard. Will pass two arguments to that callback function: first a Button followed by a Keyboard. Will unregister prior callbacks for on_key_press.

Arguments:
  • callback: The function to invoke when a key is pressed.
def on_key_release( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Keyboard')], NoneType]):
967    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
968        self._release_callback = callback

Callback for when a key is released.

Register a callback for when a key is unpressed, calling a function with the key and keyboard. Will pass two arguments to that callback function: first a Button followed by a Keyboard. Will unregister prior callbacks for on_key_release.

Arguments:
  • callback: The function to invoke when a key is released.
def report_key_down(self, event):
970    def report_key_down(self, event):
971        mapped = sketchingpy.sketch2d_keymap.KEY_MAP.get(event.key, None)
972
973        if mapped is None:
974            return
975
976        self._pressed.add(mapped)
977
978        if self._press_callback is not None:
979            button = sketchingpy.control_struct.Button(mapped)
980            self._press_callback(button)
def report_key_up(self, event):
982    def report_key_up(self, event):
983        mapped = sketchingpy.sketch2d_keymap.KEY_MAP.get(event.key, None)
984
985        if mapped is None:
986            return
987
988        self._pressed.remove(mapped)
989
990        if self._release_callback is not None:
991            button = sketchingpy.control_struct.Button(mapped)
992            self._release_callback(button)
class AppDialogLayer(sketchingpy.dialog_struct.DialogLayer):
 995class AppDialogLayer(sketchingpy.dialog_struct.DialogLayer):
 996    """Dialog / simple UI layer for local apps."""
 997
 998    def __init__(self, sketch: Sketch2DApp):
 999        """"Initialize tkinter but hide the root window."""
1000        self._sketch = sketch
1001        self._sketch_size = self._sketch._get_window_size()
1002        self._manager = pygame_gui.UIManager(self._sketch_size)
1003        self._callback = None  # type: ignore
1004        self._dialog = None  # type: ignore
1005
1006    def get_manager(self):
1007        return self._manager
1008
1009    def get_dialog(self):
1010        return self._dialog
1011
1012    def report_close(self, event):
1013        if self._callback:
1014            self._callback(event)
1015
1016    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1017        self._dispose_dialog()
1018        self._set_dialog(pygame_gui.windows.UIMessageWindow(
1019            rect=pygame.Rect(
1020                self._sketch_size[0] / 2 - 150,
1021                self._sketch_size[1] / 2 - 150,
1022                300,
1023                300
1024            ),
1025            html_message=message,
1026            manager=self._manager
1027        ))
1028
1029        def outer_callback(event):
1030            if event.type == pygame_gui._constants.UI_BUTTON_PRESSED and callback:
1031                callback()
1032
1033        self._callback = outer_callback  # type: ignore
1034
1035    def show_prompt(self, message: str,
1036        callback: typing.Optional[typing.Callable[[str], None]] = None):
1037        self._set_dialog(sketchingpy.pygame_prompt.PygameGuiPrompt(  # type: ignore
1038            rect=pygame.Rect(
1039                self._sketch_size[0] / 2 - 150,
1040                self._sketch_size[1] / 2 - 150,
1041                300,
1042                300
1043            ),
1044            action_long_desc=message,
1045            manager=self._manager,
1046            window_title='Prompt'
1047        ))
1048
1049        def outer_callback(event):
1050            if event.type == pygame_gui._constants.UI_CONFIRMATION_DIALOG_CONFIRMED:
1051                callback(str(self._dialog.get_text()))
1052
1053        self._callback = outer_callback  # type: ignore
1054
1055    def get_file_save_location(self,
1056        callback: typing.Optional[typing.Callable[[str], None]] = None):
1057        self._dispose_dialog()
1058        self._set_dialog(pygame_gui.windows.ui_file_dialog.UIFileDialog(
1059            rect=pygame.Rect(
1060                self._sketch_size[0] / 2 - 150,
1061                self._sketch_size[1] / 2 - 150,
1062                300,
1063                300
1064            ),
1065            manager=self._manager,
1066            allow_existing_files_only=False,
1067            window_title='Save'
1068        ))
1069        self._callback = self._make_file_dialog_callback(callback)  # type: ignore
1070
1071    def get_file_load_location(self,
1072        callback: typing.Optional[typing.Callable[[str], None]] = None):
1073        self._dispose_dialog()
1074        self._set_dialog(pygame_gui.windows.ui_file_dialog.UIFileDialog(
1075            rect=pygame.Rect(
1076                self._sketch_size[0] / 2 - 150,
1077                self._sketch_size[1] / 2 - 150,
1078                300,
1079                300
1080            ),
1081            manager=self._manager,
1082            allow_existing_files_only=False,
1083            window_title='Load'
1084        ))
1085        self._callback = self._make_file_dialog_callback(callback)  # type: ignore
1086
1087    def _make_file_dialog_callback(self, inner_callback):
1088        def callback(event):
1089            if event.type == pygame_gui.UI_FILE_DIALOG_PATH_PICKED:
1090                inner_callback(str(self._dialog.current_file_path))
1091
1092        return callback
1093
1094    def _dispose_dialog(self):
1095        if self._dialog:
1096            self._dialog.kill()
1097
1098    def _set_dialog(self, new_dialog):
1099        self._dialog = new_dialog  # type: ignore

Dialog / simple UI layer for local apps.

AppDialogLayer(sketch: Sketch2DApp)
 998    def __init__(self, sketch: Sketch2DApp):
 999        """"Initialize tkinter but hide the root window."""
1000        self._sketch = sketch
1001        self._sketch_size = self._sketch._get_window_size()
1002        self._manager = pygame_gui.UIManager(self._sketch_size)
1003        self._callback = None  # type: ignore
1004        self._dialog = None  # type: ignore

"Initialize tkinter but hide the root window.

def get_manager(self):
1006    def get_manager(self):
1007        return self._manager
def get_dialog(self):
1009    def get_dialog(self):
1010        return self._dialog
def report_close(self, event):
1012    def report_close(self, event):
1013        if self._callback:
1014            self._callback(event)
def show_alert( self, message: str, callback: Optional[Callable[[], NoneType]] = None):
1016    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1017        self._dispose_dialog()
1018        self._set_dialog(pygame_gui.windows.UIMessageWindow(
1019            rect=pygame.Rect(
1020                self._sketch_size[0] / 2 - 150,
1021                self._sketch_size[1] / 2 - 150,
1022                300,
1023                300
1024            ),
1025            html_message=message,
1026            manager=self._manager
1027        ))
1028
1029        def outer_callback(event):
1030            if event.type == pygame_gui._constants.UI_BUTTON_PRESSED and callback:
1031                callback()
1032
1033        self._callback = outer_callback  # type: ignore

Show an alert dialog box.

Arguments:
  • callback: Method to invoke when the box closes.
  • message: The string to show the user.
def show_prompt( self, message: str, callback: Optional[Callable[[str], NoneType]] = None):
1035    def show_prompt(self, message: str,
1036        callback: typing.Optional[typing.Callable[[str], None]] = None):
1037        self._set_dialog(sketchingpy.pygame_prompt.PygameGuiPrompt(  # type: ignore
1038            rect=pygame.Rect(
1039                self._sketch_size[0] / 2 - 150,
1040                self._sketch_size[1] / 2 - 150,
1041                300,
1042                300
1043            ),
1044            action_long_desc=message,
1045            manager=self._manager,
1046            window_title='Prompt'
1047        ))
1048
1049        def outer_callback(event):
1050            if event.type == pygame_gui._constants.UI_CONFIRMATION_DIALOG_CONFIRMED:
1051                callback(str(self._dialog.get_text()))
1052
1053        self._callback = outer_callback  # type: ignore

Get a string input from the user.

Arguments:
  • message: The message to display to the user within the dialog.
  • callback: Method to invoke when the box closes with a single string parameter provided by the user. Not invoked if cancelled.
def get_file_save_location(self, callback: Optional[Callable[[str], NoneType]] = None):
1055    def get_file_save_location(self,
1056        callback: typing.Optional[typing.Callable[[str], None]] = None):
1057        self._dispose_dialog()
1058        self._set_dialog(pygame_gui.windows.ui_file_dialog.UIFileDialog(
1059            rect=pygame.Rect(
1060                self._sketch_size[0] / 2 - 150,
1061                self._sketch_size[1] / 2 - 150,
1062                300,
1063                300
1064            ),
1065            manager=self._manager,
1066            allow_existing_files_only=False,
1067            window_title='Save'
1068        ))
1069        self._callback = self._make_file_dialog_callback(callback)  # type: ignore

Get either the filename or full location for saving a file.

Arguments:
  • callback: Method to invoke when the box closes with single string parameter which is the filename or the path selected by the user. Not invoked if cancelled.
def get_file_load_location(self, callback: Optional[Callable[[str], NoneType]] = None):
1071    def get_file_load_location(self,
1072        callback: typing.Optional[typing.Callable[[str], None]] = None):
1073        self._dispose_dialog()
1074        self._set_dialog(pygame_gui.windows.ui_file_dialog.UIFileDialog(
1075            rect=pygame.Rect(
1076                self._sketch_size[0] / 2 - 150,
1077                self._sketch_size[1] / 2 - 150,
1078                300,
1079                300
1080            ),
1081            manager=self._manager,
1082            allow_existing_files_only=False,
1083            window_title='Load'
1084        ))
1085        self._callback = self._make_file_dialog_callback(callback)  # type: ignore

Get either the filename or full location for opening a file.

Arguments:
  • callback: Method to invoke when the box closes with single string parameter which is the filename or the path selected by the user. Not invoked if cancelled.