""" 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)) @property def target_image(self) -> Optional[Image]: if self._image_ref is not None: return self._image_ref() return None @property 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)) @contextmanager 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 @property def error_flags(self) -> int: """Accumulated error flags.""" return self.accumulated_error_flags() # ======================================================================== # Properties & Styles # ======================================================================== @property def comp_op(self) -> CompOp: return CompOp(bl.bl_context_get_comp_op(ctypes.byref(self._core))) @comp_op.setter def comp_op(self, op: Union[CompOp, int]): check_result(bl.bl_context_set_comp_op(ctypes.byref(self._core), int(op))) @property def global_alpha(self) -> float: return bl.bl_context_get_global_alpha(ctypes.byref(self._core)) @global_alpha.setter def global_alpha(self, val: float): check_result(bl.bl_context_set_global_alpha(ctypes.byref(self._core), float(val))) @property def fill_alpha(self) -> float: return bl.bl_context_get_fill_alpha(ctypes.byref(self._core)) @fill_alpha.setter def fill_alpha(self, val: float): check_result(bl.bl_context_set_fill_alpha(ctypes.byref(self._core), float(val))) @property def stroke_alpha(self) -> float: return bl.bl_context_get_stroke_alpha(ctypes.byref(self._core)) @stroke_alpha.setter def stroke_alpha(self, val: float): check_result(bl.bl_context_set_stroke_alpha(ctypes.byref(self._core), float(val))) @property def fill_rule(self) -> FillRule: return FillRule(bl.bl_context_get_fill_rule(ctypes.byref(self._core))) @fill_rule.setter 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)) @property def fill_style(self): return None @fill_style.setter 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)) @property def stroke_style(self): return None @stroke_style.setter def stroke_style(self, style: StyleLike): self.set_stroke_style(style) @property def stroke_width(self) -> float: return bl.bl_context_get_stroke_width(ctypes.byref(self._core)) @stroke_width.setter def stroke_width(self, width: float): check_result(bl.bl_context_set_stroke_width(ctypes.byref(self._core), float(width))) @property def stroke_miter_limit(self) -> float: return bl.bl_context_get_stroke_miter_limit(ctypes.byref(self._core)) @stroke_miter_limit.setter 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))) @property 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))) @stroke_cap.setter def stroke_cap(self, cap: Union[StrokeCap, int]): self.set_stroke_caps(cap) @property def stroke_caps(self) -> StrokeCap: return self.stroke_cap @stroke_caps.setter 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) )) @property def stroke_transform_order(self) -> StrokeTransformOrder: """Return whether transforms are applied before or after stroke width calculation.""" return StrokeTransformOrder.AFTER @stroke_transform_order.setter 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 # ======================================================================== @property def rendering_quality(self) -> RenderingQuality: return RenderingQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.RENDERING_QUALITY))) @rendering_quality.setter 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))) @property def gradient_quality(self) -> GradientQuality: return GradientQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.GRADIENT_QUALITY))) @gradient_quality.setter 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))) @property def pattern_quality(self) -> PatternQuality: return PatternQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.PATTERN_QUALITY))) @pattern_quality.setter 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))) @property def flatten_tolerance(self) -> float: return 0.5 @flatten_tolerance.setter 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))) @property def stroke_join(self) -> StrokeJoin: return StrokeJoin(bl.bl_context_get_stroke_join(ctypes.byref(self._core))) @stroke_join.setter 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) @property def color(self): return self.fill_style @color.setter def color(self, style: StyleLike): self.set_color(style) @property def fill_color(self): return self.fill_style @fill_color.setter def fill_color(self, style: StyleLike): self.set_fill_style(style) @property def stroke_color(self): return self.stroke_style @stroke_color.setter def stroke_color(self, style: StyleLike): self.set_stroke_style(style) @property def line_width(self) -> float: return self.stroke_width @line_width.setter def line_width(self, width: float): self.stroke_width = width @property def width(self) -> float: return self.stroke_width @width.setter def width(self, width: float): self.stroke_width = width @property def alpha(self) -> float: return self.global_alpha @alpha.setter def alpha(self, val: float): self.global_alpha = val @property def opacity(self) -> float: return self.global_alpha @opacity.setter def opacity(self, val: float): self.global_alpha = val @property def blend_mode(self) -> CompOp: return self.comp_op @blend_mode.setter def blend_mode(self, op: Union[CompOp, int]): self.comp_op = op @property def cap(self) -> StrokeCap: return self.stroke_cap @cap.setter def cap(self, c: Union[StrokeCap, int]): self.stroke_cap = c @property def caps(self) -> StrokeCap: return self.stroke_cap @caps.setter def caps(self, c: Union[StrokeCap, int]): self.stroke_cap = c @property def join(self) -> StrokeJoin: return self.stroke_join @join.setter def join(self, j: Union[StrokeJoin, int]): self.stroke_join = j @property def joins(self) -> StrokeJoin: return self.stroke_join @joins.setter 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})"