minilibx-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minilibx-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python wrapper for MiniLibX — the 42 school graphics library
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/yourname/pymlx
|
|
7
|
+
Project-URL: Repository, https://github.com/yourname/pymlx
|
|
8
|
+
Keywords: minilibx,42school,graphics,x11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pymlx
|
|
17
|
+
|
|
18
|
+
A Python wrapper for [MiniLibX](https://github.com/42Paris/minilibx-linux) — the simple X11 graphics library used at 42 school.
|
|
19
|
+
|
|
20
|
+
Lets you open windows, draw shapes, and handle keyboard/mouse events in pure Python.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from pymlx import Mlx, Color
|
|
24
|
+
|
|
25
|
+
with Mlx() as mlx:
|
|
26
|
+
win = mlx.new_window(800, 600, "Hello pymlx")
|
|
27
|
+
img = mlx.new_image(800, 600)
|
|
28
|
+
|
|
29
|
+
img.draw_circle(400, 300, 100, Color.RED)
|
|
30
|
+
img.draw_rect(50, 50, 200, 100, Color.BLUE)
|
|
31
|
+
img.draw_line(0, 0, 800, 600, Color.WHITE)
|
|
32
|
+
|
|
33
|
+
win.put_image(img)
|
|
34
|
+
win.on_close(mlx.loop_end)
|
|
35
|
+
win.on_key(lambda key: mlx.loop_end() if key == 65307 else None)
|
|
36
|
+
|
|
37
|
+
mlx.loop()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Linux with X11
|
|
43
|
+
- Python 3.10+
|
|
44
|
+
- `gcc`, `make`
|
|
45
|
+
- `libX11-dev`, `libXext-dev`
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
sudo apt install gcc make libx11-dev libxext-dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/yourname/pymlx.git
|
|
55
|
+
cd pymlx
|
|
56
|
+
|
|
57
|
+
# Build the C wrapper
|
|
58
|
+
make -C src/pymlx MLX_DIR=src/pymlx/lib/minilibx-linux
|
|
59
|
+
|
|
60
|
+
# Install the package
|
|
61
|
+
pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `Mlx`
|
|
67
|
+
|
|
68
|
+
Main context. Use as a context manager.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
mlx = Mlx()
|
|
72
|
+
mlx.new_window(width, height, title) -> Window
|
|
73
|
+
mlx.new_image(width, height) -> Image
|
|
74
|
+
mlx.destroy_image(image)
|
|
75
|
+
mlx.on_loop(callback) # callback() called every frame
|
|
76
|
+
mlx.loop() # start event loop (blocking)
|
|
77
|
+
mlx.loop_end() # stop event loop
|
|
78
|
+
mlx.destroy()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `Window`
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
win.put_image(image, x=0, y=0) # blit image onto window
|
|
85
|
+
win.pixel_put(x, y, color) # direct pixel (slow)
|
|
86
|
+
win.string_put(x, y, color, text) # draw text
|
|
87
|
+
|
|
88
|
+
win.on_key(callback) # callback(keycode: int)
|
|
89
|
+
win.on_mouse(callback) # callback(button, x, y)
|
|
90
|
+
win.on_close(callback) # called on window close
|
|
91
|
+
win.on_event(event, mask, callback) # raw X11 event
|
|
92
|
+
|
|
93
|
+
win.mouse_hide()
|
|
94
|
+
win.mouse_show()
|
|
95
|
+
win.mouse_pos() -> (x, y)
|
|
96
|
+
win.mouse_move(x, y)
|
|
97
|
+
win.destroy()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `Image`
|
|
101
|
+
|
|
102
|
+
All drawing methods write directly into the pixel buffer — fast enough for real-time rendering.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Pixels
|
|
106
|
+
img.pixel_put(x, y, color)
|
|
107
|
+
img.pixel_get(x, y) -> int
|
|
108
|
+
img.clear(color)
|
|
109
|
+
|
|
110
|
+
# Rectangles
|
|
111
|
+
img.draw_rect(x, y, w, h, color)
|
|
112
|
+
img.draw_rect_outline(x, y, w, h, color, thickness=1)
|
|
113
|
+
|
|
114
|
+
# Lines
|
|
115
|
+
img.draw_line(x0, y0, x1, y1, color)
|
|
116
|
+
img.draw_line_thick(x0, y0, x1, y1, color, thickness=2)
|
|
117
|
+
|
|
118
|
+
# Circles
|
|
119
|
+
img.draw_circle(cx, cy, radius, color)
|
|
120
|
+
img.draw_circle_outline(cx, cy, radius, color, thickness=1)
|
|
121
|
+
|
|
122
|
+
# Triangles
|
|
123
|
+
img.draw_triangle(x0, y0, x1, y1, x2, y2, color)
|
|
124
|
+
img.draw_triangle_outline(x0, y0, x1, y1, x2, y2, color)
|
|
125
|
+
|
|
126
|
+
# Gradients
|
|
127
|
+
img.draw_gradient_h(x, y, w, h, color_left, color_right)
|
|
128
|
+
img.draw_gradient_v(x, y, w, h, color_top, color_bottom)
|
|
129
|
+
|
|
130
|
+
# Polygon outline
|
|
131
|
+
img.draw_polygon([(x, y), ...], color)
|
|
132
|
+
|
|
133
|
+
# Flood fill
|
|
134
|
+
img.flood_fill(x, y, color)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `Color`
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
Color.BLACK, Color.WHITE, Color.RED, Color.GREEN
|
|
141
|
+
Color.BLUE, Color.YELLOW, Color.CYAN, Color.MAGENTA
|
|
142
|
+
Color.GRAY, Color.ORANGE, Color.PINK
|
|
143
|
+
|
|
144
|
+
Color.rgb(r, g, b) -> int # 0–255 each
|
|
145
|
+
Color.to_rgb(color) -> (r, g, b)
|
|
146
|
+
Color.lerp(c1, c2, t) -> int # t in [0.0, 1.0]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `X11Event`
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
X11Event.KEY_PRESS # 2
|
|
153
|
+
X11Event.KEY_RELEASE # 3
|
|
154
|
+
X11Event.BUTTON_PRESS # 4
|
|
155
|
+
X11Event.BUTTON_RELEASE # 5
|
|
156
|
+
X11Event.MOTION_NOTIFY # 6
|
|
157
|
+
X11Event.DESTROY_NOTIFY # 17
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Example — animated gradient
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from pymlx import Mlx, Color
|
|
164
|
+
|
|
165
|
+
W, H = 800, 600
|
|
166
|
+
frame = 0
|
|
167
|
+
|
|
168
|
+
with Mlx() as mlx:
|
|
169
|
+
win = mlx.new_window(W, H, "Gradient")
|
|
170
|
+
img = mlx.new_image(W, H)
|
|
171
|
+
|
|
172
|
+
def render():
|
|
173
|
+
global frame
|
|
174
|
+
t = (frame % 256) / 255
|
|
175
|
+
img.draw_gradient_h(0, 0, W, H,
|
|
176
|
+
Color.lerp(Color.BLUE, Color.RED, t),
|
|
177
|
+
Color.lerp(Color.RED, Color.BLUE, t))
|
|
178
|
+
win.put_image(img)
|
|
179
|
+
frame += 1
|
|
180
|
+
|
|
181
|
+
win.on_close(mlx.loop_end)
|
|
182
|
+
win.on_key(lambda k: mlx.loop_end() if k == 65307 else None)
|
|
183
|
+
mlx.on_loop(render)
|
|
184
|
+
mlx.loop()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pymlx/__init__.py,sha256=fMafE7KmlVWkiPB3ngoh_G-P_pW6sOFEWQrkep2yCSs,113
|
|
2
|
+
pymlx/core.py,sha256=HTylN7jOviBCWM-Zi32thuBuvXnYWEcDWaz19dliqbw,22942
|
|
3
|
+
pymlx/lib/libmlx_wrapper.so,sha256=zTvHOhV_6Ay9pCU4L2_s9QkLLGxUzNi7J5AgR4uCa9w,39008
|
|
4
|
+
minilibx_python-0.1.0.dist-info/METADATA,sha256=dCBOmT3Mod1jLIBOoplwSZTWeUgjQ4OVP4aU37MWtTk,4479
|
|
5
|
+
minilibx_python-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
minilibx_python-0.1.0.dist-info/top_level.txt,sha256=q73JJdYIdIW82JnQHmMqAq3oZ2gy8VDsSjGoCpG_S0A,6
|
|
7
|
+
minilibx_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymlx
|
pymlx/__init__.py
ADDED
pymlx/core.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core.py — Python ctypes wrapper around libmlx_wrapper.so
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import math
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# ─── Load shared library ──────────────────────────────────────────────────────
|
|
10
|
+
_LIB_PATH = Path(__file__).parent / "lib" / "libmlx_wrapper.so"
|
|
11
|
+
try:
|
|
12
|
+
_lib = ctypes.CDLL(str(_LIB_PATH))
|
|
13
|
+
except OSError as e:
|
|
14
|
+
raise RuntimeError(
|
|
15
|
+
f"Cannot load libmlx_wrapper.so: {e}\n"
|
|
16
|
+
"Run `make` in the wrapper directory first."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# ─── C struct: t_img_info ─────────────────────────────────────────────────────
|
|
20
|
+
class _ImgInfo(ctypes.Structure):
|
|
21
|
+
_fields_ = [
|
|
22
|
+
("img", ctypes.c_void_p),
|
|
23
|
+
("addr", ctypes.c_char_p),
|
|
24
|
+
("bits_per_pixel", ctypes.c_int),
|
|
25
|
+
("line_length", ctypes.c_int),
|
|
26
|
+
("endian", ctypes.c_int),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# ─── Callback types ───────────────────────────────────────────────────────────
|
|
30
|
+
KeyCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_void_p)
|
|
31
|
+
MouseCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int,
|
|
32
|
+
ctypes.c_int, ctypes.c_int, ctypes.c_void_p)
|
|
33
|
+
LoopCb = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)
|
|
34
|
+
GenericCb = ctypes.CFUNCTYPE(ctypes.c_int)
|
|
35
|
+
|
|
36
|
+
# ─── Prototypes ───────────────────────────────────────────────────────────────
|
|
37
|
+
def _proto(name, restype, *argtypes):
|
|
38
|
+
fn = getattr(_lib, name)
|
|
39
|
+
fn.restype = restype
|
|
40
|
+
fn.argtypes = list(argtypes)
|
|
41
|
+
return fn
|
|
42
|
+
|
|
43
|
+
_mlx_init = _proto("wrap_mlx_init", ctypes.c_void_p)
|
|
44
|
+
_mlx_new_window = _proto("wrap_mlx_new_window", ctypes.c_void_p,
|
|
45
|
+
ctypes.c_void_p, ctypes.c_int, ctypes.c_int,
|
|
46
|
+
ctypes.c_char_p)
|
|
47
|
+
_mlx_destroy_window = _proto("wrap_mlx_destroy_window", None,
|
|
48
|
+
ctypes.c_void_p, ctypes.c_void_p)
|
|
49
|
+
_mlx_destroy_display = _proto("wrap_mlx_destroy_display", None,
|
|
50
|
+
ctypes.c_void_p)
|
|
51
|
+
_mlx_new_image = _proto("wrap_mlx_new_image", _ImgInfo,
|
|
52
|
+
ctypes.c_void_p, ctypes.c_int, ctypes.c_int)
|
|
53
|
+
_mlx_destroy_image = _proto("wrap_mlx_destroy_image", None,
|
|
54
|
+
ctypes.c_void_p, ctypes.c_void_p)
|
|
55
|
+
_mlx_put_image_to_win = _proto("wrap_mlx_put_image_to_window", None,
|
|
56
|
+
ctypes.c_void_p, ctypes.c_void_p,
|
|
57
|
+
ctypes.c_void_p, ctypes.c_int, ctypes.c_int)
|
|
58
|
+
_img_pixel_put = _proto("wrap_img_pixel_put", None,
|
|
59
|
+
ctypes.POINTER(_ImgInfo),
|
|
60
|
+
ctypes.c_int, ctypes.c_int, ctypes.c_int)
|
|
61
|
+
_img_pixel_get = _proto("wrap_img_pixel_get", ctypes.c_int,
|
|
62
|
+
ctypes.POINTER(_ImgInfo),
|
|
63
|
+
ctypes.c_int, ctypes.c_int)
|
|
64
|
+
_img_clear = _proto("wrap_img_clear", None,
|
|
65
|
+
ctypes.POINTER(_ImgInfo), ctypes.c_int)
|
|
66
|
+
_mlx_pixel_put = _proto("wrap_mlx_pixel_put", None,
|
|
67
|
+
ctypes.c_void_p, ctypes.c_void_p,
|
|
68
|
+
ctypes.c_int, ctypes.c_int, ctypes.c_int)
|
|
69
|
+
_mlx_string_put = _proto("wrap_mlx_string_put", None,
|
|
70
|
+
ctypes.c_void_p, ctypes.c_void_p,
|
|
71
|
+
ctypes.c_int, ctypes.c_int, ctypes.c_int,
|
|
72
|
+
ctypes.c_char_p)
|
|
73
|
+
_mlx_key_hook = _proto("wrap_mlx_key_hook", None,
|
|
74
|
+
ctypes.c_void_p, KeyCb, ctypes.c_void_p)
|
|
75
|
+
_mlx_mouse_hook = _proto("wrap_mlx_mouse_hook", None,
|
|
76
|
+
ctypes.c_void_p, MouseCb, ctypes.c_void_p)
|
|
77
|
+
_mlx_hook = _proto("wrap_mlx_hook", None,
|
|
78
|
+
ctypes.c_void_p, ctypes.c_int, ctypes.c_int,
|
|
79
|
+
GenericCb, ctypes.c_void_p)
|
|
80
|
+
_mlx_loop_hook = _proto("wrap_mlx_loop_hook", None,
|
|
81
|
+
ctypes.c_void_p, LoopCb, ctypes.c_void_p)
|
|
82
|
+
_mlx_loop = _proto("wrap_mlx_loop", None,
|
|
83
|
+
ctypes.c_void_p)
|
|
84
|
+
_mlx_loop_end = _proto("wrap_mlx_loop_end", None,
|
|
85
|
+
ctypes.c_void_p)
|
|
86
|
+
_mlx_mouse_hide = _proto("wrap_mlx_mouse_hide", ctypes.c_int,
|
|
87
|
+
ctypes.c_void_p, ctypes.c_void_p)
|
|
88
|
+
_mlx_mouse_show = _proto("wrap_mlx_mouse_show", ctypes.c_int,
|
|
89
|
+
ctypes.c_void_p, ctypes.c_void_p)
|
|
90
|
+
_mlx_mouse_get_pos = _proto("wrap_mlx_mouse_get_pos", ctypes.c_int,
|
|
91
|
+
ctypes.c_void_p, ctypes.c_void_p,
|
|
92
|
+
ctypes.POINTER(ctypes.c_int),
|
|
93
|
+
ctypes.POINTER(ctypes.c_int))
|
|
94
|
+
_mlx_mouse_move = _proto("wrap_mlx_mouse_move", ctypes.c_int,
|
|
95
|
+
ctypes.c_void_p, ctypes.c_void_p,
|
|
96
|
+
ctypes.c_int, ctypes.c_int)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ─── Color ────────────────────────────────────────────────────────────────────
|
|
100
|
+
class Color:
|
|
101
|
+
"""Color constants and helpers (0x00RRGGBB)."""
|
|
102
|
+
BLACK = 0x000000
|
|
103
|
+
WHITE = 0xFFFFFF
|
|
104
|
+
RED = 0xFF0000
|
|
105
|
+
GREEN = 0x00FF00
|
|
106
|
+
BLUE = 0x0000FF
|
|
107
|
+
YELLOW = 0xFFFF00
|
|
108
|
+
CYAN = 0x00FFFF
|
|
109
|
+
MAGENTA = 0xFF00FF
|
|
110
|
+
GRAY = 0x808080
|
|
111
|
+
ORANGE = 0xFF8000
|
|
112
|
+
PINK = 0xFF69B4
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def rgb(r: int, g: int, b: int) -> int:
|
|
116
|
+
"""Create color from R, G, B components (0–255 each)."""
|
|
117
|
+
return ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def to_rgb(color: int) -> tuple[int, int, int]:
|
|
121
|
+
"""Split color int into (r, g, b) tuple."""
|
|
122
|
+
return ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF)
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def lerp(c1: int, c2: int, t: float) -> int:
|
|
126
|
+
"""Linear interpolation between two colors. t in [0.0, 1.0]."""
|
|
127
|
+
r1, g1, b1 = Color.to_rgb(c1)
|
|
128
|
+
r2, g2, b2 = Color.to_rgb(c2)
|
|
129
|
+
return Color.rgb(
|
|
130
|
+
int(r1 + (r2 - r1) * t),
|
|
131
|
+
int(g1 + (g2 - g1) * t),
|
|
132
|
+
int(b1 + (b2 - b1) * t),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ─── X11 Event codes ──────────────────────────────────────────────────────────
|
|
137
|
+
class X11Event:
|
|
138
|
+
"""Common X11 event codes for mlx_hook."""
|
|
139
|
+
KEY_PRESS = 2
|
|
140
|
+
KEY_RELEASE = 3
|
|
141
|
+
BUTTON_PRESS = 4
|
|
142
|
+
BUTTON_RELEASE = 5
|
|
143
|
+
MOTION_NOTIFY = 6
|
|
144
|
+
DESTROY_NOTIFY = 17
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ─── Image ────────────────────────────────────────────────────────────────────
|
|
148
|
+
class Image:
|
|
149
|
+
"""
|
|
150
|
+
MLX image with direct pixel buffer access and drawing primitives.
|
|
151
|
+
|
|
152
|
+
All drawing methods write directly into the pixel buffer —
|
|
153
|
+
no X11 roundtrip, so they are fast enough for real-time rendering.
|
|
154
|
+
Call window.put_image(img) to display the result.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, info: _ImgInfo, width: int, height: int):
|
|
158
|
+
self._info = info
|
|
159
|
+
self._pinfo = ctypes.pointer(info)
|
|
160
|
+
self.width = width
|
|
161
|
+
self.height = height
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def handle(self) -> ctypes.c_void_p:
|
|
165
|
+
return self._info.img
|
|
166
|
+
|
|
167
|
+
# ── Low-level ─────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def pixel_put(self, x: int, y: int, color: int) -> None:
|
|
170
|
+
"""Write a single pixel. Out-of-bounds coords are silently ignored."""
|
|
171
|
+
if 0 <= x < self.width and 0 <= y < self.height:
|
|
172
|
+
_img_pixel_put(self._pinfo, x, y, color)
|
|
173
|
+
|
|
174
|
+
def pixel_get(self, x: int, y: int) -> int:
|
|
175
|
+
"""Read a single pixel color."""
|
|
176
|
+
if 0 <= x < self.width and 0 <= y < self.height:
|
|
177
|
+
return _img_pixel_get(self._pinfo, x, y)
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
def clear(self, color: int = Color.BLACK) -> None:
|
|
181
|
+
"""Fill the entire image with one color."""
|
|
182
|
+
_img_clear(self._pinfo, color)
|
|
183
|
+
|
|
184
|
+
# ── Rectangles ────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def draw_rect(self, x: int, y: int, w: int, h: int, color: int) -> None:
|
|
187
|
+
"""Draw a filled rectangle."""
|
|
188
|
+
x0 = max(x, 0); y0 = max(y, 0)
|
|
189
|
+
x1 = min(x + w, self.width)
|
|
190
|
+
y1 = min(y + h, self.height)
|
|
191
|
+
for py in range(y0, y1):
|
|
192
|
+
for px in range(x0, x1):
|
|
193
|
+
_img_pixel_put(self._pinfo, px, py, color)
|
|
194
|
+
|
|
195
|
+
def draw_rect_outline(self, x: int, y: int, w: int, h: int,
|
|
196
|
+
color: int, thickness: int = 1) -> None:
|
|
197
|
+
"""Draw a rectangle outline (not filled)."""
|
|
198
|
+
for t in range(thickness):
|
|
199
|
+
self.draw_line(x + t, y + t, x + w - t, y + t, color)
|
|
200
|
+
self.draw_line(x + w - t, y + t, x + w - t, y + h - t, color)
|
|
201
|
+
self.draw_line(x + w - t, y + h - t, x + t, y + h - t, color)
|
|
202
|
+
self.draw_line(x + t, y + h - t, x + t, y + t, color)
|
|
203
|
+
|
|
204
|
+
# ── Lines ─────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def draw_line(self, x0: int, y0: int, x1: int, y1: int,
|
|
207
|
+
color: int) -> None:
|
|
208
|
+
"""Draw a line using Bresenham's algorithm."""
|
|
209
|
+
dx = abs(x1 - x0); sx = 1 if x0 < x1 else -1
|
|
210
|
+
dy = -abs(y1 - y0); sy = 1 if y0 < y1 else -1
|
|
211
|
+
err = dx + dy
|
|
212
|
+
while True:
|
|
213
|
+
self.pixel_put(x0, y0, color)
|
|
214
|
+
if x0 == x1 and y0 == y1:
|
|
215
|
+
break
|
|
216
|
+
e2 = 2 * err
|
|
217
|
+
if e2 >= dy:
|
|
218
|
+
if x0 == x1:
|
|
219
|
+
break
|
|
220
|
+
err += dy
|
|
221
|
+
x0 += sx
|
|
222
|
+
if e2 <= dx:
|
|
223
|
+
if y0 == y1:
|
|
224
|
+
break
|
|
225
|
+
err += dx
|
|
226
|
+
y0 += sy
|
|
227
|
+
|
|
228
|
+
def draw_line_thick(self, x0: int, y0: int, x1: int, y1: int,
|
|
229
|
+
color: int, thickness: int = 2) -> None:
|
|
230
|
+
"""Draw a thick line by drawing multiple offset lines."""
|
|
231
|
+
half = thickness // 2
|
|
232
|
+
dx = x1 - x0; dy = y1 - y0
|
|
233
|
+
length = math.hypot(dx, dy)
|
|
234
|
+
if length == 0:
|
|
235
|
+
self.draw_rect(x0 - half, y0 - half, thickness, thickness, color)
|
|
236
|
+
return
|
|
237
|
+
nx = -dy / length # normal vector
|
|
238
|
+
ny = dx / length
|
|
239
|
+
for i in range(-half, half + 1):
|
|
240
|
+
ox = round(nx * i); oy = round(ny * i)
|
|
241
|
+
self.draw_line(x0 + ox, y0 + oy, x1 + ox, y1 + oy, color)
|
|
242
|
+
|
|
243
|
+
# ── Circles ───────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
def draw_circle(self, cx: int, cy: int, radius: int,
|
|
246
|
+
color: int) -> None:
|
|
247
|
+
"""Draw a filled circle using midpoint algorithm."""
|
|
248
|
+
for y in range(-radius, radius + 1):
|
|
249
|
+
x_max = int(math.sqrt(radius * radius - y * y))
|
|
250
|
+
for x in range(-x_max, x_max + 1):
|
|
251
|
+
self.pixel_put(cx + x, cy + y, color)
|
|
252
|
+
|
|
253
|
+
def draw_circle_outline(self, cx: int, cy: int, radius: int,
|
|
254
|
+
color: int, thickness: int = 1) -> None:
|
|
255
|
+
"""Draw a circle outline."""
|
|
256
|
+
for r in range(radius, radius - thickness, -1):
|
|
257
|
+
if r < 0:
|
|
258
|
+
break
|
|
259
|
+
x = 0; y = r; d = 1 - r
|
|
260
|
+
while x <= y:
|
|
261
|
+
for px, py in [
|
|
262
|
+
(cx+x, cy+y), (cx-x, cy+y), (cx+x, cy-y), (cx-x, cy-y),
|
|
263
|
+
(cx+y, cy+x), (cx-y, cy+x), (cx+y, cy-x), (cx-y, cy-x),
|
|
264
|
+
]:
|
|
265
|
+
self.pixel_put(px, py, color)
|
|
266
|
+
x += 1
|
|
267
|
+
if d < 0:
|
|
268
|
+
d += 2 * x + 1
|
|
269
|
+
else:
|
|
270
|
+
y -= 1; d += 2 * (x - y) + 1
|
|
271
|
+
|
|
272
|
+
# ── Triangles ─────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def draw_triangle(self, x0: int, y0: int, x1: int, y1: int,
|
|
275
|
+
x2: int, y2: int, color: int) -> None:
|
|
276
|
+
"""Draw a filled triangle."""
|
|
277
|
+
# Sort by y
|
|
278
|
+
pts = sorted([(x0, y0), (x1, y1), (x2, y2)], key=lambda p: p[1])
|
|
279
|
+
(ax, ay), (bx, by), (cx, cy) = pts
|
|
280
|
+
|
|
281
|
+
def scanline(y_start, y_end, xa, xb, x_long_start, dx_long, dx_short):
|
|
282
|
+
xl = x_long_start
|
|
283
|
+
xs = xa
|
|
284
|
+
for y in range(y_start, y_end):
|
|
285
|
+
left = min(round(xl), round(xs))
|
|
286
|
+
right = max(round(xl), round(xs))
|
|
287
|
+
for x in range(left, right + 1):
|
|
288
|
+
self.pixel_put(x, y, color)
|
|
289
|
+
xl += dx_long
|
|
290
|
+
xs += dx_short
|
|
291
|
+
|
|
292
|
+
total_h = cy - ay or 1
|
|
293
|
+
# Top half
|
|
294
|
+
if by != ay:
|
|
295
|
+
dx_long = (cx - ax) / total_h
|
|
296
|
+
dx_short = (bx - ax) / (by - ay)
|
|
297
|
+
scanline(ay, by, ax, ax, ax, dx_long, dx_short)
|
|
298
|
+
# Bottom half
|
|
299
|
+
if cy != by:
|
|
300
|
+
dx_long = (cx - ax) / total_h
|
|
301
|
+
dx_short = (cx - bx) / (cy - by)
|
|
302
|
+
x_long_at_b = ax + (cx - ax) * (by - ay) / total_h
|
|
303
|
+
scanline(by, cy, bx, x_long_at_b, x_long_at_b, dx_long, dx_short)
|
|
304
|
+
|
|
305
|
+
def draw_triangle_outline(self, x0: int, y0: int, x1: int, y1: int,
|
|
306
|
+
x2: int, y2: int, color: int) -> None:
|
|
307
|
+
"""Draw a triangle outline."""
|
|
308
|
+
self.draw_line(x0, y0, x1, y1, color)
|
|
309
|
+
self.draw_line(x1, y1, x2, y2, color)
|
|
310
|
+
self.draw_line(x2, y2, x0, y0, color)
|
|
311
|
+
|
|
312
|
+
# ── Gradients ─────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
def draw_gradient_h(self, x: int, y: int, w: int, h: int,
|
|
315
|
+
color_left: int, color_right: int) -> None:
|
|
316
|
+
"""Horizontal gradient rectangle."""
|
|
317
|
+
for px in range(w):
|
|
318
|
+
t = px / (w - 1) if w > 1 else 0
|
|
319
|
+
c = Color.lerp(color_left, color_right, t)
|
|
320
|
+
for py in range(h):
|
|
321
|
+
self.pixel_put(x + px, y + py, c)
|
|
322
|
+
|
|
323
|
+
def draw_gradient_v(self, x: int, y: int, w: int, h: int,
|
|
324
|
+
color_top: int, color_bottom: int) -> None:
|
|
325
|
+
"""Vertical gradient rectangle."""
|
|
326
|
+
for py in range(h):
|
|
327
|
+
t = py / (h - 1) if h > 1 else 0
|
|
328
|
+
c = Color.lerp(color_top, color_bottom, t)
|
|
329
|
+
for px in range(w):
|
|
330
|
+
self.pixel_put(x + px, y + py, c)
|
|
331
|
+
|
|
332
|
+
# ── Polygon ───────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
def draw_polygon(self, points: list[tuple[int, int]],
|
|
335
|
+
color: int) -> None:
|
|
336
|
+
"""Draw a polygon outline from a list of (x, y) points."""
|
|
337
|
+
n = len(points)
|
|
338
|
+
if n < 2:
|
|
339
|
+
return
|
|
340
|
+
for i in range(n):
|
|
341
|
+
x0, y0 = points[i]
|
|
342
|
+
x1, y1 = points[(i + 1) % n]
|
|
343
|
+
self.draw_line(x0, y0, x1, y1, color)
|
|
344
|
+
|
|
345
|
+
# ── Flood fill ────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
def flood_fill(self, x: int, y: int, color: int) -> None:
|
|
348
|
+
"""
|
|
349
|
+
Flood fill starting at (x, y).
|
|
350
|
+
Replaces all connected pixels of the same color with the new color.
|
|
351
|
+
Uses an iterative stack to avoid Python recursion limit.
|
|
352
|
+
"""
|
|
353
|
+
if not (0 <= x < self.width and 0 <= y < self.height):
|
|
354
|
+
return
|
|
355
|
+
target = self.pixel_get(x, y)
|
|
356
|
+
if target == color:
|
|
357
|
+
return
|
|
358
|
+
stack = [(x, y)]
|
|
359
|
+
while stack:
|
|
360
|
+
px, py = stack.pop()
|
|
361
|
+
if not (0 <= px < self.width and 0 <= py < self.height):
|
|
362
|
+
continue
|
|
363
|
+
if self.pixel_get(px, py) != target:
|
|
364
|
+
continue
|
|
365
|
+
self.pixel_put(px, py, color)
|
|
366
|
+
stack.extend([(px+1, py), (px-1, py), (px, py+1), (px, py-1)])
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ─── Window ───────────────────────────────────────────────────────────────────
|
|
370
|
+
class Window:
|
|
371
|
+
"""An MLX window."""
|
|
372
|
+
|
|
373
|
+
def __init__(self, mlx_ptr, win_ptr, width: int, height: int):
|
|
374
|
+
self._mlx = mlx_ptr
|
|
375
|
+
self._win = win_ptr
|
|
376
|
+
self.width = width
|
|
377
|
+
self.height = height
|
|
378
|
+
self._callbacks: list = []
|
|
379
|
+
|
|
380
|
+
def pixel_put(self, x: int, y: int, color: int) -> None:
|
|
381
|
+
"""Direct pixel write. Slow — prefer Image + put_image for animation."""
|
|
382
|
+
_mlx_pixel_put(self._mlx, self._win, x, y, color)
|
|
383
|
+
|
|
384
|
+
def string_put(self, x: int, y: int, color: int, text: str) -> None:
|
|
385
|
+
"""Draw text directly on the window using the built-in mlx font."""
|
|
386
|
+
_mlx_string_put(self._mlx, self._win, x, y, color, text.encode())
|
|
387
|
+
|
|
388
|
+
def put_image(self, image: Image, x: int = 0, y: int = 0) -> None:
|
|
389
|
+
"""Blit an Image onto the window at position (x, y)."""
|
|
390
|
+
_mlx_put_image_to_win(self._mlx, self._win, image.handle, x, y)
|
|
391
|
+
|
|
392
|
+
# ── Hooks ─────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def on_key(self, callback) -> None:
|
|
395
|
+
"""Register key press handler: callback(keycode: int) -> None."""
|
|
396
|
+
def _cb(keycode, _param):
|
|
397
|
+
callback(keycode)
|
|
398
|
+
return 0
|
|
399
|
+
c_cb = KeyCb(_cb)
|
|
400
|
+
self._callbacks.append(c_cb)
|
|
401
|
+
_mlx_key_hook(self._win, c_cb, None)
|
|
402
|
+
|
|
403
|
+
def on_mouse(self, callback) -> None:
|
|
404
|
+
"""Register mouse button handler: callback(button, x, y) -> None."""
|
|
405
|
+
def _cb(button, x, y, _param):
|
|
406
|
+
callback(button, x, y)
|
|
407
|
+
return 0
|
|
408
|
+
c_cb = MouseCb(_cb)
|
|
409
|
+
self._callbacks.append(c_cb)
|
|
410
|
+
_mlx_mouse_hook(self._win, c_cb, None)
|
|
411
|
+
|
|
412
|
+
def on_event(self, event: int, mask: int, callback) -> None:
|
|
413
|
+
"""Register a generic X11 event hook: callback() -> None."""
|
|
414
|
+
def _cb():
|
|
415
|
+
callback()
|
|
416
|
+
return 0
|
|
417
|
+
c_cb = GenericCb(_cb)
|
|
418
|
+
self._callbacks.append(c_cb)
|
|
419
|
+
_mlx_hook(self._win, event, mask, c_cb, None)
|
|
420
|
+
|
|
421
|
+
def on_close(self, callback) -> None:
|
|
422
|
+
"""Called when the window close button is pressed."""
|
|
423
|
+
self.on_event(X11Event.DESTROY_NOTIFY, 0, callback)
|
|
424
|
+
|
|
425
|
+
# ── Mouse control ─────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
def mouse_hide(self) -> None:
|
|
428
|
+
_mlx_mouse_hide(self._mlx, self._win)
|
|
429
|
+
|
|
430
|
+
def mouse_show(self) -> None:
|
|
431
|
+
_mlx_mouse_show(self._mlx, self._win)
|
|
432
|
+
|
|
433
|
+
def mouse_pos(self) -> tuple[int, int]:
|
|
434
|
+
x, y = ctypes.c_int(), ctypes.c_int()
|
|
435
|
+
_mlx_mouse_get_pos(self._mlx, self._win,
|
|
436
|
+
ctypes.byref(x), ctypes.byref(y))
|
|
437
|
+
return x.value, y.value
|
|
438
|
+
|
|
439
|
+
def mouse_move(self, x: int, y: int) -> None:
|
|
440
|
+
_mlx_mouse_move(self._mlx, self._win, x, y)
|
|
441
|
+
|
|
442
|
+
def destroy(self) -> None:
|
|
443
|
+
_mlx_destroy_window(self._mlx, self._win)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ─── Mlx ──────────────────────────────────────────────────────────────────────
|
|
447
|
+
class Mlx:
|
|
448
|
+
"""
|
|
449
|
+
Main MiniLibX context. Use as a context manager for automatic cleanup:
|
|
450
|
+
|
|
451
|
+
with Mlx() as mlx:
|
|
452
|
+
win = mlx.new_window(800, 600, "Title")
|
|
453
|
+
...
|
|
454
|
+
mlx.loop()
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
def __init__(self):
|
|
458
|
+
self._ptr = _mlx_init()
|
|
459
|
+
if not self._ptr:
|
|
460
|
+
raise RuntimeError("mlx_init() failed — is DISPLAY set?")
|
|
461
|
+
self._callbacks: list = []
|
|
462
|
+
|
|
463
|
+
def new_window(self, width: int, height: int, title: str) -> Window:
|
|
464
|
+
ptr = _mlx_new_window(self._ptr, width, height, title.encode())
|
|
465
|
+
if not ptr:
|
|
466
|
+
raise RuntimeError(f"mlx_new_window() failed for '{title}'")
|
|
467
|
+
return Window(self._ptr, ptr, width, height)
|
|
468
|
+
|
|
469
|
+
def new_image(self, width: int, height: int) -> Image:
|
|
470
|
+
info = _mlx_new_image(self._ptr, width, height)
|
|
471
|
+
if not info.img:
|
|
472
|
+
raise RuntimeError("mlx_new_image() failed")
|
|
473
|
+
return Image(info, width, height)
|
|
474
|
+
|
|
475
|
+
def destroy_image(self, image: Image) -> None:
|
|
476
|
+
_mlx_destroy_image(self._ptr, image.handle)
|
|
477
|
+
|
|
478
|
+
def on_loop(self, callback) -> None:
|
|
479
|
+
"""Register a per-frame callback: callback() -> None."""
|
|
480
|
+
def _cb(_param):
|
|
481
|
+
callback()
|
|
482
|
+
return 0
|
|
483
|
+
c_cb = LoopCb(_cb)
|
|
484
|
+
self._callbacks.append(c_cb)
|
|
485
|
+
_mlx_loop_hook(self._ptr, c_cb, None)
|
|
486
|
+
|
|
487
|
+
def loop(self) -> None:
|
|
488
|
+
"""Start the event loop (blocking)."""
|
|
489
|
+
_mlx_loop(self._ptr)
|
|
490
|
+
|
|
491
|
+
def loop_end(self) -> None:
|
|
492
|
+
"""Stop the event loop."""
|
|
493
|
+
_mlx_loop_end(self._ptr)
|
|
494
|
+
|
|
495
|
+
def destroy(self) -> None:
|
|
496
|
+
_mlx_destroy_display(self._ptr)
|
|
497
|
+
|
|
498
|
+
def __enter__(self):
|
|
499
|
+
return self
|
|
500
|
+
|
|
501
|
+
def __exit__(self, *_):
|
|
502
|
+
self.destroy()
|
|
Binary file
|