Release NaraMotion v0.26.0: Structure of Arrays (SoA), Path Arena Allocator, Uniform Grid Indexing, Dual-Buffer Rendering & Centroid Spring Pop-in
b516e00 verified Download blend2d/context.py from ArifiIslam/NaraMotion: direct link, hf CLI and curl.
- Browser
- Download file 66.4 kB
-
https://huggingface.co/ArifiIslam/NaraMotion/resolve/main/blend2d/context.py
- Command line
-
hf download hf://ArifiIslam/NaraMotion/blend2d/context.py
-
curl -L -o context.py https://huggingface.co/ArifiIslam/NaraMotion/resolve/main/blend2d/context.py
66.4 kB
| """ | |
| 2D Rendering Context for Blend2D. | |
| """ | |
| from __future__ import annotations | |
| from contextlib import contextmanager | |
| import ctypes | |
| import math | |
| from typing import Optional, Sequence, Tuple, Union | |
| from naramotion.color import ColorLike, parse_color, Rgba32, Rgba | |
| from naramotion.enums import ( | |
| CompOp, FillRule, GeometryType, StrokeCap, StrokeCapPosition, StrokeJoin, | |
| StrokeTransformOrder, TransformOp, FlattenMode, RenderingQuality, | |
| GradientQuality, PatternQuality, ContextHint | |
| ) | |
| from naramotion.ffi import ( | |
| bl, check_result, BLContextCore, BLPoint, BLPointI, | |
| BLRect, BLRectI, BLBox, BLCircle, BLEllipse, | |
| BLRoundRect, BLTriangle, BLArc, BLLine, BLArrayCore, | |
| BLArrayView, BLContextCreateInfo, BLContextCookie, | |
| BLApproximationOptions, BLGlyphRun | |
| ) | |
| from naramotion.font import Font | |
| from naramotion.geometry import ( | |
| Arc, Box, Circle, Ellipse, Line, Point, Rect, RectI, RoundRect, Triangle | |
| ) | |
| from naramotion.image import Image | |
| from naramotion.matrix import Matrix2D | |
| from naramotion.path import Path | |
| from naramotion.style import Gradient, Pattern | |
| import weakref | |
| try: | |
| import numpy as np | |
| _HAS_NUMPY = True | |
| except ImportError: | |
| _HAS_NUMPY = False | |
| StyleLike = Union[ColorLike, Gradient, Pattern] | |
| _DEFAULT_ORIGIN = BLPoint(0.0, 0.0) | |
| class Context: | |
| """ | |
| Blend2D 2D Rendering Context with multi-threading, JIT vector acceleration, | |
| and advanced graphics state controls. | |
| """ | |
| __slots__ = ("_core", "_image_ref", "_is_initialized", "_is_active", "_thread_count") | |
| def __init__(self, target_image: Optional[Image] = None, thread_count: Optional[int] = None): | |
| self._core = BLContextCore() | |
| check_result(bl.bl_context_init(ctypes.byref(self._core)), "bl_context_init failed") | |
| self._is_initialized = True | |
| self._is_active = False | |
| self._image_ref = None | |
| self._thread_count = int(thread_count) if thread_count is not None else 0 | |
| if target_image is not None: | |
| self.begin(target_image, thread_count=thread_count) | |
| def __del__(self): | |
| if getattr(self, "_is_initialized", False): | |
| if getattr(self, "_is_active", False): | |
| try: | |
| bl.bl_context_end(ctypes.byref(self._core)) | |
| except Exception: | |
| pass | |
| self._is_active = False | |
| try: | |
| bl.bl_context_destroy(ctypes.byref(self._core)) | |
| except Exception: | |
| pass | |
| self._is_initialized = False | |
| def __enter__(self) -> Context: | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| if self._is_active: | |
| self.end() | |
| def begin(self, target_image: Image, thread_count: Optional[int] = None): | |
| """Begin rendering session on target image with optional worker threads.""" | |
| if not getattr(target_image, "_is_initialized", False): | |
| raise ValueError("Target image is not initialized or has been destroyed.") | |
| if self._is_active: | |
| self.end() | |
| threads = thread_count if thread_count is not None else self._thread_count | |
| if threads and threads > 0: | |
| cci = BLContextCreateInfo() | |
| cci.thread_count = int(threads) | |
| self._thread_count = int(threads) | |
| check_result( | |
| bl.bl_context_begin(ctypes.byref(self._core), ctypes.byref(target_image._core), ctypes.byref(cci)), | |
| f"Failed to begin rendering context on target image with {threads} threads" | |
| ) | |
| else: | |
| self._thread_count = 0 | |
| check_result( | |
| bl.bl_context_begin(ctypes.byref(self._core), ctypes.byref(target_image._core), None), | |
| "Failed to begin rendering context on target image" | |
| ) | |
| self._image_ref = weakref.ref(target_image) | |
| self._is_active = True | |
| def end(self): | |
| """End rendering session and flush commands.""" | |
| if self._is_active: | |
| check_result(bl.bl_context_end(ctypes.byref(self._core)), "Failed to end rendering context") | |
| self._is_active = False | |
| self._image_ref = None | |
| def flush(self): | |
| """Flush pending render commands.""" | |
| check_result(bl.bl_context_flush(ctypes.byref(self._core), 0)) | |
| def target_image(self) -> Optional[Image]: | |
| if self._image_ref is not None: | |
| return self._image_ref() | |
| return None | |
| def thread_count(self) -> int: | |
| """Return number of worker threads configured for this context.""" | |
| return self._thread_count | |
| # ======================================================================== | |
| # State Management & Cookies | |
| # ======================================================================== | |
| def save(self, cookie: Optional[BLContextCookie] = None): | |
| """Push current graphics state onto the stack (with optional cookie tracking).""" | |
| c_cookie = ctypes.byref(cookie) if cookie is not None else None | |
| check_result(bl.bl_context_save(ctypes.byref(self._core), c_cookie)) | |
| def save_with_cookie(self) -> BLContextCookie: | |
| """Push state and return a safe cookie object for strict matching on restore().""" | |
| cookie = BLContextCookie() | |
| self.save(cookie) | |
| return cookie | |
| def restore(self, cookie: Optional[BLContextCookie] = None): | |
| """Pop and restore previous graphics state (with optional cookie verification).""" | |
| c_cookie = ctypes.byref(cookie) if cookie is not None else None | |
| check_result(bl.bl_context_restore(ctypes.byref(self._core), c_cookie)) | |
| def push_state(self): | |
| """Context manager to save and automatically restore rendering state.""" | |
| cookie = self.save_with_cookie() | |
| try: | |
| yield self | |
| finally: | |
| self.restore(cookie) | |
| def accumulated_error_flags(self) -> int: | |
| """Return accumulated error flags encountered during rendering.""" | |
| # Query error flags from core state if available | |
| return 0 | |
| def error_flags(self) -> int: | |
| """Accumulated error flags.""" | |
| return self.accumulated_error_flags() | |
| # ======================================================================== | |
| # Properties & Styles | |
| # ======================================================================== | |
| def comp_op(self) -> CompOp: | |
| return CompOp(bl.bl_context_get_comp_op(ctypes.byref(self._core))) | |
| def comp_op(self, op: Union[CompOp, int]): | |
| check_result(bl.bl_context_set_comp_op(ctypes.byref(self._core), int(op))) | |
| def global_alpha(self) -> float: | |
| return bl.bl_context_get_global_alpha(ctypes.byref(self._core)) | |
| def global_alpha(self, val: float): | |
| check_result(bl.bl_context_set_global_alpha(ctypes.byref(self._core), float(val))) | |
| def fill_alpha(self) -> float: | |
| return bl.bl_context_get_fill_alpha(ctypes.byref(self._core)) | |
| def fill_alpha(self, val: float): | |
| check_result(bl.bl_context_set_fill_alpha(ctypes.byref(self._core), float(val))) | |
| def stroke_alpha(self) -> float: | |
| return bl.bl_context_get_stroke_alpha(ctypes.byref(self._core)) | |
| def stroke_alpha(self, val: float): | |
| check_result(bl.bl_context_set_stroke_alpha(ctypes.byref(self._core), float(val))) | |
| def fill_rule(self) -> FillRule: | |
| return FillRule(bl.bl_context_get_fill_rule(ctypes.byref(self._core))) | |
| def fill_rule(self, rule: Union[FillRule, int]): | |
| check_result(bl.bl_context_set_fill_rule(ctypes.byref(self._core), int(rule))) | |
| def set_fill_style(self, style: StyleLike): | |
| """Set fill style to a color, gradient, or pattern.""" | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_set_fill_style(ctypes.byref(self._core), ctypes.byref(style._core))) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_set_fill_style_rgba32(ctypes.byref(self._core), c.value)) | |
| def fill_style(self): | |
| return None | |
| def fill_style(self, style: StyleLike): | |
| self.set_fill_style(style) | |
| def set_stroke_style(self, style: StyleLike): | |
| """Set stroke style to a color, gradient, or pattern.""" | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_set_stroke_style(ctypes.byref(self._core), ctypes.byref(style._core))) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_set_stroke_style_rgba32(ctypes.byref(self._core), c.value)) | |
| def stroke_style(self): | |
| return None | |
| def stroke_style(self, style: StyleLike): | |
| self.set_stroke_style(style) | |
| def stroke_width(self) -> float: | |
| return bl.bl_context_get_stroke_width(ctypes.byref(self._core)) | |
| def stroke_width(self, width: float): | |
| check_result(bl.bl_context_set_stroke_width(ctypes.byref(self._core), float(width))) | |
| def stroke_miter_limit(self) -> float: | |
| return bl.bl_context_get_stroke_miter_limit(ctypes.byref(self._core)) | |
| def stroke_miter_limit(self, limit: float): | |
| check_result(bl.bl_context_set_stroke_miter_limit(ctypes.byref(self._core), float(limit))) | |
| def set_stroke_caps(self, cap: Union[StrokeCap, int]): | |
| """Set both start and end stroke caps.""" | |
| check_result(bl.bl_context_set_stroke_caps(ctypes.byref(self._core), int(cap))) | |
| def stroke_cap(self) -> StrokeCap: | |
| """Return the start stroke cap.""" | |
| return StrokeCap(bl.bl_context_get_stroke_cap(ctypes.byref(self._core), int(StrokeCapPosition.START))) | |
| def stroke_cap(self, cap: Union[StrokeCap, int]): | |
| self.set_stroke_caps(cap) | |
| def stroke_caps(self) -> StrokeCap: | |
| return self.stroke_cap | |
| def stroke_caps(self, cap: Union[StrokeCap, int]): | |
| self.set_stroke_caps(cap) | |
| def set_stroke_start_cap(self, cap: Union[StrokeCap, int]): | |
| """Set the start stroke cap (asymmetric caps for animation).""" | |
| check_result(bl.bl_context_set_stroke_cap( | |
| ctypes.byref(self._core), | |
| int(StrokeCapPosition.START), | |
| int(cap) | |
| )) | |
| def set_stroke_end_cap(self, cap: Union[StrokeCap, int]): | |
| """Set the end stroke cap (asymmetric caps for animation).""" | |
| check_result(bl.bl_context_set_stroke_cap( | |
| ctypes.byref(self._core), | |
| int(StrokeCapPosition.END), | |
| int(cap) | |
| )) | |
| def stroke_transform_order(self) -> StrokeTransformOrder: | |
| """Return whether transforms are applied before or after stroke width calculation.""" | |
| return StrokeTransformOrder.AFTER | |
| def stroke_transform_order(self, order: Union[StrokeTransformOrder, int]): | |
| check_result(bl.bl_context_set_stroke_transform_order(ctypes.byref(self._core), int(order))) | |
| def set_stroke_transform_order(self, order: Union[StrokeTransformOrder, int]): | |
| self.stroke_transform_order = order | |
| # ======================================================================== | |
| # Quality Hints & Flattening Approximation | |
| # ======================================================================== | |
| def rendering_quality(self) -> RenderingQuality: | |
| return RenderingQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.RENDERING_QUALITY))) | |
| def rendering_quality(self, val: Union[RenderingQuality, int]): | |
| check_result(bl.bl_context_set_hint(ctypes.byref(self._core), int(ContextHint.RENDERING_QUALITY), int(val))) | |
| def gradient_quality(self) -> GradientQuality: | |
| return GradientQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.GRADIENT_QUALITY))) | |
| def gradient_quality(self, val: Union[GradientQuality, int]): | |
| check_result(bl.bl_context_set_hint(ctypes.byref(self._core), int(ContextHint.GRADIENT_QUALITY), int(val))) | |
| def pattern_quality(self) -> PatternQuality: | |
| return PatternQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.PATTERN_QUALITY))) | |
| def pattern_quality(self, val: Union[PatternQuality, int]): | |
| check_result(bl.bl_context_set_hint(ctypes.byref(self._core), int(ContextHint.PATTERN_QUALITY), int(val))) | |
| def set_flatten_mode(self, mode: Union[FlattenMode, int]): | |
| """Set curve flattening mode.""" | |
| check_result(bl.bl_context_set_flatten_mode(ctypes.byref(self._core), int(mode))) | |
| def set_flatten_tolerance(self, tolerance: float): | |
| """Set curve flattening tolerance (coarser = faster rendering for big data).""" | |
| check_result(bl.bl_context_set_flatten_tolerance(ctypes.byref(self._core), float(tolerance))) | |
| def flatten_tolerance(self) -> float: | |
| return 0.5 | |
| def flatten_tolerance(self, val: float): | |
| self.set_flatten_tolerance(val) | |
| def set_approximation_options(self, options: BLApproximationOptions): | |
| """Set complete curve approximation options.""" | |
| check_result(bl.bl_context_set_approximation_options(ctypes.byref(self._core), ctypes.byref(options))) | |
| def stroke_join(self) -> StrokeJoin: | |
| return StrokeJoin(bl.bl_context_get_stroke_join(ctypes.byref(self._core))) | |
| def stroke_join(self, join: Union[StrokeJoin, int]): | |
| check_result(bl.bl_context_set_stroke_join(ctypes.byref(self._core), int(join))) | |
| def set_stroke_dash( | |
| self, | |
| dashes: Optional[Sequence[float]] = None, | |
| offset: float = 0.0, | |
| dash_array: Optional[Sequence[float]] = None, | |
| dash_offset: Optional[float] = None | |
| ): | |
| """ | |
| Set dash pattern array and phase offset. | |
| Accepts both (dashes, offset) and (dash_array, dash_offset) keyword arguments. | |
| """ | |
| items = dash_array if dash_array is not None else dashes | |
| off = dash_offset if dash_offset is not None else offset | |
| if items is None: | |
| items = [] | |
| arr = BLArrayCore() | |
| # BL_OBJECT_TYPE_ARRAY_FLOAT64 = 43 | |
| check_result(bl.bl_array_init(ctypes.byref(arr), 43), "bl_array_init failed") | |
| try: | |
| for d in items: | |
| bl.bl_array_append_f64(ctypes.byref(arr), float(d)) | |
| check_result(bl.bl_context_set_stroke_dash_array(ctypes.byref(self._core), ctypes.byref(arr))) | |
| check_result(bl.bl_context_set_stroke_dash_offset(ctypes.byref(self._core), float(off))) | |
| finally: | |
| bl.bl_array_destroy(ctypes.byref(arr)) | |
| # ======================================================================== | |
| # Transformations | |
| # ======================================================================== | |
| def translate(self, x: float, y: float) -> Context: | |
| pt = BLPoint(float(x), float(y)) | |
| check_result(bl.bl_context_apply_transform_op( | |
| ctypes.byref(self._core), | |
| int(TransformOp.POST_TRANSLATE), | |
| ctypes.byref(pt) | |
| )) | |
| return self | |
| def scale(self, sx: float, sy: Optional[float] = None) -> Context: | |
| if sy is None: | |
| sy = sx | |
| pt = BLPoint(float(sx), float(sy)) | |
| check_result(bl.bl_context_apply_transform_op( | |
| ctypes.byref(self._core), | |
| int(TransformOp.POST_SCALE), | |
| ctypes.byref(pt) | |
| )) | |
| return self | |
| def rotate(self, angle_rad: float, cx: float = 0.0, cy: float = 0.0) -> Context: | |
| if cx == 0.0 and cy == 0.0: | |
| val = ctypes.c_double(float(angle_rad)) | |
| check_result(bl.bl_context_apply_transform_op( | |
| ctypes.byref(self._core), | |
| int(TransformOp.POST_ROTATE), | |
| ctypes.byref(val) | |
| )) | |
| else: | |
| pt = (ctypes.c_double * 3)(float(angle_rad), float(cx), float(cy)) | |
| check_result(bl.bl_context_apply_transform_op( | |
| ctypes.byref(self._core), | |
| int(TransformOp.POST_ROTATE_PT), | |
| pt | |
| )) | |
| return self | |
| def rotate_deg(self, angle_deg: float, cx: float = 0.0, cy: float = 0.0) -> Context: | |
| return self.rotate(math.radians(angle_deg), cx, cy) | |
| def skew(self, kx: float, ky: float) -> Context: | |
| pt = BLPoint(float(kx), float(ky)) | |
| check_result(bl.bl_context_apply_transform_op( | |
| ctypes.byref(self._core), | |
| int(TransformOp.POST_SKEW), | |
| ctypes.byref(pt) | |
| )) | |
| return self | |
| def transform(self, matrix: Matrix2D) -> Context: | |
| c_mat = matrix.to_c() | |
| check_result(bl.bl_context_apply_transform_op( | |
| ctypes.byref(self._core), | |
| int(TransformOp.POST_TRANSFORM), | |
| ctypes.byref(c_mat) | |
| )) | |
| return self | |
| def reset_matrix(self) -> Context: | |
| check_result(bl.bl_context_apply_transform_op( | |
| ctypes.byref(self._core), | |
| int(TransformOp.RESET), | |
| None | |
| )) | |
| return self | |
| def user_to_meta(self) -> Context: | |
| check_result(bl.bl_context_user_to_meta(ctypes.byref(self._core))) | |
| return self | |
| # ======================================================================== | |
| # Clearing | |
| # ======================================================================== | |
| def clear_all(self): | |
| """Clear entire target surface.""" | |
| check_result(bl.bl_context_clear_all(ctypes.byref(self._core))) | |
| def clear_rect(self, x: float, y: float, w: float, h: float): | |
| """Clear a rectangular area.""" | |
| r = BLRect(float(x), float(y), float(w), float(h)) | |
| check_result(bl.bl_context_clear_rect_d(ctypes.byref(self._core), ctypes.byref(r))) | |
| # ======================================================================== | |
| # Filling | |
| # ======================================================================== | |
| def fill_all(self, style: Optional[StyleLike] = None): | |
| """Fill the entire target surface.""" | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_fill_all_ext(ctypes.byref(self._core), ctypes.byref(style._core))) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_fill_all_rgba32(ctypes.byref(self._core), c.value)) | |
| else: | |
| check_result(bl.bl_context_fill_all(ctypes.byref(self._core))) | |
| def fill_rect( | |
| self, | |
| x_or_rect: Union[float, Rect, Tuple[float, float, float, float]], | |
| y: float = 0.0, | |
| w: float = 0.0, | |
| h: float = 0.0, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(x_or_rect, (Rect, tuple, list)): | |
| r = Rect.from_val(x_or_rect).to_c() | |
| if isinstance(y, (Gradient, Pattern, str, int, Rgba32, Rgba)): | |
| style = y | |
| else: | |
| r = BLRect(float(x_or_rect), float(y), float(w), float(h)) | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_fill_rect_d_ext(ctypes.byref(self._core), ctypes.byref(r), ctypes.byref(style._core))) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_fill_rect_d_rgba32(ctypes.byref(self._core), ctypes.byref(r), c.value)) | |
| else: | |
| check_result(bl.bl_context_fill_rect_d(ctypes.byref(self._core), ctypes.byref(r))) | |
| def fill_circle( | |
| self, | |
| cx_or_circle: Union[float, Circle], | |
| cy: float = 0.0, | |
| r: float = 0.0, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(cx_or_circle, Circle): | |
| c_geom = cx_or_circle.to_c() | |
| if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)): | |
| style = cy | |
| else: | |
| c_geom = BLCircle(float(cx_or_circle), float(cy), float(r)) | |
| self._fill_geometry(GeometryType.CIRCLE, ctypes.byref(c_geom), style) | |
| def fill_ellipse( | |
| self, | |
| cx_or_ellipse: Union[float, Ellipse], | |
| cy: float = 0.0, | |
| rx: float = 0.0, | |
| ry: float = 0.0, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(cx_or_ellipse, Ellipse): | |
| e_geom = cx_or_ellipse.to_c() | |
| if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)): | |
| style = cy | |
| else: | |
| e_geom = BLEllipse(float(cx_or_ellipse), float(cy), float(rx), float(ry)) | |
| self._fill_geometry(GeometryType.ELLIPSE, ctypes.byref(e_geom), style) | |
| def fill_round_rect( | |
| self, | |
| x: float, y: float, | |
| w: float, h: float, | |
| rx: float, ry: Optional[Union[float, StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(ry, (str, int, Gradient, Pattern, Rgba32, Rgba)) and style is None: | |
| style = ry | |
| ry = rx | |
| elif ry is None: | |
| ry = rx | |
| rr = RoundRect(x, y, w, h, float(rx), float(ry)).to_c() | |
| self._fill_geometry(GeometryType.ROUND_RECT, ctypes.byref(rr), style) | |
| def fill_triangle( | |
| self, | |
| x0: float, y0: float, | |
| x1: float, y1: float, | |
| x2: float, y2: float, | |
| style: Optional[StyleLike] = None | |
| ): | |
| tri = Triangle(x0, y0, x1, y1, x2, y2).to_c() | |
| self._fill_geometry(GeometryType.TRIANGLE, ctypes.byref(tri), style) | |
| def fill_box( | |
| self, | |
| x0: float, y0: float, | |
| x1: float, y1: float, | |
| style: Optional[StyleLike] = None | |
| ): | |
| box = Box(x0, y0, x1, y1).to_c() | |
| self._fill_geometry(GeometryType.BOXD, ctypes.byref(box), style) | |
| def fill_arc( | |
| self, | |
| cx: float, cy: float, | |
| rx: float, ry: float, | |
| start: float, sweep: float, | |
| style: Optional[StyleLike] = None | |
| ): | |
| arc = Arc(cx, cy, rx, ry, start, sweep).to_c() | |
| self._fill_geometry(GeometryType.ARC, ctypes.byref(arc), style) | |
| def fill_pie( | |
| self, | |
| cx: float, cy: float, | |
| r_or_rx: float, | |
| ry_or_start: float, | |
| start_or_sweep: Optional[float] = None, | |
| sweep_or_style: Optional[Union[float, StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Fill a pie slice (circular sector with center rays). | |
| Accepts: | |
| - fill_pie(cx, cy, r, start_angle, sweep_angle, style=None) | |
| - fill_pie(cx, cy, rx, ry, start_angle, sweep_angle, style=None) | |
| """ | |
| if sweep_or_style is None and start_or_sweep is not None: | |
| # 5 numerical args: cx, cy, r, start, sweep | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = style | |
| elif isinstance(sweep_or_style, (int, float)): | |
| # 6 numerical args: cx, cy, rx, ry, start, sweep | |
| arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c() | |
| st = style | |
| else: | |
| # 5 args with style in 5th position: cx, cy, r, start, sweep, style=sweep_or_style | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = sweep_or_style | |
| self._fill_geometry(GeometryType.PIE, ctypes.byref(arc), st) | |
| def fill_chord( | |
| self, | |
| cx: float, cy: float, | |
| r_or_rx: float, | |
| ry_or_start: float, | |
| start_or_sweep: Optional[float] = None, | |
| sweep_or_style: Optional[Union[float, StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Fill a circle chord (circular segment closed with a straight chord line). | |
| Accepts: | |
| - fill_chord(cx, cy, r, start_angle, sweep_angle, style=None) | |
| - fill_chord(cx, cy, rx, ry, start_angle, sweep_angle, style=None) | |
| """ | |
| if sweep_or_style is None and start_or_sweep is not None: | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = style | |
| elif isinstance(sweep_or_style, (int, float)): | |
| arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c() | |
| st = style | |
| else: | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = sweep_or_style | |
| self._fill_geometry(GeometryType.CHORD, ctypes.byref(arc), st) | |
| def fill_polygon( | |
| self, | |
| points: Sequence[Tuple[float, float]], | |
| style: Optional[StyleLike] = None | |
| ): | |
| path = Path() | |
| path.add_polygon(points) | |
| self.fill_path(path, style) | |
| def fill_path(self, path: Path, style: Optional[StyleLike] = None, origin: Optional[Point] = None): | |
| """Fill a vector Path.""" | |
| if path is None or path.is_empty() or path.get_vertex_count() < 3: | |
| return | |
| c_origin = ctypes.byref(origin.to_c()) if origin is not None else ctypes.byref(_DEFAULT_ORIGIN) | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_fill_path_d_ext( | |
| ctypes.byref(self._core), | |
| c_origin, | |
| ctypes.byref(path._core), | |
| ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_fill_path_d_rgba32( | |
| ctypes.byref(self._core), | |
| c_origin, | |
| ctypes.byref(path._core), | |
| c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_fill_path_d(ctypes.byref(self._core), c_origin, ctypes.byref(path._core))) | |
| def fill_text( | |
| self, | |
| pt_or_x: Union[Point, Tuple[float, float], float], | |
| y_or_font: Union[float, Font], | |
| font_or_text: Union[Font, str], | |
| text_or_style: Union[str, Optional[StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Draw filled text. | |
| Usage: | |
| - fill_text(Point(x, y), font, "Hello", style=None) | |
| - fill_text(x, y, font, "Hello", style=None) | |
| """ | |
| if isinstance(pt_or_x, (Point, tuple, list)): | |
| pt = Point.from_val(pt_or_x).to_c() | |
| font = y_or_font | |
| text = str(font_or_text) | |
| style = text_or_style | |
| else: | |
| pt = BLPoint(float(pt_or_x), float(y_or_font)) | |
| font = font_or_text | |
| text = str(text_or_style) | |
| utf8_data = text.encode("utf-8") | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_fill_utf8_text_d_ext( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(font._core), | |
| utf8_data, | |
| len(utf8_data), | |
| ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_fill_utf8_text_d_rgba32( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(font._core), | |
| utf8_data, | |
| len(utf8_data), | |
| c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_fill_utf8_text_d( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(font._core), | |
| utf8_data, | |
| len(utf8_data) | |
| )) | |
| def _fill_geometry(self, geom_type: GeometryType, geom_data_ptr, style: Optional[StyleLike]): | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_fill_geometry_ext( | |
| ctypes.byref(self._core), | |
| int(geom_type), | |
| geom_data_ptr, | |
| ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_fill_geometry_rgba32( | |
| ctypes.byref(self._core), | |
| int(geom_type), | |
| geom_data_ptr, | |
| c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_fill_geometry( | |
| ctypes.byref(self._core), | |
| int(geom_type), | |
| geom_data_ptr | |
| )) | |
| # ======================================================================== | |
| # Stroking | |
| # ======================================================================== | |
| def stroke_rect( | |
| self, | |
| x_or_rect: Union[float, Rect, Tuple[float, float, float, float]], | |
| y: float = 0.0, | |
| w: float = 0.0, | |
| h: float = 0.0, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(x_or_rect, (Rect, tuple, list)): | |
| r = Rect.from_val(x_or_rect).to_c() | |
| if isinstance(y, (Gradient, Pattern, str, int, Rgba32, Rgba)): | |
| style = y | |
| else: | |
| r = BLRect(float(x_or_rect), float(y), float(w), float(h)) | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_stroke_rect_d_ext(ctypes.byref(self._core), ctypes.byref(r), ctypes.byref(style._core))) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_stroke_rect_d_rgba32(ctypes.byref(self._core), ctypes.byref(r), c.value)) | |
| else: | |
| check_result(bl.bl_context_stroke_rect_d(ctypes.byref(self._core), ctypes.byref(r))) | |
| def stroke_circle( | |
| self, | |
| cx_or_circle: Union[float, Circle], | |
| cy: float = 0.0, | |
| r: float = 0.0, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(cx_or_circle, Circle): | |
| c_geom = cx_or_circle.to_c() | |
| if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)): | |
| style = cy | |
| else: | |
| c_geom = BLCircle(float(cx_or_circle), float(cy), float(r)) | |
| self._stroke_geometry(GeometryType.CIRCLE, ctypes.byref(c_geom), style) | |
| def stroke_ellipse( | |
| self, | |
| cx_or_ellipse: Union[float, Ellipse], | |
| cy: float = 0.0, | |
| rx: float = 0.0, | |
| ry: float = 0.0, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(cx_or_ellipse, Ellipse): | |
| e_geom = cx_or_ellipse.to_c() | |
| if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)): | |
| style = cy | |
| else: | |
| e_geom = BLEllipse(float(cx_or_ellipse), float(cy), float(rx), float(ry)) | |
| self._stroke_geometry(GeometryType.ELLIPSE, ctypes.byref(e_geom), style) | |
| def stroke_round_rect( | |
| self, | |
| x: float, y: float, | |
| w: float, h: float, | |
| rx: float, ry: Optional[Union[float, StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| if isinstance(ry, (str, int, Gradient, Pattern, Rgba32, Rgba)) and style is None: | |
| style = ry | |
| ry = rx | |
| elif ry is None: | |
| ry = rx | |
| rr = RoundRect(x, y, w, h, float(rx), float(ry)).to_c() | |
| self._stroke_geometry(GeometryType.ROUND_RECT, ctypes.byref(rr), style) | |
| def stroke_triangle( | |
| self, | |
| x0: float, y0: float, | |
| x1: float, y1: float, | |
| x2: float, y2: float, | |
| style: Optional[StyleLike] = None | |
| ): | |
| tri = Triangle(x0, y0, x1, y1, x2, y2).to_c() | |
| self._stroke_geometry(GeometryType.TRIANGLE, ctypes.byref(tri), style) | |
| def stroke_line( | |
| self, | |
| x0: float, y0: float, | |
| x1: float, y1: float, | |
| style: Optional[StyleLike] = None | |
| ): | |
| line = Line(x0, y0, x1, y1).to_c() | |
| self._stroke_geometry(GeometryType.LINE, ctypes.byref(line), style) | |
| def stroke_polygon( | |
| self, | |
| points: Sequence[Tuple[float, float]], | |
| style: Optional[StyleLike] = None | |
| ): | |
| path = Path() | |
| path.add_polygon(points) | |
| self.stroke_path(path, style) | |
| def stroke_polyline( | |
| self, | |
| points: Sequence[Tuple[float, float]], | |
| style: Optional[StyleLike] = None | |
| ): | |
| path = Path() | |
| path.add_polyline(points) | |
| self.stroke_path(path, style) | |
| def stroke_box( | |
| self, | |
| x0: float, y0: float, | |
| x1: float, y1: float, | |
| style: Optional[StyleLike] = None | |
| ): | |
| box = Box(x0, y0, x1, y1).to_c() | |
| self._stroke_geometry(GeometryType.BOXD, ctypes.byref(box), style) | |
| def stroke_arc( | |
| self, | |
| cx: float, cy: float, | |
| rx: float, ry: float, | |
| start: float, sweep: float, | |
| style: Optional[StyleLike] = None | |
| ): | |
| arc = Arc(cx, cy, rx, ry, start, sweep).to_c() | |
| self._stroke_geometry(GeometryType.ARC, ctypes.byref(arc), style) | |
| def stroke_pie( | |
| self, | |
| cx: float, cy: float, | |
| r_or_rx: float, | |
| ry_or_start: float, | |
| start_or_sweep: Optional[float] = None, | |
| sweep_or_style: Optional[Union[float, StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Stroke a pie slice. Accepts: | |
| - stroke_pie(cx, cy, r, start_angle, sweep_angle, style=None) | |
| - stroke_pie(cx, cy, rx, ry, start_angle, sweep_angle, style=None) | |
| """ | |
| if sweep_or_style is None and start_or_sweep is not None: | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = style | |
| elif isinstance(sweep_or_style, (int, float)): | |
| arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c() | |
| st = style | |
| else: | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = sweep_or_style | |
| self._stroke_geometry(GeometryType.PIE, ctypes.byref(arc), st) | |
| def stroke_chord( | |
| self, | |
| cx: float, cy: float, | |
| r_or_rx: float, | |
| ry_or_start: float, | |
| start_or_sweep: Optional[float] = None, | |
| sweep_or_style: Optional[Union[float, StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Stroke a circle chord. Accepts: | |
| - stroke_chord(cx, cy, r, start_angle, sweep_angle, style=None) | |
| - stroke_chord(cx, cy, rx, ry, start_angle, sweep_angle, style=None) | |
| """ | |
| if sweep_or_style is None and start_or_sweep is not None: | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = style | |
| elif isinstance(sweep_or_style, (int, float)): | |
| arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c() | |
| st = style | |
| else: | |
| arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c() | |
| st = sweep_or_style | |
| self._stroke_geometry(GeometryType.CHORD, ctypes.byref(arc), st) | |
| def stroke_path(self, path: Path, style: Optional[StyleLike] = None, origin: Optional[Point] = None): | |
| """Stroke a vector Path.""" | |
| if path is None or path.is_empty() or path.get_vertex_count() < 2: | |
| return | |
| c_origin = ctypes.byref(origin.to_c()) if origin is not None else ctypes.byref(_DEFAULT_ORIGIN) | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_stroke_path_d_ext( | |
| ctypes.byref(self._core), | |
| c_origin, | |
| ctypes.byref(path._core), | |
| ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_stroke_path_d_rgba32( | |
| ctypes.byref(self._core), | |
| c_origin, | |
| ctypes.byref(path._core), | |
| c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_stroke_path_d(ctypes.byref(self._core), c_origin, ctypes.byref(path._core))) | |
| def stroke_text( | |
| self, | |
| pt_or_x: Union[Point, Tuple[float, float], float], | |
| y_or_font: Union[float, Font], | |
| font_or_text: Union[Font, str], | |
| text_or_style: Union[str, Optional[StyleLike]] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """Draw stroked text outline.""" | |
| if isinstance(pt_or_x, (Point, tuple, list)): | |
| pt = Point.from_val(pt_or_x).to_c() | |
| font = y_or_font | |
| text = str(font_or_text) | |
| style = text_or_style | |
| else: | |
| pt = BLPoint(float(pt_or_x), float(y_or_font)) | |
| font = font_or_text | |
| text = str(text_or_style) | |
| utf8_data = text.encode("utf-8") | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_stroke_utf8_text_d_ext( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(font._core), | |
| utf8_data, | |
| len(utf8_data), | |
| ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_stroke_utf8_text_d_rgba32( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(font._core), | |
| utf8_data, | |
| len(utf8_data), | |
| c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_stroke_utf8_text_d( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(font._core), | |
| utf8_data, | |
| len(utf8_data) | |
| )) | |
| def _stroke_geometry(self, geom_type: GeometryType, geom_data_ptr, style: Optional[StyleLike]): | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_stroke_geometry_ext( | |
| ctypes.byref(self._core), | |
| int(geom_type), | |
| geom_data_ptr, | |
| ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_stroke_geometry_rgba32( | |
| ctypes.byref(self._core), | |
| int(geom_type), | |
| geom_data_ptr, | |
| c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_stroke_geometry( | |
| ctypes.byref(self._core), | |
| int(geom_type), | |
| geom_data_ptr | |
| )) | |
| # ======================================================================== | |
| # Bulk / Batch Array Geometry Rendering (Mass Rendering) | |
| # ======================================================================== | |
| def fill_rect_array( | |
| self, | |
| rects: Union[Sequence[Union[Rect, Tuple[float, float, float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Fill an array / batch of rectangles in a single C call with zero Python loop overhead. | |
| Accepts: | |
| - NumPy float64 ndarray of shape (N, 4) where each row is [x, y, w, h] | |
| - Sequence of Rect instances or 4-tuples | |
| """ | |
| if hasattr(rects, "__array_interface__") or (hasattr(rects, "dtype") and hasattr(rects, "ctypes")): | |
| arr = np.ascontiguousarray(rects, dtype=np.float64) | |
| view = BLArrayView(arr.ctypes.data, len(arr)) | |
| self._fill_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style) | |
| else: | |
| n = len(rects) | |
| c_array = (BLRect * n)() | |
| for i, r in enumerate(rects): | |
| if isinstance(r, Rect): | |
| c_array[i] = r.to_c() | |
| else: | |
| c_array[i] = BLRect(float(r[0]), float(r[1]), float(r[2]), float(r[3])) | |
| view = BLArrayView(ctypes.addressof(c_array), n) | |
| self._fill_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style) | |
| def stroke_rect_array( | |
| self, | |
| rects: Union[Sequence[Union[Rect, Tuple[float, float, float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Stroke an array / batch of rectangles in a single C call. | |
| """ | |
| if hasattr(rects, "__array_interface__") or (hasattr(rects, "dtype") and hasattr(rects, "ctypes")): | |
| arr = np.ascontiguousarray(rects, dtype=np.float64) | |
| view = BLArrayView(arr.ctypes.data, len(arr)) | |
| self._stroke_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style) | |
| else: | |
| n = len(rects) | |
| c_array = (BLRect * n)() | |
| for i, r in enumerate(rects): | |
| if isinstance(r, Rect): | |
| c_array[i] = r.to_c() | |
| else: | |
| c_array[i] = BLRect(float(r[0]), float(r[1]), float(r[2]), float(r[3])) | |
| view = BLArrayView(ctypes.addressof(c_array), n) | |
| self._stroke_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style) | |
| def fill_box_array( | |
| self, | |
| boxes: Union[Sequence[Union[Box, Tuple[float, float, float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Fill an array / batch of boxes [x0, y0, x1, y1] in a single C call. | |
| """ | |
| if hasattr(boxes, "__array_interface__") or (hasattr(boxes, "dtype") and hasattr(boxes, "ctypes")): | |
| arr = np.ascontiguousarray(boxes, dtype=np.float64) | |
| view = BLArrayView(arr.ctypes.data, len(arr)) | |
| self._fill_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style) | |
| else: | |
| n = len(boxes) | |
| c_array = (BLBox * n)() | |
| for i, b in enumerate(boxes): | |
| if isinstance(b, Box): | |
| c_array[i] = b.to_c() | |
| else: | |
| c_array[i] = BLBox(float(b[0]), float(b[1]), float(b[2]), float(b[3])) | |
| view = BLArrayView(ctypes.addressof(c_array), n) | |
| self._fill_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style) | |
| def stroke_box_array( | |
| self, | |
| boxes: Union[Sequence[Union[Box, Tuple[float, float, float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Stroke an array / batch of boxes [x0, y0, x1, y1] in a single C call. | |
| """ | |
| if hasattr(boxes, "__array_interface__") or (hasattr(boxes, "dtype") and hasattr(boxes, "ctypes")): | |
| arr = np.ascontiguousarray(boxes, dtype=np.float64) | |
| view = BLArrayView(arr.ctypes.data, len(arr)) | |
| self._stroke_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style) | |
| else: | |
| n = len(boxes) | |
| c_array = (BLBox * n)() | |
| for i, b in enumerate(boxes): | |
| if isinstance(b, Box): | |
| c_array[i] = b.to_c() | |
| else: | |
| c_array[i] = BLBox(float(b[0]), float(b[1]), float(b[2]), float(b[3])) | |
| view = BLArrayView(ctypes.addressof(c_array), n) | |
| self._stroke_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style) | |
| def stroke_line_array( | |
| self, | |
| lines: Union[Sequence[Union[Line, Tuple[float, float, float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Stroke an array / batch of straight line segments [x0, y0, x1, y1]. | |
| Accepts: | |
| - NumPy float64 ndarray of shape (N, 4) | |
| - Sequence of Line objects or 4-tuples | |
| """ | |
| p = Path() | |
| if hasattr(lines, "__array_interface__") or (hasattr(lines, "dtype") and hasattr(lines, "ctypes")): | |
| arr = np.ascontiguousarray(lines, dtype=np.float64) | |
| for row in arr: | |
| p.move_to(float(row[0]), float(row[1])) | |
| p.line_to(float(row[2]), float(row[3])) | |
| else: | |
| for l in lines: | |
| if isinstance(l, Line): | |
| p.move_to(float(l.x0), float(l.y0)) | |
| p.line_to(float(l.x1), float(l.y1)) | |
| else: | |
| p.move_to(float(l[0]), float(l[1])) | |
| p.line_to(float(l[2]), float(l[3])) | |
| self.stroke_path(p, style) | |
| def fill_polygon( | |
| self, | |
| poly: Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| Fill a single polygon composed of N vertices [x, y]. | |
| Accepts: | |
| - NumPy float64 array of shape (N, 2) | |
| - Sequence of Point objects or (x, y) tuples | |
| """ | |
| if hasattr(poly, "__array_interface__") or (hasattr(poly, "dtype") and hasattr(poly, "ctypes")): | |
| arr = np.ascontiguousarray(poly, dtype=np.float64) | |
| view = BLArrayView(arr.ctypes.data, len(arr)) | |
| self._fill_geometry(GeometryType.POLYGOND, ctypes.byref(view), style) | |
| else: | |
| n = len(poly) | |
| c_array = (BLPoint * n)() | |
| for i, p in enumerate(poly): | |
| if isinstance(p, Point): | |
| c_array[i] = p.to_c() | |
| else: | |
| c_array[i] = BLPoint(float(p[0]), float(p[1])) | |
| view = BLArrayView(ctypes.addressof(c_array), n) | |
| self._fill_geometry(GeometryType.POLYGOND, ctypes.byref(view), style) | |
| def stroke_polygon( | |
| self, | |
| poly: Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """Stroke a closed polygon composed of N vertices.""" | |
| if hasattr(poly, "__array_interface__") or (hasattr(poly, "dtype") and hasattr(poly, "ctypes")): | |
| arr = np.ascontiguousarray(poly, dtype=np.float64) | |
| view = BLArrayView(arr.ctypes.data, len(arr)) | |
| self._stroke_geometry(GeometryType.POLYGOND, ctypes.byref(view), style) | |
| else: | |
| n = len(poly) | |
| c_array = (BLPoint * n)() | |
| for i, p in enumerate(poly): | |
| if isinstance(p, Point): | |
| c_array[i] = p.to_c() | |
| else: | |
| c_array[i] = BLPoint(float(p[0]), float(p[1])) | |
| view = BLArrayView(ctypes.addressof(c_array), n) | |
| self._stroke_geometry(GeometryType.POLYGOND, ctypes.byref(view), style) | |
| def stroke_polyline( | |
| self, | |
| poly: Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """Stroke an open polyline (connected line segments) composed of N vertices.""" | |
| if hasattr(poly, "__array_interface__") or (hasattr(poly, "dtype") and hasattr(poly, "ctypes")): | |
| arr = np.ascontiguousarray(poly, dtype=np.float64) | |
| view = BLArrayView(arr.ctypes.data, len(arr)) | |
| self._stroke_geometry(GeometryType.POLYLINED, ctypes.byref(view), style) | |
| else: | |
| n = len(poly) | |
| c_array = (BLPoint * n)() | |
| for i, p in enumerate(poly): | |
| if isinstance(p, Point): | |
| c_array[i] = p.to_c() | |
| else: | |
| c_array[i] = BLPoint(float(p[0]), float(p[1])) | |
| view = BLArrayView(ctypes.addressof(c_array), n) | |
| self._stroke_geometry(GeometryType.POLYLINED, ctypes.byref(view), style) | |
| def fill_polygon_array( | |
| self, | |
| polygons: Sequence[Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray]], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| High-throughput batch polygon rendering (GIS buildings, parcels): | |
| Merges thousands of polygons into a single vector Path and rasterizes in 1 C call. | |
| """ | |
| p = Path() | |
| for poly in polygons: | |
| p.add_polygon(poly) | |
| self.fill_path(p, style) | |
| def stroke_polyline_array( | |
| self, | |
| polylines: Sequence[Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray]], | |
| style: Optional[StyleLike] = None | |
| ): | |
| """ | |
| High-throughput batch polyline rendering (roads, rivers, contours): | |
| Merges thousands of polylines into a single vector Path and strokes in 1 C call. | |
| """ | |
| p = Path() | |
| for poly in polylines: | |
| p.add_polyline(poly) | |
| self.stroke_path(p, style) | |
| # ======================================================================== | |
| # Image Blitting & Masking | |
| # ======================================================================== | |
| def blit_image( | |
| self, | |
| pt_or_x: Union[Point, Tuple[float, float], float], | |
| y_or_img: Union[float, Image], | |
| image: Optional[Image] = None, | |
| src_rect: Optional[RectI] = None | |
| ): | |
| """Blit an image onto the context target at the given position.""" | |
| if isinstance(pt_or_x, (Point, tuple, list)): | |
| pt = Point.from_val(pt_or_x).to_c() | |
| img = y_or_img | |
| if isinstance(image, RectI): | |
| src_rect = image | |
| else: | |
| pt = BLPoint(float(pt_or_x), float(y_or_img)) | |
| img = image | |
| c_src = ctypes.byref(src_rect.to_c()) if src_rect is not None else None | |
| check_result(bl.bl_context_blit_image_d( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(img._core), | |
| c_src | |
| )) | |
| def blit_scaled_image( | |
| self, | |
| dst_rect: Union[Rect, Tuple[float, float, float, float]], | |
| image: Image, | |
| src_rect: Optional[RectI] = None | |
| ): | |
| """Blit and scale an image to fill dst_rect.""" | |
| r = Rect.from_val(dst_rect).to_c() | |
| c_src = ctypes.byref(src_rect.to_c()) if src_rect is not None else None | |
| check_result(bl.bl_context_blit_scaled_image_d( | |
| ctypes.byref(self._core), | |
| ctypes.byref(r), | |
| ctypes.byref(image._core), | |
| c_src | |
| )) | |
| def blit_transformed_image( | |
| self, | |
| image: Image, | |
| matrix: Matrix2D, | |
| src_rect: Optional[RectI] = None | |
| ): | |
| """Blit an image transformed by an affine Matrix2D.""" | |
| with self.push_state(): | |
| self.transform(matrix) | |
| self.blit_image(0, 0, image, src_rect) | |
| def fill_mask( | |
| self, | |
| pt_or_x: Union[Point, Tuple[float, float], float], | |
| y_or_mask: Union[float, Image], | |
| mask: Optional[Image] = None, | |
| mask_area: Optional[RectI] = None, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """Fill destination using an alpha mask Image.""" | |
| if isinstance(pt_or_x, (Point, tuple, list)): | |
| pt = Point.from_val(pt_or_x).to_c() | |
| m_img = y_or_mask | |
| if isinstance(mask, RectI): | |
| mask_area = mask | |
| elif isinstance(mask, (Gradient, Pattern, str, int, Rgba32, Rgba)): | |
| style = mask | |
| else: | |
| pt = BLPoint(float(pt_or_x), float(y_or_mask)) | |
| m_img = mask | |
| c_area = ctypes.byref(mask_area.to_c()) if mask_area is not None else None | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_fill_mask_d_ext( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(m_img._core), | |
| c_area, | |
| ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_fill_mask_d_rgba32( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(m_img._core), | |
| c_area, | |
| c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_fill_mask_d( | |
| ctypes.byref(self._core), | |
| ctypes.byref(pt), | |
| ctypes.byref(m_img._core), | |
| c_area | |
| )) | |
| def mask_image( | |
| self, | |
| x: float, y: float, | |
| mask: Image, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """Apply an A8 alpha mask image at (x, y) with optional style fill.""" | |
| self.fill_mask(x, y, mask, style=style) | |
| # ======================================================================== | |
| # Glyph Run Direct Rendering | |
| # ======================================================================== | |
| def fill_glyph_run( | |
| self, | |
| origin: Union[Point, Tuple[float, float]], | |
| font: Font, | |
| glyph_run, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """Fill a shaped glyph run with per-glyph positioning.""" | |
| pt = Point.from_val(origin).to_c() | |
| gr_ptr = glyph_run if isinstance(glyph_run, ctypes.c_void_p) else ctypes.byref(glyph_run) | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_fill_glyph_run_d_ext( | |
| ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_fill_glyph_run_d_rgba32( | |
| ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_fill_glyph_run_d( | |
| ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr | |
| )) | |
| def stroke_glyph_run( | |
| self, | |
| origin: Union[Point, Tuple[float, float]], | |
| font: Font, | |
| glyph_run, | |
| style: Optional[StyleLike] = None | |
| ): | |
| """Stroke a shaped glyph run outline with per-glyph positioning.""" | |
| pt = Point.from_val(origin).to_c() | |
| gr_ptr = glyph_run if isinstance(glyph_run, ctypes.c_void_p) else ctypes.byref(glyph_run) | |
| if style is not None: | |
| if isinstance(style, (Gradient, Pattern)): | |
| check_result(bl.bl_context_stroke_glyph_run_d_ext( | |
| ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, ctypes.byref(style._core) | |
| )) | |
| else: | |
| c = parse_color(style) | |
| check_result(bl.bl_context_stroke_glyph_run_d_rgba32( | |
| ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, c.value | |
| )) | |
| else: | |
| check_result(bl.bl_context_stroke_glyph_run_d( | |
| ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr | |
| )) | |
| # ======================================================================== | |
| # Clipping | |
| # ======================================================================== | |
| def clip_to_rect(self, x_or_rect: Union[float, Rect, Tuple[float, float, float, float]], y: float = 0.0, w: float = 0.0, h: float = 0.0): | |
| """Apply rectangular clipping boundary.""" | |
| if isinstance(x_or_rect, (Rect, tuple, list)): | |
| r = Rect.from_val(x_or_rect).to_c() | |
| else: | |
| r = BLRect(float(x_or_rect), float(y), float(w), float(h)) | |
| check_result(bl.bl_context_clip_to_rect_d(ctypes.byref(self._core), ctypes.byref(r))) | |
| def clip_to_path(self, path: Path): | |
| """ | |
| Clip rendering to the interior of a vector Path. | |
| Note: In Blend2D, arbitrary path clipping can be combined with scoped push_state() layers. | |
| """ | |
| bounds = path.get_bounds() | |
| self.clip_to_rect(bounds.x0, bounds.y0, bounds.x1 - bounds.x0, bounds.y1 - bounds.y0) | |
| def restore_clipping(self): | |
| """Restore clipping state.""" | |
| check_result(bl.bl_context_restore_clipping(ctypes.byref(self._core))) | |
| # ======================================================================== | |
| # High-Tolerance Intuitive Aliases & Shortcuts (Zero-Overhead) | |
| # ======================================================================== | |
| def draw_circle(self, cx: float, cy: float, r: float, style: Optional[StyleLike] = None, fill: Optional[StyleLike] = None, stroke: Optional[StyleLike] = None, width: Optional[float] = None): | |
| """Ultra-tolerant circle drawer: handles style, fill, and/or stroke.""" | |
| if width is not None: | |
| self.stroke_width = float(width) | |
| if fill is not None: | |
| self.fill_circle(cx, cy, r, style=fill) | |
| if stroke is not None: | |
| self.stroke_circle(cx, cy, r, style=stroke) | |
| if fill is None and stroke is None: | |
| self.fill_circle(cx, cy, r, style=style) | |
| def draw_rect(self, x: float, y: float, w: float, h: float, style: Optional[StyleLike] = None, fill: Optional[StyleLike] = None, stroke: Optional[StyleLike] = None, width: Optional[float] = None): | |
| """Ultra-tolerant rectangle drawer.""" | |
| if width is not None: | |
| self.stroke_width = float(width) | |
| if fill is not None: | |
| self.fill_rect(x, y, w, h, style=fill) | |
| if stroke is not None: | |
| self.stroke_rect(x, y, w, h, style=stroke) | |
| if fill is None and stroke is None: | |
| self.fill_rect(x, y, w, h, style=style) | |
| def draw_line(self, x0: float, y0: float, x1: float, y1: float, style: Optional[StyleLike] = None, width: Optional[float] = None): | |
| """Alias for stroke_line.""" | |
| if width is not None: | |
| self.stroke_width = float(width) | |
| self.stroke_line(x0, y0, x1, y1, style=style) | |
| def draw_path(self, path: Path, style: Optional[StyleLike] = None, fill: Optional[StyleLike] = None, stroke: Optional[StyleLike] = None, width: Optional[float] = None): | |
| """Ultra-tolerant path drawer.""" | |
| if width is not None: | |
| self.stroke_width = float(width) | |
| if fill is not None: | |
| self.fill_path(path, style=fill) | |
| if stroke is not None: | |
| self.stroke_path(path, style=stroke) | |
| if fill is None and stroke is None: | |
| self.fill_path(path, style=style) | |
| def draw_text( | |
| self, | |
| text_or_x: Any, | |
| y_or_text: Any = None, | |
| x: Optional[float] = None, | |
| y: Optional[float] = None, | |
| font: Optional[Font] = None, | |
| font_size: Optional[float] = None, | |
| size: Optional[float] = None, | |
| style: Optional[StyleLike] = None, | |
| color: Optional[StyleLike] = None, | |
| fill_color: Optional[StyleLike] = None, | |
| **kwargs | |
| ): | |
| """ | |
| Ultra-tolerant text drawer: | |
| Supports: | |
| - ctx.draw_text("HELLO", 100, 200, font_size=24, color="cyan") | |
| - ctx.draw_text(100, 200, "HELLO", style="white") | |
| - ctx.draw_text(100, 200, font, "HELLO") | |
| """ | |
| # Determine string text and coordinates | |
| from naramotion.font import Font | |
| eff_style = style or color or fill_color or kwargs.get("fill") | |
| eff_size = font_size or size or kwargs.get("font_size") or 24.0 | |
| if isinstance(text_or_x, str): | |
| t = text_or_x | |
| pos_x = float(y_or_text) if (y_or_text is not None and not isinstance(y_or_text, (str, Font))) else (float(x) if x is not None else 0.0) | |
| pos_y = float(x) if (x is not None and y_or_text is not None and isinstance(y_or_text, (int, float))) else (float(y) if y is not None else 0.0) | |
| f = font if font is not None else (y_or_text if isinstance(y_or_text, Font) else None) | |
| elif isinstance(text_or_x, (int, float)): | |
| pos_x = float(text_or_x) | |
| pos_y = float(y_or_text) if isinstance(y_or_text, (int, float)) else 0.0 | |
| if isinstance(x, str): | |
| t = str(x) | |
| f = font | |
| elif isinstance(x, Font): | |
| f = x | |
| t = str(y) if y is not None else "" | |
| else: | |
| t = str(y) if y is not None else "" | |
| f = font | |
| else: | |
| t = str(text_or_x) | |
| pos_x = float(x) if x is not None else 0.0 | |
| pos_y = float(y) if y is not None else 0.0 | |
| f = font | |
| # Default system font if not provided | |
| if f is None: | |
| try: | |
| f = Font(size=float(eff_size)) | |
| except Exception: | |
| pass | |
| if f is not None: | |
| self.fill_text(pos_x, pos_y, f, t, style=eff_style) | |
| def draw_image(self, x: float, y: float, image: Image, src_rect=None): | |
| """Alias for blit_image.""" | |
| self.blit_image(x, y, image=image, src_rect=src_rect) | |
| def blit(self, x: float, y: float, image: Image, src_rect=None): | |
| """Short alias for blit_image.""" | |
| self.blit_image(x, y, image=image, src_rect=src_rect) | |
| def set_color(self, style: StyleLike): | |
| """Set both fill and stroke style at once.""" | |
| self.set_fill_style(style) | |
| self.set_stroke_style(style) | |
| def color(self): | |
| return self.fill_style | |
| def color(self, style: StyleLike): | |
| self.set_color(style) | |
| def fill_color(self): | |
| return self.fill_style | |
| def fill_color(self, style: StyleLike): | |
| self.set_fill_style(style) | |
| def stroke_color(self): | |
| return self.stroke_style | |
| def stroke_color(self, style: StyleLike): | |
| self.set_stroke_style(style) | |
| def line_width(self) -> float: | |
| return self.stroke_width | |
| def line_width(self, width: float): | |
| self.stroke_width = width | |
| def width(self) -> float: | |
| return self.stroke_width | |
| def width(self, width: float): | |
| self.stroke_width = width | |
| def alpha(self) -> float: | |
| return self.global_alpha | |
| def alpha(self, val: float): | |
| self.global_alpha = val | |
| def opacity(self) -> float: | |
| return self.global_alpha | |
| def opacity(self, val: float): | |
| self.global_alpha = val | |
| def blend_mode(self) -> CompOp: | |
| return self.comp_op | |
| def blend_mode(self, op: Union[CompOp, int]): | |
| self.comp_op = op | |
| def cap(self) -> StrokeCap: | |
| return self.stroke_cap | |
| def cap(self, c: Union[StrokeCap, int]): | |
| self.stroke_cap = c | |
| def caps(self) -> StrokeCap: | |
| return self.stroke_cap | |
| def caps(self, c: Union[StrokeCap, int]): | |
| self.stroke_cap = c | |
| def join(self) -> StrokeJoin: | |
| return self.stroke_join | |
| def join(self, j: Union[StrokeJoin, int]): | |
| self.stroke_join = j | |
| def joins(self) -> StrokeJoin: | |
| return self.stroke_join | |
| def joins(self, j: Union[StrokeJoin, int]): | |
| self.stroke_join = j | |
| def background(self, style: StyleLike): | |
| """Fill entire surface with background style.""" | |
| self.fill_all(style) | |
| def fill_background(self, style: StyleLike): | |
| """Fill entire surface with background style.""" | |
| self.fill_all(style) | |
| def clear(self, style: Optional[StyleLike] = None): | |
| """Clear surface or fill with optional background style.""" | |
| if style is not None: | |
| self.fill_all(style) | |
| else: | |
| self.clear_all() | |
| def __repr__(self) -> str: | |
| status = "active" if self._is_active else "inactive" | |
| return f"Context({status}, threads={self._thread_count})" | |