NinePatch

Play Demo

Generates a test panel texture with distinct corners, edges, and centre, then renders NinePatchRect nodes at various sizes to demonstrate that:

  • Corners maintain their original pixel size

  • Edges stretch in one direction only

  • Centre fills the remaining space

Shows the engine’s in-memory texture API: the texture property on NinePatchRect accepts an RGBA uint8 numpy.ndarray directly, no file I/O required.

Source Code

  1"""NinePatchRect demo -- 9-slice sprite scaling via Draw2D.draw_texture_region().
  2
  3Generates a test panel texture with distinct corners, edges, and centre,
  4then renders NinePatchRect nodes at various sizes to demonstrate that:
  5  - Corners maintain their original pixel size
  6  - Edges stretch in one direction only
  7  - Centre fills the remaining space
  8
  9Shows the engine's in-memory texture API: the ``texture`` property on
 10NinePatchRect accepts an RGBA uint8 ``numpy.ndarray`` directly, no file
 11I/O required.
 12"""
 13
 14
 15import sys
 16
 17import numpy as np
 18
 19from simvx.core import NinePatchRect, Node2D, Text2D
 20from simvx.core.math.types import Vec2
 21from simvx.graphics import App
 22
 23
 24def _make_ninepatch_panel(size: int = 64, margin: int = 16) -> np.ndarray:
 25    """Generate a panel texture with visually distinct 9-slice regions.
 26
 27    Corners are bright red, edges are green (horizontal) / blue (vertical),
 28    and the centre is a dark grey. A 1px border outlines the whole texture.
 29    """
 30    img = np.zeros((size, size, 4), dtype=np.uint8)
 31
 32    for y in range(size):
 33        for x in range(size):
 34            in_left = x < margin
 35            in_right = x >= size - margin
 36            in_top = y < margin
 37            in_bottom = y >= size - margin
 38
 39            if (in_top or in_bottom) and (in_left or in_right):
 40                # Corners -- bright red/orange
 41                img[y, x] = [220, 80, 60, 255]
 42            elif in_top or in_bottom:
 43                # Horizontal edges -- green
 44                img[y, x] = [60, 180, 80, 255]
 45            elif in_left or in_right:
 46                # Vertical edges -- blue
 47                img[y, x] = [60, 100, 220, 255]
 48            else:
 49                # Centre -- dark grey
 50                img[y, x] = [80, 80, 90, 255]
 51
 52    # 1px border
 53    img[0, :] = [255, 255, 255, 255]
 54    img[-1, :] = [255, 255, 255, 255]
 55    img[:, 0] = [255, 255, 255, 255]
 56    img[:, -1] = [255, 255, 255, 255]
 57    return img
 58
 59
 60# ---------------------------------------------------------------------------
 61# Scene
 62# ---------------------------------------------------------------------------
 63
 64class NinePatchScene(Node2D):
 65    """Root scene displaying NinePatchRect nodes at different sizes."""
 66
 67    def ready(self):
 68        margin = 16
 69
 70        # Generate the panel pixels in memory and hand the ndarray directly to
 71        # NinePatchRect.texture — the renderer uploads it via
 72        # TextureManager.resolve() / load_from_array().
 73        panel = _make_ninepatch_panel(64, margin)
 74
 75        # Small -- just larger than the margins
 76        self.add_child(NinePatchRect(
 77            texture=panel,
 78            size=(80, 60),
 79            patch_margin_left=margin, patch_margin_right=margin,
 80            patch_margin_top=margin, patch_margin_bottom=margin,
 81            position=Vec2(40, 60), name="Small",
 82        ))
 83
 84        # Medium -- typical button/panel size
 85        self.add_child(NinePatchRect(
 86            texture=panel,
 87            size=(250, 100),
 88            patch_margin_left=margin, patch_margin_right=margin,
 89            patch_margin_top=margin, patch_margin_bottom=margin,
 90            position=Vec2(40, 160), name="Medium",
 91        ))
 92
 93        # Large -- wide dialogue box
 94        self.add_child(NinePatchRect(
 95            texture=panel,
 96            size=(500, 200),
 97            patch_margin_left=margin, patch_margin_right=margin,
 98            patch_margin_top=margin, patch_margin_bottom=margin,
 99            position=Vec2(40, 300), name="Large",
100        ))
101
102        # Tall narrow panel
103        self.add_child(NinePatchRect(
104            texture=panel,
105            size=(80, 250),
106            patch_margin_left=margin, patch_margin_right=margin,
107            patch_margin_top=margin, patch_margin_bottom=margin,
108            position=Vec2(580, 60), name="Tall",
109        ))
110
111        # Labels
112        self.add_child(Text2D(text="NinePatchRect Demo -- 9-Slice Scaling", x=10, y=10, font_scale=1.5, name="Title"))
113        self.add_child(Text2D(text="Small (80x60)", x=140, y=75, name="LabelSmall"))
114        self.add_child(Text2D(text="Medium (250x100)", x=300, y=195, name="LabelMed"))
115        self.add_child(Text2D(text="Large (500x200)", x=300, y=385, name="LabelLarge"))
116        self.add_child(Text2D(text="Tall (80x250)", x=580, y=330, name="LabelTall"))
117
118
119if __name__ == "__main__":
120    test_mode = "--test" in sys.argv
121    app = App(width=900, height=600, title="SimVX NinePatchRect Demo", visible=not test_mode)
122    scene = NinePatchScene()
123
124    if test_mode:
125        frames = app.run_headless(scene, frames=5, capture_frames=[4])
126        if frames and frames[0].max() > 0:
127            print(f"PASS: frame {frames[0].shape} is not blank")
128        else:
129            print("FAIL: frame is blank or missing")
130    else:
131        app.run(scene)