From 2f39e1c2d8c3eda8d8d89befe547e1e312f6d3e6 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Fri, 19 Jan 2024 17:43:55 +0100 Subject: [PATCH 01/25] bbb-presentation-video for Tldraw v2 --- bbb_presentation_video/events/helpers.py | 24 +- bbb_presentation_video/events/tldraw.py | 48 +- bbb_presentation_video/renderer/__init__.py | 4 +- .../renderer/presentation.py | 8 +- .../renderer/tldraw/__init__.py | 96 +++- .../renderer/tldraw/geo/arrow_geo 2.py | 309 +++++++++++++ .../renderer/tldraw/geo/arrow_geo.py | 309 +++++++++++++ .../renderer/tldraw/geo/checkbox 2.py | 146 ++++++ .../renderer/tldraw/geo/checkbox.py | 146 ++++++ .../renderer/tldraw/geo/cloud.py | 426 ++++++++++++++++++ .../renderer/tldraw/geo/diamond 2.py | 146 ++++++ .../renderer/tldraw/geo/diamond.py | 146 ++++++ .../renderer/tldraw/geo/ellipse 2.py | 68 +++ .../renderer/tldraw/geo/ellipse.py | 68 +++ .../renderer/tldraw/geo/hexagon.py | 111 +++++ .../renderer/tldraw/geo/oval.py | 67 +++ .../renderer/tldraw/geo/rectangle 2.py | 161 +++++++ .../renderer/tldraw/geo/rectangle.py | 161 +++++++ .../renderer/tldraw/geo/rhombus.py | 143 ++++++ .../renderer/tldraw/geo/star 2.py | 136 ++++++ .../renderer/tldraw/geo/star.py | 136 ++++++ .../renderer/tldraw/geo/trapezoid.py | 155 +++++++ .../renderer/tldraw/geo/triangle.py | 151 +++++++ .../renderer/tldraw/geo/xbox 2.py | 118 +++++ .../renderer/tldraw/geo/xbox.py | 118 +++++ .../renderer/tldraw/shape/__init__.py | 373 ++++++++++++++- .../renderer/tldraw/shape/arrow.py | 8 +- .../renderer/tldraw/shape/arrow_v2 2.py | 242 ++++++++++ .../renderer/tldraw/shape/arrow_v2.py | 242 ++++++++++ .../renderer/tldraw/shape/draw.py | 28 +- .../renderer/tldraw/shape/highlighter 2.py | 65 +++ .../renderer/tldraw/shape/highlighter.py | 65 +++ .../renderer/tldraw/shape/line 2.py | 277 ++++++++++++ .../renderer/tldraw/shape/line.py | 277 ++++++++++++ .../renderer/tldraw/shape/sticky_v2 2.py | 53 +++ .../renderer/tldraw/shape/sticky_v2.py | 53 +++ .../renderer/tldraw/shape/text.py | 1 + .../renderer/tldraw/shape/text_v2 2.py | 175 +++++++ .../renderer/tldraw/shape/text_v2.py | 175 +++++++ .../renderer/tldraw/shape/triangle.py | 2 +- .../renderer/tldraw/utils.py | 331 +++++++++++++- bbb_presentation_video/renderer/tldraw/vec.py | 15 + tests/renderer/tldraw/test_shape.py | 107 ++++- 43 files changed, 5822 insertions(+), 68 deletions(-) create mode 100644 bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/checkbox.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/cloud.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/diamond 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/diamond.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/ellipse.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/hexagon.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/oval.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/rectangle.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/rhombus.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/star 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/star.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/trapezoid.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/triangle.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/xbox 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/geo/xbox.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/highlighter.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/line 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/line.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py create mode 100644 bbb_presentation_video/renderer/tldraw/shape/text_v2.py diff --git a/bbb_presentation_video/events/helpers.py b/bbb_presentation_video/events/helpers.py index 3b77969..11b73e3 100644 --- a/bbb_presentation_video/events/helpers.py +++ b/bbb_presentation_video/events/helpers.py @@ -65,12 +65,10 @@ class Position(Sequence[float]): y: float @overload - def __init__(self, iterable: Iterable[float], /) -> None: - ... + def __init__(self, iterable: Iterable[float], /) -> None: ... @overload - def __init__(self, x: float, y: float) -> None: - ... + def __init__(self, x: float, y: float) -> None: ... def __init__( self, x: Union[Iterable[float], float], y: Optional[float] = None @@ -100,12 +98,10 @@ def __truediv__(self: PositionSelf, other: float) -> PositionSelf: return self.__class__(self.x / other, self.y / other) @overload - def __getitem__(self, index: int) -> float: - ... + def __getitem__(self, index: int) -> float: ... @overload - def __getitem__(self, index: slice) -> Sequence[float]: - ... + def __getitem__(self, index: slice) -> Sequence[float]: ... def __getitem__(self, index: Union[int, slice]) -> Union[float, Sequence[float]]: return (self.x, self.y)[index] @@ -123,12 +119,10 @@ class Size(Sequence[float]): height: float @overload - def __init__(self, iterable: Iterable[float], /) -> None: - ... + def __init__(self, iterable: Iterable[float], /) -> None: ... @overload - def __init__(self, width: float, height: float) -> None: - ... + def __init__(self, width: float, height: float) -> None: ... def __init__( self, width: Union[Iterable[float], float], height: Optional[float] = None @@ -155,12 +149,10 @@ def __truediv__(self: SizeSelf, other: float) -> SizeSelf: return self.__class__(self.width / other, self.height / other) @overload - def __getitem__(self, index: int) -> float: - ... + def __getitem__(self, index: int) -> float: ... @overload - def __getitem__(self, index: slice) -> Sequence[float]: - ... + def __getitem__(self, index: slice) -> Sequence[float]: ... def __getitem__(self, index: Union[int, slice]) -> Union[float, Sequence[float]]: return (self.width, self.height)[index] diff --git a/bbb_presentation_video/events/tldraw.py b/bbb_presentation_video/events/tldraw.py index 3f50ddb..4afeb61 100644 --- a/bbb_presentation_video/events/tldraw.py +++ b/bbb_presentation_video/events/tldraw.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import json -from typing import Dict, List, Optional, TypedDict +from typing import Collection, Dict, List, Optional, Sequence, TypedDict, Union from lxml import etree @@ -11,21 +11,48 @@ class StyleData(TypedDict, total=False): - color: str dash: str font: str isFilled: bool scale: float - size: str textAlign: str + opacity: float + isComplete: bool + fill: str + color: str + size: str + isClosed: bool + segments: List[Dict[str, Sequence[Collection[str]]]] class HandleData(TypedDict, total=False): + bindingId: str + canBind: bool + canSnap: bool id: str - index: float + index: Union[str, int] point: List[float] - canBind: bool - bindingId: str + type: str + x: float + y: float + + +class PropsData(StyleData, total=False): + align: str + arrowheadEnd: str + arrowheadStart: str + bend: float + end: HandleData + geo: str + handles: Dict[str, HandleData] + isPen: bool + spline: str + start: HandleData + text: str + verticalAlign: str + w: float + h: float + growY: float class ShapeData(TypedDict, total=False): @@ -34,20 +61,29 @@ class ShapeData(TypedDict, total=False): decorations: Dict[str, Optional[str]] handles: Dict[str, HandleData] id: str + index: Union[int, str] isComplete: bool + isLocked: bool + isModerator: bool label: str labelPoint: List[float] + meta: Dict[str, str] name: str + opacity: float parentId: str point: List[float] points: List[List[float]] + props: PropsData radius: List[float] rotation: float size: List[float] style: StyleData text: str type: str + typeName: str userId: str + x: float + y: float class AddShapeEvent(TypedDict): diff --git a/bbb_presentation_video/renderer/__init__.py b/bbb_presentation_video/renderer/__init__.py index 944dbe2..33715e8 100644 --- a/bbb_presentation_video/renderer/__init__.py +++ b/bbb_presentation_video/renderer/__init__.py @@ -213,7 +213,9 @@ def render(self) -> None: bbb_version=self.events.bbb_version, ) shapes = ShapesRenderer(self.ctx, presentation.transform) - tldraw = TldrawRenderer(self.ctx, presentation.transform) + tldraw = TldrawRenderer( + self.ctx, presentation.transform, self.events.bbb_version + ) encoder = Encoder( self.output, self.width, self.height, self.framerate, self.codec diff --git a/bbb_presentation_video/renderer/presentation.py b/bbb_presentation_video/renderer/presentation.py index aa79e2d..a86d462 100644 --- a/bbb_presentation_video/renderer/presentation.py +++ b/bbb_presentation_video/renderer/presentation.py @@ -167,9 +167,11 @@ def __init__( size=self.size, pos=Position(-0.0, -0.0), shapes_scale=1.0, - shapes_size=self.tldraw_drawing_size - if self.tldraw_whiteboard - else Size(DRAWING_SIZE, DRAWING_SIZE), + shapes_size=( + self.tldraw_drawing_size + if self.tldraw_whiteboard + else Size(DRAWING_SIZE, DRAWING_SIZE) + ), ) @property diff --git a/bbb_presentation_video/renderer/tldraw/__init__.py b/bbb_presentation_video/renderer/tldraw/__init__.py index ca8d849..f42ca0e 100644 --- a/bbb_presentation_video/renderer/tldraw/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/__init__.py @@ -16,26 +16,63 @@ apply_shapes_transform, ) from bbb_presentation_video.renderer.tldraw.fonts import add_fontconfig_app_font_dir +from bbb_presentation_video.renderer.tldraw.geo.arrow_geo import finalize_geo_arrow +from bbb_presentation_video.renderer.tldraw.geo.checkbox import finalize_checkmark +from bbb_presentation_video.renderer.tldraw.geo.cloud import finalize_cloud +from bbb_presentation_video.renderer.tldraw.geo.diamond import finalize_diamond +from bbb_presentation_video.renderer.tldraw.geo.ellipse import finalize_geo_ellipse +from bbb_presentation_video.renderer.tldraw.geo.hexagon import finalize_hexagon +from bbb_presentation_video.renderer.tldraw.geo.oval import finalize_oval +from bbb_presentation_video.renderer.tldraw.geo.rectangle import finalize_geo_rectangle +from bbb_presentation_video.renderer.tldraw.geo.rhombus import finalize_rhombus +from bbb_presentation_video.renderer.tldraw.geo.star import finalize_star +from bbb_presentation_video.renderer.tldraw.geo.trapezoid import finalize_trapezoid +from bbb_presentation_video.renderer.tldraw.geo.triangle import finalize_geo_triangle +from bbb_presentation_video.renderer.tldraw.geo.xbox import finalize_x_box from bbb_presentation_video.renderer.tldraw.shape import ( + ArrowGeo, ArrowShape, + ArrowShape_v2, + CheckBox, + Cloud, + Diamond, DrawShape, EllipseShape, + EllipseGeo, GroupShape, + Hexagon, + HighlighterShape, + LineShape, + Oval, + RectangleGeo, RectangleShape, + Rhombus, Shape, + Star, StickyShape, + StickyShape_v2, TextShape, + TextShape_v2, + Trapezoid, TriangleShape, + TriangleGeo, + XBox, parse_shape_from_data, shape_sort_key, ) from bbb_presentation_video.renderer.tldraw.shape.arrow import finalize_arrow +from bbb_presentation_video.renderer.tldraw.shape.arrow_v2 import finalize_arrow_v2 from bbb_presentation_video.renderer.tldraw.shape.draw import finalize_draw from bbb_presentation_video.renderer.tldraw.shape.ellipse import finalize_ellipse +from bbb_presentation_video.renderer.tldraw.shape.highlighter import finalize_highlight +from bbb_presentation_video.renderer.tldraw.shape.line import finalize_line from bbb_presentation_video.renderer.tldraw.shape.rectangle import finalize_rectangle from bbb_presentation_video.renderer.tldraw.shape.sticky import finalize_sticky +from bbb_presentation_video.renderer.tldraw.shape.sticky_v2 import finalize_sticky_v2 from bbb_presentation_video.renderer.tldraw.shape.text import finalize_text +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_text from bbb_presentation_video.renderer.tldraw.shape.triangle import finalize_triangle +from packaging.version import Version CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) @@ -70,12 +107,20 @@ class TldrawRenderer(Generic[CairoSomeSurface]): shape_patterns: Dict[str, cairo.SurfacePattern] """Cached rendered individual shapes for current presentation/slide.""" - def __init__(self, ctx: cairo.Context[CairoSomeSurface], transform: Transform): + bbb_version: Version + + def __init__( + self, + ctx: cairo.Context[CairoSomeSurface], + transform: Transform, + bbb_version: Version, + ): self.ctx = ctx self.presentation_slide = {} self.shapes = {} self.shape_patterns = {} self.transform = transform + self.bbb_version = bbb_version add_fontconfig_app_font_dir() @@ -146,7 +191,7 @@ def add_shape_event(self, event: tldraw.AddShapeEvent) -> None: action = "updated" else: if "type" in data: - shape = parse_shape_from_data(data) + shape = parse_shape_from_data(data, self.bbb_version) self.shapes[presentation][slide][id] = shape action = "added" else: @@ -230,20 +275,57 @@ def finalize_frame(self, transform: Transform) -> bool: ctx.push_group() ctx.translate(*shape.point) - if isinstance(shape, DrawShape): + if isinstance(shape, ArrowShape): + finalize_arrow(ctx, id, shape) + elif isinstance(shape, ArrowShape_v2): + finalize_arrow_v2(ctx, id, shape) + elif isinstance(shape, CheckBox): + finalize_checkmark(ctx, id, shape) + elif isinstance(shape, Cloud): + finalize_cloud(ctx, id, shape) + elif isinstance(shape, Diamond): + finalize_diamond(ctx, id, shape) + elif isinstance(shape, DrawShape): finalize_draw(ctx, id, shape) + elif isinstance(shape, ArrowGeo): + finalize_geo_arrow(ctx, id, shape) + elif isinstance(shape, EllipseGeo): + finalize_geo_ellipse(ctx, id, shape) + elif isinstance(shape, EllipseShape): + finalize_ellipse(ctx, id, shape) + elif isinstance(shape, Hexagon): + finalize_hexagon(ctx, id, shape) + elif isinstance(shape, HighlighterShape): + finalize_highlight(ctx, id, shape) + elif isinstance(shape, LineShape): + finalize_line(ctx, id, shape) + elif isinstance(shape, Oval): + finalize_oval(ctx, id, shape) + elif isinstance(shape, RectangleGeo): + finalize_geo_rectangle(ctx, id, shape) elif isinstance(shape, RectangleShape): finalize_rectangle(ctx, id, shape) + elif isinstance(shape, Rhombus): + finalize_rhombus(ctx, id, shape) + elif isinstance(shape, Star): + finalize_star(ctx, id, shape) + elif isinstance(shape, Trapezoid): + finalize_trapezoid(ctx, id, shape) + elif isinstance(shape, TriangleGeo): + finalize_geo_triangle(ctx, id, shape) elif isinstance(shape, TriangleShape): finalize_triangle(ctx, id, shape) - elif isinstance(shape, EllipseShape): - finalize_ellipse(ctx, id, shape) - elif isinstance(shape, ArrowShape): - finalize_arrow(ctx, id, shape) elif isinstance(shape, TextShape): finalize_text(ctx, id, shape) + elif isinstance(shape, TextShape_v2): + finalize_v2_text(ctx, id, shape) elif isinstance(shape, StickyShape): finalize_sticky(ctx, shape) + elif isinstance(shape, StickyShape_v2): + finalize_sticky_v2(ctx, shape) + elif isinstance(shape, XBox): + finalize_x_box(ctx, id, shape) + elif isinstance(shape, GroupShape): # Nothing to do? All group-related updates seem to be propagated to the # individual shapes in the group. diff --git a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py new file mode 100644 index 0000000..18a257d --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py @@ -0,0 +1,309 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from math import floor +from random import Random +from typing import List, Tuple, TypeVar + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ArrowGeo +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + GeoShape, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def arrow_geo_stroke_points( + id: str, shape: ArrowGeo +) -> List[perfect_freehand.types.StrokePoint]: + random = Random(id) + sw = STROKE_WIDTHS[shape.style.size] + + # Dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + variation = sw * 0.75 + + v = [] + + if shape.geo is GeoShape.ARROW_DOWN: + oy = min(w, h) * 0.38 + ox = w * 0.16 + v = [ + ( + ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + w / 2 + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ] + elif shape.geo is GeoShape.ARROW_LEFT: + ox = min(w, h) * 0.38 + oy = h * 0.16 + v = [ + ( + ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + h / 2 + random.uniform(-variation, variation), + ), + ] + elif shape.geo is GeoShape.ARROW_UP: + oy = min(w, h) * 0.38 + ox = w * 0.16 + v = [ + ( + w / 2 + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ] + else: + ox = min(w, h) * 0.38 + oy = h * 0.16 + v = [ + ( + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + h / 2 + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ] + # Which side to start drawing first + rm = random.randrange(0, 4) + + # Number of points per side + p = max(8, floor(w / 16)) + + lines = [vec.points_between(v[i], v[(i + 1) % len(v)], p) for i in range(len(v))] + + lines = lines[rm:] + lines[0:rm] + + points: List[Tuple[float, float, float]] = [ + *lines[0], + *lines[1], + *lines[2], + *lines[3], + *lines[4], + *lines[5], + *lines[6], + *lines[0], + ] + + return perfect_freehand.get_stroke_points( + points[5 : floor(len(lines[0]) / -2) + 3], + size=sw, + streamline=0.3, + last=True, + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_geo_arrow( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo +) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = arrow_geo_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_geo_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowGeo) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + if shape.geo == GeoShape.ARROW_DOWN or shape.geo == GeoShape.ARROW_UP: + ox = w * 0.16 + oy = min(w, h) * 0.38 + else: + ox = min(w, h) * 0.38 + oy = h * 0.16 + + if shape.geo == GeoShape.ARROW_UP: + points = [ + Position(w / 2, 0), + Position(w, oy), + Position(w - ox, oy), + Position(w - ox, h), + Position(ox, h), + Position(ox, oy), + Position(0, oy), + ] + elif shape.geo == GeoShape.ARROW_DOWN: + points = [ + Position(ox, 0), + Position(w - ox, 0), + Position(w - ox, h - oy), + Position(w, h - oy), + Position(w / 2, h), + Position(0, h - oy), + Position(ox, h - oy), + ] + elif shape.geo == GeoShape.ARROW_LEFT: + points = [ + Position(ox, 0), + Position(ox, oy), + Position(w, oy), + Position(w, h - oy), + Position(ox, h - oy), + Position(ox, h), + Position(0, h / 2), + ] + else: + points = [ + Position(0, oy), + Position(w - ox, oy), + Position(w - ox, 0), + Position(w, h / 2), + Position(w - ox, h), + Position(w - ox, h - oy), + Position(0, h - oy), + ] + + finalize_geo_path(ctx, points, style) + + +def finalize_geo_arrow( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo +) -> None: + print(f"\tTldraw: Finalizing Arrow (geo): {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_geo_arrow(ctx, id, shape) + else: + dash_geo_arrow(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py new file mode 100644 index 0000000..18a257d --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py @@ -0,0 +1,309 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from math import floor +from random import Random +from typing import List, Tuple, TypeVar + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ArrowGeo +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + GeoShape, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def arrow_geo_stroke_points( + id: str, shape: ArrowGeo +) -> List[perfect_freehand.types.StrokePoint]: + random = Random(id) + sw = STROKE_WIDTHS[shape.style.size] + + # Dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + variation = sw * 0.75 + + v = [] + + if shape.geo is GeoShape.ARROW_DOWN: + oy = min(w, h) * 0.38 + ox = w * 0.16 + v = [ + ( + ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + w / 2 + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ] + elif shape.geo is GeoShape.ARROW_LEFT: + ox = min(w, h) * 0.38 + oy = h * 0.16 + v = [ + ( + ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + h / 2 + random.uniform(-variation, variation), + ), + ] + elif shape.geo is GeoShape.ARROW_UP: + oy = min(w, h) * 0.38 + ox = w * 0.16 + v = [ + ( + w / 2 + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ] + else: + ox = min(w, h) * 0.38 + oy = h * 0.16 + v = [ + ( + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + oy + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ), + ( + w + random.uniform(-variation, variation), + h / 2 + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h + random.uniform(-variation, variation), + ), + ( + w - ox + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ( + random.uniform(-variation, variation), + h - oy + random.uniform(-variation, variation), + ), + ] + # Which side to start drawing first + rm = random.randrange(0, 4) + + # Number of points per side + p = max(8, floor(w / 16)) + + lines = [vec.points_between(v[i], v[(i + 1) % len(v)], p) for i in range(len(v))] + + lines = lines[rm:] + lines[0:rm] + + points: List[Tuple[float, float, float]] = [ + *lines[0], + *lines[1], + *lines[2], + *lines[3], + *lines[4], + *lines[5], + *lines[6], + *lines[0], + ] + + return perfect_freehand.get_stroke_points( + points[5 : floor(len(lines[0]) / -2) + 3], + size=sw, + streamline=0.3, + last=True, + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_geo_arrow( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo +) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = arrow_geo_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_geo_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowGeo) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + if shape.geo == GeoShape.ARROW_DOWN or shape.geo == GeoShape.ARROW_UP: + ox = w * 0.16 + oy = min(w, h) * 0.38 + else: + ox = min(w, h) * 0.38 + oy = h * 0.16 + + if shape.geo == GeoShape.ARROW_UP: + points = [ + Position(w / 2, 0), + Position(w, oy), + Position(w - ox, oy), + Position(w - ox, h), + Position(ox, h), + Position(ox, oy), + Position(0, oy), + ] + elif shape.geo == GeoShape.ARROW_DOWN: + points = [ + Position(ox, 0), + Position(w - ox, 0), + Position(w - ox, h - oy), + Position(w, h - oy), + Position(w / 2, h), + Position(0, h - oy), + Position(ox, h - oy), + ] + elif shape.geo == GeoShape.ARROW_LEFT: + points = [ + Position(ox, 0), + Position(ox, oy), + Position(w, oy), + Position(w, h - oy), + Position(ox, h - oy), + Position(ox, h), + Position(0, h / 2), + ] + else: + points = [ + Position(0, oy), + Position(w - ox, oy), + Position(w - ox, 0), + Position(w, h / 2), + Position(w - ox, h), + Position(w - ox, h - oy), + Position(0, h - oy), + ] + + finalize_geo_path(ctx, points, style) + + +def finalize_geo_arrow( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo +) -> None: + print(f"\tTldraw: Finalizing Arrow (geo): {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_geo_arrow(ctx, id, shape) + else: + dash_geo_arrow(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py b/bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py new file mode 100644 index 0000000..38ff5fe --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from typing import List, TypeVar + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw.shape import ( + CheckBox, +) +from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( + rectangle_stroke_points, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def get_check_box_lines(w: float, h: float) -> List[List[List[float]]]: + size = min(w, h) * 0.82 + ox = (w - size) / 2 + oy = (h - size) / 2 + + def clamp_x(x: float) -> float: + return max(0, min(w, x)) + + def clamp_y(y: float) -> float: + return max(0, min(h, y)) + + return [ + [ + [clamp_x(ox + size * 0.25), clamp_y(oy + size * 0.52)], + [clamp_x(ox + size * 0.45), clamp_y(oy + size * 0.82)], + ], + [ + [clamp_x(ox + size * 0.45), clamp_y(oy + size * 0.82)], + [clamp_x(ox + size * 0.82), clamp_y(oy + size * 0.22)], + ], + ] + + +def overlay_checkmark(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: + sw = STROKE_WIDTHS[shape.style.size] + + # Calculate dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + # Get checkmark lines based on the dimensions + lines = get_check_box_lines(w, h) + + stroke = STROKES[shape.style.color] + + sw = 1 + sw + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, shape.style.opacity) + + # Set stroke width and other drawing properties + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + + # Draw each line of the checkmark + for start, end in lines: + for point in [start, end]: + ctx.line_to(*point) + ctx.stroke() + + +def draw_checkbox( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox +) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + stroke_points = rectangle_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + overlay_checkmark(ctx, shape) + + +def dash_checkbox(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [ + Position(0, 0), + Position(w, 0), + Position(w, h), + Position(0, h), + ] + + overlay_checkmark(ctx, shape) + finalize_geo_path(ctx, points, style) + + +def finalize_checkmark( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox +) -> None: + print(f"\tTldraw: Finalizing checkmark: {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_checkbox(ctx, id, shape) + else: + dash_checkbox(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/checkbox.py b/bbb_presentation_video/renderer/tldraw/geo/checkbox.py new file mode 100644 index 0000000..38ff5fe --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/checkbox.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from typing import List, TypeVar + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw.shape import ( + CheckBox, +) +from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( + rectangle_stroke_points, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def get_check_box_lines(w: float, h: float) -> List[List[List[float]]]: + size = min(w, h) * 0.82 + ox = (w - size) / 2 + oy = (h - size) / 2 + + def clamp_x(x: float) -> float: + return max(0, min(w, x)) + + def clamp_y(y: float) -> float: + return max(0, min(h, y)) + + return [ + [ + [clamp_x(ox + size * 0.25), clamp_y(oy + size * 0.52)], + [clamp_x(ox + size * 0.45), clamp_y(oy + size * 0.82)], + ], + [ + [clamp_x(ox + size * 0.45), clamp_y(oy + size * 0.82)], + [clamp_x(ox + size * 0.82), clamp_y(oy + size * 0.22)], + ], + ] + + +def overlay_checkmark(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: + sw = STROKE_WIDTHS[shape.style.size] + + # Calculate dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + # Get checkmark lines based on the dimensions + lines = get_check_box_lines(w, h) + + stroke = STROKES[shape.style.color] + + sw = 1 + sw + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, shape.style.opacity) + + # Set stroke width and other drawing properties + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + + # Draw each line of the checkmark + for start, end in lines: + for point in [start, end]: + ctx.line_to(*point) + ctx.stroke() + + +def draw_checkbox( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox +) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + stroke_points = rectangle_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + overlay_checkmark(ctx, shape) + + +def dash_checkbox(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [ + Position(0, 0), + Position(w, 0), + Position(w, h), + Position(0, h), + ] + + overlay_checkmark(ctx, shape) + finalize_geo_path(ctx, points, style) + + +def finalize_checkmark( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox +) -> None: + print(f"\tTldraw: Finalizing checkmark: {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_checkbox(ctx, id, shape) + else: + dash_checkbox(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/cloud.py b/bbb_presentation_video/renderer/tldraw/geo/cloud.py new file mode 100644 index 0000000..d03e50c --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/cloud.py @@ -0,0 +1,426 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Adapted from: https://github.com/tldraw/tldraw/blob/main/packages/tldraw/src/lib/shapes/geo/cloudOutline.ts + +from __future__ import annotations +from math import atan2, tau +import math +import attr +from typing import Any, Callable, List, Optional, Tuple, TypeVar, TypedDict, Union + +import cairo +from random import Random + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw.shape import ( + Cloud, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + SizeStyle, + apply_geo_fill, + circle_from_three_points, + get_perfect_dash_props, + get_point_on_circle, +) +from bbb_presentation_video.renderer.tldraw import vec + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +@attr.s(auto_attribs=True, slots=True) +class StraightPillSection: + start: Position + delta: Position + type: str = "straight" + center: Position = Position(-1, -1) + start_angle: float = 0 + + +@attr.s(auto_attribs=True, slots=True) +class ArcPillSection: + start_angle: float + center: Position + type: str = "arc" + start: Position = Position(-1, -1) + delta: Position = Position(-1, -1) + + +class Arc(TypedDict): + leftPoint: Position + rightPoint: Position + arcPoint: Position + center: Optional[Position] + radius: float + + +def get_pill_circumference(w: float, h: float) -> float: + radius = min(w, h) / 2 + long_side = max(w, h) - tau + return tau * radius + 2 * long_side + + +def get_pill_points(width: float, height: float, numPoints: int) -> List[Position]: + radius = min(width, height) / 2 + long_side = max(width, height) - radius * 2 + circumference = tau * radius + 2 * long_side + spacing = circumference / numPoints + + sections: List[Union[StraightPillSection, ArcPillSection]] = [] + + if width > height: + # Definitions for a horizontally oriented pill + sections = [ + StraightPillSection(start=Position(radius, 0), delta=Position(1, 0)), + ArcPillSection( + center=Position(width - radius, radius), start_angle=-tau / 4 + ), + StraightPillSection( + start=Position(width - radius, height), delta=Position(-1, 0) + ), + ArcPillSection(center=Position(radius, radius), start_angle=tau / 4), + ] + else: + # Definitions for a vertically oriented pill + sections = [ + StraightPillSection(start=Position(width, radius), delta=Position(0, 1)), + ArcPillSection(center=Position(radius, height - radius), start_angle=0), + StraightPillSection( + start=Position(0, height - radius), delta=Position(0, -1) + ), + ArcPillSection(center=Position(radius, radius), start_angle=tau / 2), + ] + + points: List[Position] = [] + section_offset = 0.0 + + for _ in range(numPoints): + section = sections[0] + + if section.type == "straight": + straight_point = vec.add( + section.start, vec.mul(section.delta, section_offset) + ) + points.append(Position(straight_point[0], straight_point[1])) + else: + point = get_point_on_circle( + section.center, radius, section.start_angle + section_offset / radius + ) + points.append(point) + + section_offset += spacing + section_length = long_side if section.type == "straight" else tau / 2 * radius + + while section_offset > section_length: + section_offset -= section_length + sections.append(sections.pop(0)) + section = sections[0] + section_length = ( + long_side if section.type == "straight" else tau / 2 * radius + ) + + return points + + +def switchSize(size: SizeStyle) -> float: + if size is SizeStyle.S: + return 50.0 + elif size is SizeStyle.M: + return 70.0 + elif size is SizeStyle.L: + return 100.0 + elif size is SizeStyle.XL: + return 130.0 + else: + return 70.0 + + +def get_cloud_arcs( + width: float, height: float, seed: str, size: SizeStyle +) -> List[Arc]: + random = Random(seed) + pillCircumference = get_pill_circumference(width, height) + + numBumps = max( + math.ceil(pillCircumference / switchSize(size)), + 6, + math.ceil(pillCircumference / min(width, height)), + ) + + targetBumpProtrusion = (pillCircumference / numBumps) * 0.2 + innerWidth = max(width - targetBumpProtrusion * 2, 1) + innerHeight = max(height - targetBumpProtrusion * 2, 1) + paddingX = (width - innerWidth) / 2 + paddingY = (height - innerHeight) / 2 + + distanceBetweenPointsOnPerimeter = ( + get_pill_circumference(innerWidth, innerHeight) / numBumps + ) + + bumpPoints = [ + vec.add(p, (paddingX, paddingY)) + for p in get_pill_points(innerWidth, innerHeight, numBumps) + ] + maxWiggleX = 0 if width < 20 else targetBumpProtrusion * 0.3 + maxWiggleY = 0 if height < 20 else targetBumpProtrusion * 0.3 + + for i in range(math.floor(numBumps / 2)): + bumpPoints[i] = vec.add( + bumpPoints[i], (random.random() * maxWiggleX, random.random() * maxWiggleY) + ) + bumpPoints[numBumps - i - 1] = vec.add( + bumpPoints[numBumps - i - 1], + (random.random() * maxWiggleX, random.random() * maxWiggleY), + ) + + arcs = [] + + for i in range(len(bumpPoints)): + j = 0 if i == len(bumpPoints) - 1 else i + 1 + leftWigglePoint = bumpPoints[i] + rightWigglePoint = bumpPoints[j] + leftPoint = bumpPoints[i] + rightPoint = bumpPoints[j] + + midPoint = vec.med(leftPoint, rightPoint) + offsetAngle = vec.angle(leftPoint, rightPoint) - tau / 4 + + distanceBetweenOriginalPoints = vec.dist(leftPoint, rightPoint) + curvatureOffset = ( + distanceBetweenPointsOnPerimeter - distanceBetweenOriginalPoints + ) + distanceBetweenWigglePoints = vec.dist(leftWigglePoint, rightWigglePoint) + relativeSize = distanceBetweenWigglePoints / distanceBetweenOriginalPoints + finalDistance = (max(paddingX, paddingY) + curvatureOffset) * relativeSize + + arcPoint = vec.add(midPoint, vec.from_angle(offsetAngle, finalDistance)) + + arcPoint_x = ( + 0 if arcPoint[0] < 0 else (width if arcPoint[0] > width else arcPoint[0]) + ) + arcPoint_y = ( + 0 if arcPoint[1] < 0 else (height if arcPoint[1] > height else arcPoint[1]) + ) + arcPoint = (arcPoint_x, arcPoint_y) + + center_pos, _ = circle_from_three_points( + leftWigglePoint, rightWigglePoint, arcPoint + ) + center = (center_pos[0], center_pos[1]) + + radius = vec.dist( + center if center_pos else vec.med(leftWigglePoint, rightWigglePoint), + leftWigglePoint, + ) + + arc_dict = Arc( + leftPoint=Position(*leftWigglePoint), + rightPoint=Position(*rightWigglePoint), + arcPoint=Position(*arcPoint), + center=Position(*center) if center is not None else None, + radius=radius, + ) + + arcs.append(arc_dict) + + return arcs + + +def clockwise_angle_dist(a0: float, a1: float) -> float: + a0 = a0 % tau + a1 = a1 % tau + + if a0 > a1: + a1 += tau + + return a1 - a0 + + +def points_on_arc( + start_point: Position, + end_point: Position, + center: Position, + radius: float, + num_points: int, +) -> List[Position]: + if center is None: + return [start_point, end_point] + + results = [] + + start_angle = vec.angle(center, start_point) + end_angle = vec.angle(center, end_point) + + l = clockwise_angle_dist(start_angle, end_angle) + + for i in range(num_points): + t = i / (num_points - 1) + angle = start_angle + l * t + point = get_point_on_circle(center, radius, angle) + results.append(point) + + return results + + +def mutate_point(p: Position, mut_func: Callable[[Any], Any]) -> Position: + return Position(mut_func(p[0]), mut_func(p[1])) + + +def calculate_angle(center: Position, point: Position) -> float: + dx = point[0] - center[0] + dy = point[1] - center[1] + angle = atan2(dy, dx) + return angle + + +def dash_cloud(ctx: cairo.Context[CairoSomeSurface], shape: Cloud, id: str) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + + stroke = STROKES[style.color] + + ctx.save() + + arcs: List[Arc] = get_cloud_arcs(w, h, id, style.size) + + ctx.new_sub_path() + + for arc in arcs: + leftPoint, rightPoint, radius, center = ( + arc["leftPoint"], + arc["rightPoint"], + arc["radius"], + arc["center"], + ) + + if center is None: + # Move to leftPoint and draw a line to rightPoint instead of an arc + ctx.move_to(*leftPoint) + ctx.line_to(*rightPoint) + else: + # Calculate start and end angles + start_angle = calculate_angle(center, leftPoint) + end_angle = calculate_angle(center, rightPoint) + + ctx.arc(center[0], center[1], radius, start_angle, end_angle) + + ctx.close_path() + + if style.isFilled: + preserve_path = True + apply_geo_fill(ctx, style, preserve_path) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + + dash_array, dash_offset = get_perfect_dash_props( + abs(2 * w + 2 * h), sw, style.dash, snap=2, outset=False + ) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + + ctx.stroke() + ctx.restore() + + +def draw_cloud(ctx: cairo.Context[CairoSomeSurface], shape: Cloud, id: str) -> None: + style = shape.style + random = Random(id) + + stroke_width = STROKE_WIDTHS[shape.style.size] + sw = 1 + stroke_width * 1.618 + + stroke = STROKES[shape.style.color] + ctx.save() + + size_multipliers = { + SizeStyle.S: 0.5, + SizeStyle.M: 0.7, + SizeStyle.L: 0.9, + SizeStyle.XL: 1.6, + } + mut_multiplier = size_multipliers.get(shape.style.size, 1.0) + mut = lambda n: n + random.random() * mut_multiplier * 2 + + width = max(0, shape.size.width) + height = max(0, shape.size.height) + arcs = get_cloud_arcs(width, height, id, shape.style.size) + avg_arc_length = sum( + math.sqrt( + (arc["leftPoint"][0] - arc["rightPoint"][0]) ** 2 + + (arc["leftPoint"][1] - arc["rightPoint"][1]) ** 2 + ) + for arc in arcs + ) / len(arcs) + should_mutate_points = avg_arc_length > mut_multiplier * 15 + + ctx.new_sub_path() + + for arc in arcs: + leftPoint, rightPoint, radius, center = ( + arc["leftPoint"], + arc["rightPoint"], + arc["radius"], + arc["center"], + ) + + if should_mutate_points: + leftPoint = mutate_point(leftPoint, mut) + rightPoint = mutate_point(rightPoint, mut) + + if center is None: + ctx.move_to(*leftPoint) + ctx.line_to(*rightPoint) + else: + start_angle = calculate_angle(center, leftPoint) + end_angle = calculate_angle(center, rightPoint) + + if should_mutate_points: + center = mutate_point(center, mut) + radius += random.random() * mut_multiplier + + ctx.arc(center[0], center[1], radius, start_angle, end_angle) + + ctx.close_path() + + if style.isFilled: + preserve_path = True + apply_geo_fill(ctx, style, preserve_path) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + + dash_array, dash_offset = get_perfect_dash_props( + abs(2 * width + 2 * height), sw, shape.style.dash, snap=2, outset=False + ) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, shape.style.opacity) + ctx.stroke() + ctx.restore() + + +def finalize_cloud(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Cloud) -> None: + print(f"\tTldraw: Finalizing Cloud: {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_cloud(ctx, shape, id) + else: + dash_cloud(ctx, shape, id) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/diamond 2.py b/bbb_presentation_video/renderer/tldraw/geo/diamond 2.py new file mode 100644 index 0000000..e85180e --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/diamond 2.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from random import Random +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + Diamond, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def diamond_stroke_points(id: str, shape: Diamond) -> List[StrokePoint]: + random = Random(id) + size = shape.size + + width = size.width + height = size.height + half_width = size.width / 2 + half_height = size.height / 2 + + stroke_width = STROKE_WIDTHS[shape.style.size] + + # Corners with random offsets + variation = stroke_width * 0.75 + + t = ( + half_width + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ) + r = ( + width + random.uniform(-variation, variation), + half_height + random.uniform(-variation, variation), + ) + b = ( + half_width + random.uniform(-variation, variation), + height + random.uniform(-variation, variation), + ) + l = ( + random.uniform(-variation, variation), + half_height + random.uniform(-variation, variation), + ) + + # Which side to start drawing first + rm = random.randrange(0, 3) + + lines = [ + vec.points_between(t, r, 32), + vec.points_between(r, b, 32), + vec.points_between(b, l, 32), + vec.points_between(l, t, 32), + ] + + lines = lines[rm:] + lines[0:rm] + points = [*lines[0], *lines[1], *lines[2], *lines[3], *lines[0]] + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_diamond(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = diamond_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_diamond(ctx: cairo.Context[CairoSomeSurface], shape: Diamond) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + half_width = w / 2 + half_height = h / 2 + + points = [ + Position(half_width, 0), + Position(w, half_height), + Position(half_width, h), + Position(0, half_height), + ] + + finalize_geo_path(ctx, points, style) + + +def finalize_diamond( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond +) -> None: + print(f"\tTldraw: Finalizing Diamond: {id}") + + style = shape.style + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_diamond(ctx, id, shape) + else: + dash_diamond(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/diamond.py b/bbb_presentation_video/renderer/tldraw/geo/diamond.py new file mode 100644 index 0000000..e85180e --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/diamond.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from random import Random +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + Diamond, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def diamond_stroke_points(id: str, shape: Diamond) -> List[StrokePoint]: + random = Random(id) + size = shape.size + + width = size.width + height = size.height + half_width = size.width / 2 + half_height = size.height / 2 + + stroke_width = STROKE_WIDTHS[shape.style.size] + + # Corners with random offsets + variation = stroke_width * 0.75 + + t = ( + half_width + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ) + r = ( + width + random.uniform(-variation, variation), + half_height + random.uniform(-variation, variation), + ) + b = ( + half_width + random.uniform(-variation, variation), + height + random.uniform(-variation, variation), + ) + l = ( + random.uniform(-variation, variation), + half_height + random.uniform(-variation, variation), + ) + + # Which side to start drawing first + rm = random.randrange(0, 3) + + lines = [ + vec.points_between(t, r, 32), + vec.points_between(r, b, 32), + vec.points_between(b, l, 32), + vec.points_between(l, t, 32), + ] + + lines = lines[rm:] + lines[0:rm] + points = [*lines[0], *lines[1], *lines[2], *lines[3], *lines[0]] + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_diamond(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = diamond_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_diamond(ctx: cairo.Context[CairoSomeSurface], shape: Diamond) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + half_width = w / 2 + half_height = h / 2 + + points = [ + Position(half_width, 0), + Position(w, half_height), + Position(half_width, h), + Position(0, half_height), + ] + + finalize_geo_path(ctx, points, style) + + +def finalize_diamond( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond +) -> None: + print(f"\tTldraw: Finalizing Diamond: {id}") + + style = shape.style + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_diamond(ctx, id, shape) + else: + dash_diamond(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py b/bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py new file mode 100644 index 0000000..bc8053e --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import TypeVar + +import cairo +from bbb_presentation_video.renderer.tldraw.shape import ( + EllipseGeo, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + apply_geo_fill, + get_perfect_dash_props, + perimeter_of_ellipse, +) +from bbb_presentation_video.renderer.utils import cairo_draw_ellipse + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def dash_ellipse(ctx: cairo.Context[CairoSomeSurface], shape: EllipseGeo) -> None: + radius = (shape.size.width / 2, shape.size.height / 2) + style = shape.style + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + sw = 1 + stroke_width * 1.618 + rx = max(0, radius[0] - sw / 2) + ry = max(0, radius[1] - sw / 2) + perimeter = perimeter_of_ellipse(rx, ry) + dash_array, dash_offset = get_perfect_dash_props( + perimeter * 2 if perimeter < 64 else perimeter, + stroke_width * 1.618, + style.dash, + snap=4, + ) + + if style.isFilled: + cairo_draw_ellipse(ctx, radius[0], radius[1], radius[0], radius[1]) + apply_geo_fill(ctx, style) + + cairo_draw_ellipse(ctx, radius[0], radius[1], radius[0], radius[1]) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + + +def finalize_geo_ellipse( + ctx: cairo.Context[CairoSomeSurface], + id: str, + shape: EllipseGeo, +) -> None: + print(f"\tTldraw: Finalizing Ellipse (geo): {id}") + + ctx.rotate(shape.rotation) + + dash_ellipse(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/ellipse.py b/bbb_presentation_video/renderer/tldraw/geo/ellipse.py new file mode 100644 index 0000000..bc8053e --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/ellipse.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import TypeVar + +import cairo +from bbb_presentation_video.renderer.tldraw.shape import ( + EllipseGeo, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + apply_geo_fill, + get_perfect_dash_props, + perimeter_of_ellipse, +) +from bbb_presentation_video.renderer.utils import cairo_draw_ellipse + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def dash_ellipse(ctx: cairo.Context[CairoSomeSurface], shape: EllipseGeo) -> None: + radius = (shape.size.width / 2, shape.size.height / 2) + style = shape.style + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + sw = 1 + stroke_width * 1.618 + rx = max(0, radius[0] - sw / 2) + ry = max(0, radius[1] - sw / 2) + perimeter = perimeter_of_ellipse(rx, ry) + dash_array, dash_offset = get_perfect_dash_props( + perimeter * 2 if perimeter < 64 else perimeter, + stroke_width * 1.618, + style.dash, + snap=4, + ) + + if style.isFilled: + cairo_draw_ellipse(ctx, radius[0], radius[1], radius[0], radius[1]) + apply_geo_fill(ctx, style) + + cairo_draw_ellipse(ctx, radius[0], radius[1], radius[0], radius[1]) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + + +def finalize_geo_ellipse( + ctx: cairo.Context[CairoSomeSurface], + id: str, + shape: EllipseGeo, +) -> None: + print(f"\tTldraw: Finalizing Ellipse (geo): {id}") + + ctx.rotate(shape.rotation) + + dash_ellipse(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/hexagon.py b/bbb_presentation_video/renderer/tldraw/geo/hexagon.py new file mode 100644 index 0000000..8c4863b --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/hexagon.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint + +from bbb_presentation_video.renderer.tldraw.shape import ( + Hexagon, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, + get_polygon_draw_vertices, + get_polygon_strokes, +) + + +def hexagon_stroke_points(id: str, shape: Hexagon) -> List[StrokePoint]: + size = shape.size + + width = size.width + height = size.height + + stroke_width = STROKE_WIDTHS[shape.style.size] + + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + sides = 6 + + strokes = get_polygon_strokes(width, height, sides) + points = get_polygon_draw_vertices(strokes, stroke_width, id) + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_hexagon(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Hexagon) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = hexagon_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_hexagon(ctx: cairo.Context[CairoSomeSurface], shape: Hexagon) -> None: + style = shape.style + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + sides = 6 + + strokes = get_polygon_strokes(width, height, sides) + points = [stroke[0] for stroke in strokes] + + finalize_geo_path(ctx, points, style) + + +def finalize_hexagon( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: Hexagon +) -> None: + print(f"\tTldraw: Finalizing Hexagon: {id}") + + style = shape.style + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_hexagon(ctx, id, shape) + else: + dash_hexagon(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/oval.py b/bbb_presentation_video/renderer/tldraw/geo/oval.py new file mode 100644 index 0000000..104dee6 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/oval.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from math import cos, sin, tau +from typing import List, TypeVar + +import cairo +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw.shape import Oval +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + finalize_geo_path, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def oval_points(w: float, h: float, n_vertices: int = 25) -> List[Position]: + cx = w / 2 + cy = h / 2 + + points: List[Position] = [Position(0, 0)] * (n_vertices * 2 - 2) + + if h > w: + for i in range(n_vertices - 1): + t1 = -(tau / 2) + ((tau / 2) * i) / (n_vertices - 2) + t2 = ((tau / 2) * i) / (n_vertices - 2) + points[i] = Position(cx + cx * cos(t1), cx + cx * sin(t1)) + points[i + (n_vertices - 1)] = Position( + cx + cx * cos(t2), h - cx + cx * sin(t2) + ) + else: + for i in range(n_vertices - 1): + t1 = -(tau / 4) + (tau / 2 * i) / (n_vertices - 2) + t2 = (tau / 4) + (tau / 2 * -i) / (n_vertices - 2) + points[i] = Position(w - cy + cy * cos(t1), h - cy + cy * sin(t1)) + points[i + (n_vertices - 1)] = Position( + cy - cy * cos(t2), h - cy + cy * sin(t2) + ) + + return points + + +def dash_oval(ctx: cairo.Context[CairoSomeSurface], shape: Oval) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + n_vertices = 50 + points = oval_points(w, h, n_vertices) + + finalize_geo_path(ctx, points, style) + + +def finalize_oval(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Oval) -> None: + print(f"\tTldraw: Finalizing Oval: {id}") + + ctx.rotate(shape.rotation) + + dash_oval(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py new file mode 100644 index 0000000..2b4cdc2 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from math import floor +from random import Random +from typing import List, Tuple, TypeVar, Union + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + CheckBox, + RectangleGeo, + XBox, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def rectangle_stroke_points( + id: str, shape: Union[RectangleGeo, XBox, CheckBox] +) -> List[perfect_freehand.types.StrokePoint]: + random = Random(id) + sw = STROKE_WIDTHS[shape.style.size] + + # Dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + # Corners + variation = sw * 0.75 + tl = ( + sw / 2 + random.uniform(-variation, variation), + sw / 2 + random.uniform(-variation, variation), + ) + tr = ( + w - sw / 2 + random.uniform(-variation, variation), + sw / 2 + random.uniform(-variation, variation), + ) + br = ( + w - sw / 2 + random.uniform(-variation, variation), + h - sw / 2 + random.uniform(-variation, variation), + ) + bl = ( + sw / 2 + random.uniform(-variation, variation), + h - sw / 2 + random.uniform(-variation, variation), + ) + + # Which side to start drawing first + rm = random.randrange(0, 4) + + # Corner radii + rx = min(w / 4, sw * 2) + ry = min(h / 4, sw / 2) + + # Number of points per side + px = max(8, floor(w / 16)) + py = max(8, floor(h / 16)) + + # Insert each line by the corner radii and let the freehand algo + # interpolate points for the corners. + lines = [ + vec.points_between(vec.add(tl, (rx, 0)), vec.sub(tr, (rx, 0)), px), + vec.points_between(vec.add(tr, (0, ry)), vec.sub(br, (0, ry)), py), + vec.points_between(vec.sub(br, (rx, 0)), vec.add(bl, (rx, 0)), px), + vec.points_between(vec.sub(bl, (0, ry)), vec.add(tl, (0, ry)), py), + ] + lines = lines[rm:] + lines[0:rm] + + # For the final points, include the first half of the first line again, + # so that the line wraps around and avoids ending on a sharp corner. + # This has a bit of finesse and magic—if you change the points_between + # function, then you'll likely need to change this one too. + points: List[Tuple[float, float, float]] = [ + *lines[0], + *lines[1], + *lines[2], + *lines[3], + *lines[0], + ] + + return perfect_freehand.get_stroke_points( + points[5 : floor(len(lines[0]) / -2) + 3], + size=sw, + streamline=0.3, + last=True, + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_rectangle( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo +) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = rectangle_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_rectangle(ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeo) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [Position(0, 0), Position(w, 0), Position(w, h), Position(0, h)] + + finalize_geo_path(ctx, points, style) + + +def finalize_geo_rectangle( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo +) -> None: + print(f"\tTldraw: Finalizing Rectangle (geo): {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_rectangle(ctx, id, shape) + else: + dash_rectangle(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py new file mode 100644 index 0000000..2b4cdc2 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from math import floor +from random import Random +from typing import List, Tuple, TypeVar, Union + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + CheckBox, + RectangleGeo, + XBox, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def rectangle_stroke_points( + id: str, shape: Union[RectangleGeo, XBox, CheckBox] +) -> List[perfect_freehand.types.StrokePoint]: + random = Random(id) + sw = STROKE_WIDTHS[shape.style.size] + + # Dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + # Corners + variation = sw * 0.75 + tl = ( + sw / 2 + random.uniform(-variation, variation), + sw / 2 + random.uniform(-variation, variation), + ) + tr = ( + w - sw / 2 + random.uniform(-variation, variation), + sw / 2 + random.uniform(-variation, variation), + ) + br = ( + w - sw / 2 + random.uniform(-variation, variation), + h - sw / 2 + random.uniform(-variation, variation), + ) + bl = ( + sw / 2 + random.uniform(-variation, variation), + h - sw / 2 + random.uniform(-variation, variation), + ) + + # Which side to start drawing first + rm = random.randrange(0, 4) + + # Corner radii + rx = min(w / 4, sw * 2) + ry = min(h / 4, sw / 2) + + # Number of points per side + px = max(8, floor(w / 16)) + py = max(8, floor(h / 16)) + + # Insert each line by the corner radii and let the freehand algo + # interpolate points for the corners. + lines = [ + vec.points_between(vec.add(tl, (rx, 0)), vec.sub(tr, (rx, 0)), px), + vec.points_between(vec.add(tr, (0, ry)), vec.sub(br, (0, ry)), py), + vec.points_between(vec.sub(br, (rx, 0)), vec.add(bl, (rx, 0)), px), + vec.points_between(vec.sub(bl, (0, ry)), vec.add(tl, (0, ry)), py), + ] + lines = lines[rm:] + lines[0:rm] + + # For the final points, include the first half of the first line again, + # so that the line wraps around and avoids ending on a sharp corner. + # This has a bit of finesse and magic—if you change the points_between + # function, then you'll likely need to change this one too. + points: List[Tuple[float, float, float]] = [ + *lines[0], + *lines[1], + *lines[2], + *lines[3], + *lines[0], + ] + + return perfect_freehand.get_stroke_points( + points[5 : floor(len(lines[0]) / -2) + 3], + size=sw, + streamline=0.3, + last=True, + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_rectangle( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo +) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = rectangle_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_rectangle(ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeo) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [Position(0, 0), Position(w, 0), Position(w, h), Position(0, h)] + + finalize_geo_path(ctx, points, style) + + +def finalize_geo_rectangle( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo +) -> None: + print(f"\tTldraw: Finalizing Rectangle (geo): {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_rectangle(ctx, id, shape) + else: + dash_rectangle(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/rhombus.py b/bbb_presentation_video/renderer/tldraw/geo/rhombus.py new file mode 100644 index 0000000..b1cf2a8 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/rhombus.py @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from random import Random +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint + +from bbb_presentation_video.events.helpers import Position, Size +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + Rhombus, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def rhombus_stroke_points(id: str, shape: Rhombus) -> List[StrokePoint]: + random = Random(id) + size = shape.size + + width = size.width + height = size.height + + x_offset = min(width * 0.38, height * 0.38) + stroke_width = STROKE_WIDTHS[shape.style.size] + + # Corners with random offsets + variation = stroke_width * 0.75 + + tl = ( + x_offset + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ) + tr = ( + width + random.uniform(-variation, variation), + 0 + random.uniform(-variation, variation), + ) + br = ( + width - x_offset + random.uniform(-variation, variation), + height + random.uniform(-variation, variation), + ) + bl = ( + random.uniform(-variation, variation), + height + random.uniform(-variation, variation), + ) + + # Which side to start drawing first + rm = random.randrange(0, 3) + + lines = [ + vec.points_between(tl, tr, 32), + vec.points_between(tr, br, 32), + vec.points_between(br, bl, 32), + vec.points_between(bl, tl, 32), + ] + lines = lines[rm:] + lines[0:rm] + + points = [*lines[0], *lines[1], *lines[2], *lines[3], *lines[0]] + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_rhombus(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Rhombus) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = rhombus_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_rhombus(ctx: cairo.Context[CairoSomeSurface], shape: Rhombus) -> None: + style = shape.style + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + # Internal angle between adjacent sides varies with width and height + x_offset = min(width * 0.38, height * 0.38) + points = [ + Position(x_offset, 0), + Position(width, 0), + Position(width - x_offset, height), + Position(0, height), + Position(x_offset, 0), + ] + finalize_geo_path(ctx, points, style) + + +def finalize_rhombus( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: Rhombus +) -> None: + print(f"\tTldraw: Finalizing Rhombus: {id}") + + style = shape.style + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_rhombus(ctx, id, shape) + else: + dash_rhombus(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/star 2.py b/bbb_presentation_video/renderer/tldraw/geo/star 2.py new file mode 100644 index 0000000..b8915f2 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/star 2.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from math import cos, sin, tau + +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw.shape import ( + Star, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, + get_polygon_draw_vertices, +) + + +def get_star_points(w: float, h: float, n: int) -> List[Position]: + sides = n + step = tau / sides / 2 + + # Calculate the bounding box adjustments + cx, cy = w / 2, h / 2 + ratio = 1 + ox, oy = (w / 2, h / 2) + ix, iy = (ox * ratio) / 2, (oy * ratio) / 2 + + points = [ + Position( + cx + (ix if i % 2 else ox) * cos(-(tau / 4) + i * step), + cy + (iy if i % 2 else oy) * sin(-(tau / 4) + i * step), + ) + for i in range(sides * 2) + ] + + return points + + +def star_stroke_points(id: str, shape: Star) -> List[StrokePoint]: + size = shape.size + + width = size.width + height = size.height + + stroke_width = STROKE_WIDTHS[shape.style.size] + + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + vertices = 5 + + star_points = get_star_points(width, height, vertices) + strokes = [] + + for i in range(len(star_points)): + pos1 = star_points[i] + pos2 = star_points[(i + 1) % len(star_points)] + distance = ((pos2.x - pos1.x) ** 2 + (pos2.y - pos1.y) ** 2) ** 0.5 + strokes.append((pos1, pos2, distance)) + + points = get_polygon_draw_vertices(strokes, stroke_width, id) + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = star_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_star(ctx: cairo.Context[CairoSomeSurface], shape: Star) -> None: + style = shape.style + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + vertices = 5 + points = get_star_points(width, height, vertices) + + finalize_geo_path(ctx, points, style) + + +def finalize_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: + print(f"\tTldraw: Finalizing Star: {id}") + + style = shape.style + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_star(ctx, id, shape) + else: + dash_star(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/star.py b/bbb_presentation_video/renderer/tldraw/geo/star.py new file mode 100644 index 0000000..b8915f2 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/star.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from math import cos, sin, tau + +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw.shape import ( + Star, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, + get_polygon_draw_vertices, +) + + +def get_star_points(w: float, h: float, n: int) -> List[Position]: + sides = n + step = tau / sides / 2 + + # Calculate the bounding box adjustments + cx, cy = w / 2, h / 2 + ratio = 1 + ox, oy = (w / 2, h / 2) + ix, iy = (ox * ratio) / 2, (oy * ratio) / 2 + + points = [ + Position( + cx + (ix if i % 2 else ox) * cos(-(tau / 4) + i * step), + cy + (iy if i % 2 else oy) * sin(-(tau / 4) + i * step), + ) + for i in range(sides * 2) + ] + + return points + + +def star_stroke_points(id: str, shape: Star) -> List[StrokePoint]: + size = shape.size + + width = size.width + height = size.height + + stroke_width = STROKE_WIDTHS[shape.style.size] + + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + vertices = 5 + + star_points = get_star_points(width, height, vertices) + strokes = [] + + for i in range(len(star_points)): + pos1 = star_points[i] + pos2 = star_points[(i + 1) % len(star_points)] + distance = ((pos2.x - pos1.x) ** 2 + (pos2.y - pos1.y) ** 2) ** 0.5 + strokes.append((pos1, pos2, distance)) + + points = get_polygon_draw_vertices(strokes, stroke_width, id) + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = star_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_star(ctx: cairo.Context[CairoSomeSurface], shape: Star) -> None: + style = shape.style + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + vertices = 5 + points = get_star_points(width, height, vertices) + + finalize_geo_path(ctx, points, style) + + +def finalize_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: + print(f"\tTldraw: Finalizing Star: {id}") + + style = shape.style + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_star(ctx, id, shape) + else: + dash_star(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py new file mode 100644 index 0000000..cde094e --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from random import Random +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + Trapezoid, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def trapezoid_stroke_points(id: str, shape: Trapezoid) -> List[StrokePoint]: + random = Random(id) + size = shape.size + + width = size.width + height = size.height + + top_width = width * 0.6 + x_offset = (width - top_width) / 2 + + stroke_width = STROKE_WIDTHS[shape.style.size] + + # Corners with random offsets + variation = stroke_width * 0.75 + + tl = ( + x_offset + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ) + tr = ( + x_offset + top_width + random.uniform(-variation, variation), + 0 + random.uniform(-variation, variation), + ) + br = ( + width + random.uniform(-variation, variation), + height + random.uniform(-variation, variation), + ) + bl = ( + random.uniform(-variation, variation), + height + random.uniform(-variation, variation), + ) + + # Which side to start drawing first + rm = random.randrange(0, 3) + # Number of points per side + # Insert each line by the corner radii and let the freehand algo + # interpolate points for the corners. + lines = [ + vec.points_between(tl, tr, 32), + vec.points_between(tr, br, 32), + vec.points_between(br, bl, 32), + vec.points_between(bl, tl, 32), + ] + lines = lines[rm:] + lines[0:rm] + + # For the final points, include the first half of the first line again, + # so that the line wraps around and avoids ending on a sharp corner. + # This has a bit of finesse and magic—if you change the points between + # function, then you'll likely need to change this one too. + # TODO: It actually includes the whole first line again, not just half? + points = [*lines[0], *lines[1], *lines[2], *lines[3], *lines[0]] + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_trapezoid( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: Trapezoid +) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = trapezoid_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_trapezoid(ctx: cairo.Context[CairoSomeSurface], shape: Trapezoid) -> None: + style = shape.style + width = max(0, shape.size.width) + height = max(0, shape.size.height) + + top_width = width * 0.6 + x_offset = (width - top_width) / 2 + + points = [ + Position(x_offset, 0), + Position(top_width + x_offset, 0), + Position(width, height), + Position(0, height), + ] + + finalize_geo_path(ctx, points, style) + + +def finalize_trapezoid( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: Trapezoid +) -> None: + print(f"\tTldraw: Finalizing Trapezoid: {id}") + + style = shape.style + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_trapezoid(ctx, id, shape) + else: + dash_trapezoid(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/triangle.py b/bbb_presentation_video/renderer/tldraw/geo/triangle.py new file mode 100644 index 0000000..f0cc37b --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/triangle.py @@ -0,0 +1,151 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from random import Random +from typing import List, TypeVar + +import cairo +import perfect_freehand +from perfect_freehand.types import StrokePoint + +from bbb_presentation_video.events.helpers import Position, Size +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + TriangleGeo, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + + +def triangle_centroid(size: Size) -> Position: + w, h = size + return (Position(w / 2, 0) + Position(w, h) + Position(0, h)) / 3 + + +def triangle_stroke_points(id: str, shape: TriangleGeo) -> List[StrokePoint]: + random = Random(id) + size = shape.size + stroke_width = STROKE_WIDTHS[shape.style.size] + + # Corners with random offsets + variation = stroke_width * 0.75 + t = ( + size.width / 2 + random.uniform(-variation, variation), + random.uniform(-variation, variation), + ) + br = ( + size.width + random.uniform(-variation, variation), + size.height + random.uniform(-variation, variation), + ) + bl = ( + random.uniform(-variation, variation), + size.height + random.uniform(-variation, variation), + ) + + # Which side to start drawing first + rm = random.randrange(0, 3) + # Number of points per side + # Insert each line by the corner radii and let the freehand algo + # interpolate points for the corners. + lines = [ + vec.points_between(t, br, 32), + vec.points_between(br, bl, 32), + vec.points_between(bl, t, 32), + ] + lines = lines[rm:] + lines[0:rm] + + # For the final points, include the first half of the first line again, + # so that the line wraps around and avoids ending on a sharp corner. + # This has a bit of finesse and magic—if you change the points between + # function, then you'll likely need to change this one too. + # TODO: It actually includes the whole first line again, not just half? + points = [*lines[0], *lines[1], *lines[2], *lines[0]] + + return perfect_freehand.get_stroke_points( + points, size=stroke_width, streamline=0.3, last=True + ) + + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def draw_triangle( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TriangleGeo +) -> None: + style = shape.style + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + + stroke_points = triangle_stroke_points(id, shape) + + if style.isFilled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + +def dash_triangle(ctx: cairo.Context[CairoSomeSurface], shape: TriangleGeo) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [ + Position(w / 2, 0), + Position(w, h), + Position(0, h), + Position(w / 2, 0), + ] + + finalize_geo_path(ctx, points, style) + + +def finalize_geo_triangle( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TriangleGeo +) -> None: + print(f"\tTldraw: Finalizing Triangle (geo): {id}") + + style = shape.style + size = shape.size + + ctx.rotate(shape.rotation) + + if style.dash is DashStyle.DRAW: + draw_triangle(ctx, id, shape) + else: + dash_triangle(ctx, shape) + + center = Position(size / 2) + centeroid = triangle_centroid(size) + offset_y = (centeroid.y - center.y) * 0.72 + offset = shape.label_offset() + Position(0, offset_y) + + finalize_v2_label(ctx, shape, offset=offset) diff --git a/bbb_presentation_video/renderer/tldraw/geo/xbox 2.py b/bbb_presentation_video/renderer/tldraw/geo/xbox 2.py new file mode 100644 index 0000000..590fe1a --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/xbox 2.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from typing import TypeVar + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw.shape import ( + XBox, +) +from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( + rectangle_stroke_points, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def overlay_x_cross(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: + sw = STROKE_WIDTHS[shape.style.size] + + # Dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + # The cross doesn't touch the vertices of the box to + # prevent opacities from adding up + x_offset = 2 * sw + y_offset = 2 * sw + + tl = (x_offset, y_offset) + tr = (w - x_offset, y_offset) + + br = (w - x_offset, h - y_offset) + bl = (x_offset, h - y_offset) + + ctx.move_to(*tl) + ctx.line_to(*br) + ctx.move_to(*tr) + ctx.line_to(*bl) + ctx.set_line_width(2 * sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.stroke() + + +def draw_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + stroke_points = rectangle_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + overlay_x_cross(ctx, shape) + + +def dash_x_box(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [ + Position(0, 0), + Position(w, 0), + Position(w, h), + Position(0, h), + ] + + finalize_geo_path(ctx, points, style) + overlay_x_cross(ctx, shape) + + +def finalize_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: + print(f"\tTldraw: Finalizing x-box: {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_x_box(ctx, id, shape) + else: + dash_x_box(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/xbox.py b/bbb_presentation_video/renderer/tldraw/geo/xbox.py new file mode 100644 index 0000000..590fe1a --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/geo/xbox.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from typing import TypeVar + +import cairo +import perfect_freehand +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw.shape import ( + XBox, +) +from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( + rectangle_stroke_points, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + apply_geo_fill, + draw_smooth_path, + draw_smooth_stroke_point_path, + finalize_geo_path, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def overlay_x_cross(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: + sw = STROKE_WIDTHS[shape.style.size] + + # Dimensions + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + # The cross doesn't touch the vertices of the box to + # prevent opacities from adding up + x_offset = 2 * sw + y_offset = 2 * sw + + tl = (x_offset, y_offset) + tr = (w - x_offset, y_offset) + + br = (w - x_offset, h - y_offset) + bl = (x_offset, h - y_offset) + + ctx.move_to(*tl) + ctx.line_to(*br) + ctx.move_to(*tr) + ctx.line_to(*bl) + ctx.set_line_width(2 * sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.stroke() + + +def draw_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: + style = shape.style + is_filled = style.isFilled + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] + stroke_points = rectangle_stroke_points(id, shape) + + if is_filled: + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + apply_geo_fill(ctx, style) + + stroke_outline_points = perfect_freehand.get_stroke_outline_points( + stroke_points, + size=stroke_width, + thinning=0.65, + smoothing=1, + simulate_pressure=False, + last=True, + ) + + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.fill_preserve() + ctx.set_line_width(stroke_width) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + + overlay_x_cross(ctx, shape) + + +def dash_x_box(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [ + Position(0, 0), + Position(w, 0), + Position(w, h), + Position(0, h), + ] + + finalize_geo_path(ctx, points, style) + overlay_x_cross(ctx, shape) + + +def finalize_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: + print(f"\tTldraw: Finalizing x-box: {id}") + + ctx.rotate(shape.rotation) + + if shape.style.dash is DashStyle.DRAW: + draw_x_box(ctx, id, shape) + else: + dash_x_box(ctx, shape) + + finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index cea82d9..5a94e51 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -11,7 +11,16 @@ from bbb_presentation_video.events.helpers import Position, Size from bbb_presentation_video.events.tldraw import HandleData, ShapeData -from bbb_presentation_video.renderer.tldraw.utils import Decoration, DrawPoints, Style +from bbb_presentation_video.renderer.tldraw.utils import ( + AlignStyle, + GeoShape, + Decoration, + DrawPoints, + SplineType, + Style, +) + +from packaging.version import Version BaseShapeSelf = TypeVar("BaseShapeSelf", bound="BaseShapeProto") @@ -26,6 +35,8 @@ class BaseShapeProto(Protocol): """Unsure: possibly z-position of this shape within a group?""" point: Position = Position(0, 0) """Position of the origin of the shape.""" + opacity: float = 1.0 + """Opacity of the shape.""" @classmethod def from_data(cls: Type[BaseShapeSelf], data: ShapeData) -> BaseShapeSelf: @@ -36,12 +47,23 @@ def from_data(cls: Type[BaseShapeSelf], data: ShapeData) -> BaseShapeSelf: def update_from_data(self, data: ShapeData) -> None: if "style" in data: self.style.update_from_data(data["style"]) + + elif "props" in data: + self.style.update_from_data(data["props"]) + if "childIndex" in data: self.childIndex = data["childIndex"] + if "point" in data: point = data["point"] self.point = Position(point[0], point[1]) + elif "x" in data and "y" in data: + self.point = Position(data["x"], data["y"]) + + if "opacity" in data: + self.style.opacity = data["opacity"] + @attr.s(order=False, slots=True, auto_attribs=True) class SizedShapeProto(BaseShapeProto, Protocol): @@ -56,6 +78,12 @@ def update_from_data(self, data: ShapeData) -> None: if "size" in data: self.size = Size(data["size"]) + if "props" in data: + props = data["props"] + + if "w" in props and "h" in props and "growY" in props: + self.size = Size(props["w"], props["h"] + props["growY"]) + @attr.s(order=False, slots=True, auto_attribs=True) class RotatableShapeProto(SizedShapeProto, Protocol): @@ -78,6 +106,15 @@ class LabelledShapeProto(RotatableShapeProto, Protocol): labelPoint: Position = Position(0.5, 0.5) """The position of the label within the shape. Ranges from 0 to 1.""" + align: AlignStyle = AlignStyle.MIDDLE + """Horizontal alignment of the label.""" + + verticalAlign: AlignStyle = AlignStyle.MIDDLE + """Vertical alignment of the label.""" + + geo: GeoShape = GeoShape.NONE + """Which geo type the shape is, if any.""" + def label_offset(self) -> Position: """Calculate the offset needed when drawing the label for most shapes.""" return Position( @@ -92,6 +129,17 @@ def update_from_data(self, data: ShapeData) -> None: self.label = data["label"] if data["label"] != "" else None if "labelPoint" in data: self.labelPoint = Position(data["labelPoint"]) + if "props" in data: + props = data["props"] + + if "text" in props: + self.label = props["text"] + if "align" in props: + self.align = AlignStyle(props["align"]) + if "verticalAlign" in props: + self.verticalAlign = AlignStyle(props["verticalAlign"]) + if "geo" in props: + self.geo = GeoShape(props["geo"]) def shape_sort_key(shape: BaseShapeProto) -> float: @@ -115,8 +163,57 @@ def update_from_data(self, data: ShapeData) -> None: self.points.append((point[0], point[1], point[2])) else: self.points.append((point[0], point[1])) + + elif "props" in data and "segments" in data["props"]: + self.points = [] + for segment in data["props"]["segments"]: + if ( + isinstance(segment, dict) + and "points" in segment + and isinstance(segment["points"], list) + ): + for point in segment["points"]: + if isinstance(point, dict) and "x" in point and "y" in point: + if "z" in point: + self.points.append((point["x"], point["y"], point["z"])) + else: + self.points.append((point["x"], point["y"])) + if "isComplete" in data: self.isComplete = data["isComplete"] + elif "props" in data and "isComplete" in data["props"]: + self.isComplete = data["props"]["isComplete"] + + +@attr.s(order=False, slots=True, auto_attribs=True) +class HighlighterShape(RotatableShapeProto): + points: DrawPoints = [] + """List of input points from the drawing tool.""" + isComplete: bool = False + """Whether the last point in the line is present (pen lifted).""" + + def update_from_data(self, data: ShapeData) -> None: + super().update_from_data(data) + + if "props" in data and "segments" in data["props"]: + self.points = [] + for segment in data["props"]["segments"]: + if ( + isinstance(segment, dict) + and "points" in segment + and isinstance(segment["points"], list) + ): + for point in segment["points"]: + if isinstance(point, dict) and "x" in point and "y" in point: + if "z" in point: + self.points.append((point["x"], point["y"], point["z"])) + else: + self.points.append((point["x"], point["y"])) + + if "isComplete" in data: + self.isComplete = data["isComplete"] + elif "props" in data and "isComplete" in data["props"]: + self.isComplete = data["props"]["isComplete"] @attr.s(order=False, slots=True, auto_attribs=True) @@ -125,6 +222,11 @@ class RectangleShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) +@attr.s(order=False, slots=True, auto_attribs=True) +class RectangleGeo(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + @attr.s(order=False, slots=True, auto_attribs=True) class EllipseShape(LabelledShapeProto): radius: Tuple[float, float] = (1.0, 1.0) @@ -141,12 +243,32 @@ def update_from_data(self, data: ShapeData) -> None: self.radius = (radius[0], radius[1]) +@attr.s(order=False, slots=True, auto_attribs=True) +class EllipseGeo(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + @attr.s(order=False, slots=True, auto_attribs=True) class TriangleShape(LabelledShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) +@attr.s(order=False, slots=True, auto_attribs=True) +class TriangleGeo(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class Diamond(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class Trapezoid(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + @attr.s(order=False, slots=True, auto_attribs=True) class TextShape(RotatableShapeProto): text: str = "" @@ -158,6 +280,58 @@ def update_from_data(self, data: ShapeData) -> None: self.text = data["text"] +@attr.s(order=False, slots=True, auto_attribs=True) +class TextShape_v2(RotatableShapeProto): + text: str = "" + + def update_from_data(self, data: ShapeData) -> None: + super().update_from_data(data) + + if "props" in data: + if "text" in data["props"]: + self.text = data["props"]["text"] + + +@attr.s(order=False, slots=True, auto_attribs=True) +class Rhombus(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class Hexagon(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class Cloud(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class Star(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class Oval(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class XBox(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class CheckBox(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class ArrowGeo(LabelledShapeProto): + size: Size = Size(1.0, 1.0) + + @attr.s(order=False, slots=True, auto_attribs=True) class GroupShape(BaseShapeProto): pass @@ -177,6 +351,30 @@ def update_from_data(self, data: ShapeData) -> None: self.text = data["text"] +@attr.s(order=False, slots=True, auto_attribs=True) +class StickyShape_v2(RotatableShapeProto): + text: str = "" + align: AlignStyle = AlignStyle.MIDDLE + verticalAlign: AlignStyle = AlignStyle.MIDDLE + size: Size = Size(200.0, 200.0) + + def update_from_data(self, data: ShapeData) -> None: + super().update_from_data(data) + + if "props" in data: + props = data["props"] + if "text" in props: + self.text = props["text"] + if "align" in props: + self.align = AlignStyle(props["align"]) + if "verticalAlign" in props: + self.verticalAlign = AlignStyle(props["verticalAlign"]) + if "growY" in props: + self.size = Size(self.size.width, self.size.height + props["growY"]) + if props["growY"] != 0: + self.verticalAlign = AlignStyle.START + + @attr.s(order=False, slots=True, auto_attribs=True, init=False) class ArrowHandles: start: Position @@ -208,6 +406,43 @@ def update_from_data(self, data: Dict[str, HandleData]) -> None: pass +@attr.s(order=False, slots=True, auto_attribs=True, init=False) +class LineHandles: + start: Position + controlPoint: Position + end: Position + + def __init__( + self, + start: Position = Position(0.0, 0.0), + end: Position = Position(1.0, 1.0), + controlPoint: Position = Position(0.5, 0.5), + ) -> None: + self.start = start + self.controlPoint = controlPoint + self.end = end + + def update_from_data(self, data: Dict[str, HandleData]) -> None: + + if "start" in data: + if "point" in data["start"]: + self.start = Position(data["start"]["point"]) + elif "x" in data["start"] and "y" in data["start"]: + self.start = Position(data["start"]["x"], data["start"]["y"]) + + if "end" in data: + if "point" in data["end"]: + self.end = Position(data["end"]["point"]) + elif "x" in data["end"] and "y" in data["end"]: + self.end = Position(data["end"]["x"], data["end"]["y"]) + + if "handle:a1V" in data: + if "x" in data["handle:a1V"] and "y" in data["handle:a1V"]: + self.controlPoint = Position( + data["handle:a1V"]["x"], data["handle:a1V"]["y"] + ) + + @attr.s(order=False, slots=True, auto_attribs=True) class ArrowDecorations: start: Optional[Decoration] = None @@ -239,20 +474,94 @@ def update_from_data(self, data: ShapeData) -> None: self.decorations.update_from_data(data["decorations"]) +@attr.s(order=False, slots=True, auto_attribs=True) +class ArrowShape_v2(LabelledShapeProto): + bend: float = 0.0 + handles: ArrowHandles = attr.Factory(ArrowHandles) + """Locations of the line start, end, and bend points.""" + decorations: ArrowDecorations = attr.Factory(ArrowDecorations) + """Whether the arrow head decorations are present on start/end of the line.""" + + def update_from_data(self, data: ShapeData) -> None: + super().update_from_data(data) + + if "props" in data: + props = data["props"] + + if "bend" in props: + self.bend = props["bend"] + if "start" in props: + if "x" in props["start"] and "y" in props["start"]: + self.handles.start = Position( + props["start"]["x"], props["start"]["y"] + ) + if "end" in props: + if "x" in props["end"] and "y" in props["end"]: + self.handles.end = Position(props["end"]["x"], props["end"]["y"]) + if "arrowheadStart" in props: + self.decorations.start = Decoration(props["arrowheadStart"]) + if "arrowheadEnd" in props: + self.decorations.end = Decoration(props["arrowheadEnd"]) + + +@attr.s(order=False, slots=True, auto_attribs=True) +class LineShape(LabelledShapeProto): + handles: LineHandles = attr.Factory(LineHandles) + """Locations of the line start, end, and bend points.""" + spline: SplineType = SplineType.NONE + """Whether a bent line is straight or curved.""" + + def update_from_data(self, data: ShapeData) -> None: + super().update_from_data(data) + + if "props" in data: + if "handles" in data["props"]: + self.handles.update_from_data(data["props"]["handles"]) + if "spline" in data["props"]: + spline_prop = data["props"]["spline"] + + if spline_prop == "line": + self.spline = SplineType.LINE + elif spline_prop == "cubic": + self.spline = SplineType.CUBIC + else: + self.spline = SplineType.NONE + + Shape = Union[ + ArrowShape, + ArrowShape_v2, + Diamond, DrawShape, - RectangleShape, + EllipseGeo, EllipseShape, - TriangleShape, - ArrowShape, - TextShape, GroupShape, + HighlighterShape, + LineShape, + RectangleGeo, + RectangleShape, StickyShape, + StickyShape_v2, + TextShape, + TextShape_v2, + Trapezoid, + TriangleGeo, + TriangleShape, + Rhombus, + Hexagon, + Cloud, + Star, + Oval, + XBox, + CheckBox, + ArrowGeo, ] -def parse_shape_from_data(data: ShapeData) -> Shape: +def parse_shape_from_data(data: ShapeData, bbb_version: Version) -> Shape: type = data["type"] + is_tldraw_v2 = bbb_version >= Version("3.0.0") + if type == "draw": return DrawShape.from_data(data) elif type == "rectangle": @@ -262,13 +571,61 @@ def parse_shape_from_data(data: ShapeData) -> Shape: elif type == "triangle": return TriangleShape.from_data(data) elif type == "arrow": - return ArrowShape.from_data(data) + if not is_tldraw_v2: + return ArrowShape.from_data(data) + else: + return ArrowShape_v2.from_data(data) elif type == "text": - return TextShape.from_data(data) + if not is_tldraw_v2: + return TextShape.from_data(data) + else: + return TextShape_v2.from_data(data) elif type == "group": return GroupShape.from_data(data) elif type == "sticky": return StickyShape.from_data(data) + elif type == "note": + return StickyShape_v2.from_data(data) + elif type == "line": + return LineShape.from_data(data) + elif type == "highlight": + return HighlighterShape.from_data(data) + elif type == "geo": + if "geo" in data["props"]: + geo_type = GeoShape(data["props"]["geo"]) + + if geo_type is GeoShape.DIAMOND: + return Diamond.from_data(data) + if geo_type is GeoShape.ELLIPSE: + return EllipseGeo.from_data(data) + if geo_type is GeoShape.RECTANGLE: + return RectangleGeo.from_data(data) + if geo_type is GeoShape.TRIANGLE: + return TriangleGeo.from_data(data) + if geo_type is GeoShape.TRAPEZOID: + return Trapezoid.from_data(data) + if geo_type is GeoShape.RHOMBUS: + return Rhombus.from_data(data) + if geo_type is GeoShape.HEXAGON: + return Hexagon.from_data(data) + if geo_type is GeoShape.CLOUD: + return Cloud.from_data(data) + if geo_type is GeoShape.STAR: + return Star.from_data(data) + if geo_type is GeoShape.OVAL: + return Oval.from_data(data) + if geo_type is GeoShape.CHECKBOX: + return CheckBox.from_data(data) + if geo_type is GeoShape.XBOX: + return XBox.from_data(data) + if geo_type in [ + GeoShape.ARROW_DOWN, + GeoShape.ARROW_LEFT, + GeoShape.ARROW_RIGHT, + GeoShape.ARROW_UP, + ]: + return ArrowGeo.from_data(data) + raise Exception(f"Unknown geo shape: {type}") else: raise Exception(f"Unknown shape type: {type}") diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow.py b/bbb_presentation_video/renderer/tldraw/shape/arrow.py index b319095..5887940 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow.py @@ -36,17 +36,11 @@ circle_from_three_points, draw_smooth_path, get_perfect_dash_props, - get_sweep, lerp_angles, rounded_rect, + get_arc_length, ) - -def get_arc_length(C: Position, r: float, A: Position, B: Position) -> float: - sweep = get_sweep(C, A, B) - return r * tau * (sweep / tau) - - CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py new file mode 100644 index 0000000..868aae4 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py @@ -0,0 +1,242 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from math import tau +from typing import TypeVar + +import cairo + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.intersect import ( + intersect_circle_circle, + intersect_circle_line_segment, +) +from bbb_presentation_video.renderer.tldraw.shape import ( + ArrowShape_v2, + apply_shape_rotation, +) +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + Decoration, + circle_from_three_points, + get_perfect_dash_props, + get_arc_length, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def curved_arrow_shaft( + ctx: cairo.Context[CairoSomeSurface], + start: Position, + end: Position, + center: Position, + radius: float, + bend: float, +) -> None: + start_angle = vec.angle(center, start) + end_angle = vec.angle(center, end) + + ctx.new_sub_path() + + if bend > 0: + ctx.arc(center.x, center.y, radius, start_angle, end_angle) + else: + ctx.arc_negative(center.x, center.y, radius, start_angle, end_angle) + + +def straight_arrow_head( + ctx: cairo.Context[CairoSomeSurface], + a: Position, + b: Position, + r: float, +) -> None: + ints = intersect_circle_line_segment(a, r, a, b).points + if len(ints) == 0: + print("\t\tCould not find an intersection for the arrow head.") + left = a + right = a + else: + int = ints[0] + left = Position(vec.rot_with(int, a, tau / 12)) + right = Position(vec.rot_with(int, a, -tau / 12)) + + ctx.move_to(left.x, left.y) + ctx.line_to(a.x, a.y) + ctx.line_to(right.x, right.y) + + +def curved_arrow_head( + ctx: cairo.Context[CairoSomeSurface], + a: Position, + r1: float, + C: Position, + r2: float, + sweep: bool, +) -> None: + ints = intersect_circle_circle(a, r1 * 0.618, C, r2).points + if len(ints) == 0: + print("\t\tCould not find an intersection for the arrow head.") + left = a + right = a + else: + int = ints[0] if sweep else ints[1] + left = Position(vec.nudge(vec.rot_with(int, a, tau / 12), a, r1 * -0.382)) + right = Position(vec.nudge(vec.rot_with(int, a, -tau / 12), a, r1 * -0.382)) + + ctx.move_to(left.x, left.y) + ctx.line_to(a.x, a.y) + ctx.line_to(right.x, right.y) + + +def straight_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowShape_v2) -> float: + style = shape.style + start = shape.handles.start + end = shape.handles.end + deco_start = shape.decorations.start + deco_end = shape.decorations.end + opacity = style.opacity + arrow_dist = vec.dist(start, end) + if arrow_dist < 2: + return arrow_dist + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + + stroke = STROKES[style.color] + + # Path between start and end points + ctx.save() + + ctx.move_to(start[0], start[1]) + ctx.line_to(end[0], end[1]) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + dash_array, dash_offset = get_perfect_dash_props( + arrow_dist, stroke_width * 1.618, style.dash, snap=2, outset=False + ) + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.stroke() + ctx.restore() + + # Arrowheads + arrow_head_len = min(arrow_dist / 3, stroke_width * 8) + if deco_start is Decoration.ARROW: + straight_arrow_head(ctx, start, end, arrow_head_len) + if deco_end is Decoration.ARROW: + straight_arrow_head(ctx, end, start, arrow_head_len) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.stroke() + + return arrow_dist + + +def get_midpoint(start: Position, end: Position, bend: float) -> Position: + mid = [(start.x + end.x) / 2, (start.y + end.y) / 2] + + unit_vector = vec.uni([end.x - start.x, end.y - start.y]) + + unit_rotated = [unit_vector[1], -unit_vector[0]] + bend_offset = [unit_rotated[0] * -bend, unit_rotated[1] * -bend] + + middle = Position(mid[0] + bend_offset[0], mid[1] + bend_offset[1]) + + return middle + + +def curved_arrow( + ctx: cairo.Context[CairoSomeSurface], + shape: ArrowShape_v2, +) -> float: + style = shape.style + start = shape.handles.start + bend = shape.handles.bend + end = shape.handles.end + arrow_bend = shape.bend + deco_start = shape.decorations.start + deco_end = shape.decorations.end + + arrow_dist = vec.dist(start, end) + if arrow_dist < 2: + return arrow_dist + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + # Calculate a path as a segment of a circle passing through the three points start, bend, and end + center, radius = circle_from_three_points(start, bend, end) + length = get_arc_length(center, radius, start, end) + stroke = STROKES[style.color] + + ctx.save() + + arrow_bend = -arrow_bend + + curved_arrow_shaft(ctx, start, end, center, radius, arrow_bend) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + dash_array, dash_offset = get_perfect_dash_props( + abs(length), sw, style.dash, snap=2, outset=False + ) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + ctx.restore() + + # Arrowheads + arrow_head_len = min(arrow_dist / 3, stroke_width * 8) + + sweepFlag = ( + (end.x - start.x) * (bend.y - start.y) - (bend.x - start.x) * (end.y - start.y) + ) < 0 + + if deco_start is not Decoration.NONE: + curved_arrow_head(ctx, start, arrow_head_len, center, radius, sweepFlag) + if deco_end is not Decoration.NONE: + curved_arrow_head(ctx, end, arrow_head_len, center, radius, sweepFlag) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + + return abs(length) + + +def finalize_arrow_v2( + ctx: cairo.Context[CairoSomeSurface], + id: str, + shape: ArrowShape_v2, +) -> None: + print(f"\tTldraw: Finalizing Arrow (v2): {id}") + + apply_shape_rotation(ctx, shape) + + start = shape.handles.start + end = shape.handles.end + + is_straight_line = shape.bend == 0.0 + shape.handles.bend = get_midpoint(start, end, shape.bend) + + ctx.push_group() + if is_straight_line: + straight_arrow(ctx, shape) + else: + curved_arrow(ctx, shape) + + arrow_pattern = ctx.pop_group() + ctx.set_source(arrow_pattern) + ctx.paint() diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py new file mode 100644 index 0000000..868aae4 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py @@ -0,0 +1,242 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from math import tau +from typing import TypeVar + +import cairo + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.intersect import ( + intersect_circle_circle, + intersect_circle_line_segment, +) +from bbb_presentation_video.renderer.tldraw.shape import ( + ArrowShape_v2, + apply_shape_rotation, +) +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + Decoration, + circle_from_three_points, + get_perfect_dash_props, + get_arc_length, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def curved_arrow_shaft( + ctx: cairo.Context[CairoSomeSurface], + start: Position, + end: Position, + center: Position, + radius: float, + bend: float, +) -> None: + start_angle = vec.angle(center, start) + end_angle = vec.angle(center, end) + + ctx.new_sub_path() + + if bend > 0: + ctx.arc(center.x, center.y, radius, start_angle, end_angle) + else: + ctx.arc_negative(center.x, center.y, radius, start_angle, end_angle) + + +def straight_arrow_head( + ctx: cairo.Context[CairoSomeSurface], + a: Position, + b: Position, + r: float, +) -> None: + ints = intersect_circle_line_segment(a, r, a, b).points + if len(ints) == 0: + print("\t\tCould not find an intersection for the arrow head.") + left = a + right = a + else: + int = ints[0] + left = Position(vec.rot_with(int, a, tau / 12)) + right = Position(vec.rot_with(int, a, -tau / 12)) + + ctx.move_to(left.x, left.y) + ctx.line_to(a.x, a.y) + ctx.line_to(right.x, right.y) + + +def curved_arrow_head( + ctx: cairo.Context[CairoSomeSurface], + a: Position, + r1: float, + C: Position, + r2: float, + sweep: bool, +) -> None: + ints = intersect_circle_circle(a, r1 * 0.618, C, r2).points + if len(ints) == 0: + print("\t\tCould not find an intersection for the arrow head.") + left = a + right = a + else: + int = ints[0] if sweep else ints[1] + left = Position(vec.nudge(vec.rot_with(int, a, tau / 12), a, r1 * -0.382)) + right = Position(vec.nudge(vec.rot_with(int, a, -tau / 12), a, r1 * -0.382)) + + ctx.move_to(left.x, left.y) + ctx.line_to(a.x, a.y) + ctx.line_to(right.x, right.y) + + +def straight_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowShape_v2) -> float: + style = shape.style + start = shape.handles.start + end = shape.handles.end + deco_start = shape.decorations.start + deco_end = shape.decorations.end + opacity = style.opacity + arrow_dist = vec.dist(start, end) + if arrow_dist < 2: + return arrow_dist + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + + stroke = STROKES[style.color] + + # Path between start and end points + ctx.save() + + ctx.move_to(start[0], start[1]) + ctx.line_to(end[0], end[1]) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + dash_array, dash_offset = get_perfect_dash_props( + arrow_dist, stroke_width * 1.618, style.dash, snap=2, outset=False + ) + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.stroke() + ctx.restore() + + # Arrowheads + arrow_head_len = min(arrow_dist / 3, stroke_width * 8) + if deco_start is Decoration.ARROW: + straight_arrow_head(ctx, start, end, arrow_head_len) + if deco_end is Decoration.ARROW: + straight_arrow_head(ctx, end, start, arrow_head_len) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.stroke() + + return arrow_dist + + +def get_midpoint(start: Position, end: Position, bend: float) -> Position: + mid = [(start.x + end.x) / 2, (start.y + end.y) / 2] + + unit_vector = vec.uni([end.x - start.x, end.y - start.y]) + + unit_rotated = [unit_vector[1], -unit_vector[0]] + bend_offset = [unit_rotated[0] * -bend, unit_rotated[1] * -bend] + + middle = Position(mid[0] + bend_offset[0], mid[1] + bend_offset[1]) + + return middle + + +def curved_arrow( + ctx: cairo.Context[CairoSomeSurface], + shape: ArrowShape_v2, +) -> float: + style = shape.style + start = shape.handles.start + bend = shape.handles.bend + end = shape.handles.end + arrow_bend = shape.bend + deco_start = shape.decorations.start + deco_end = shape.decorations.end + + arrow_dist = vec.dist(start, end) + if arrow_dist < 2: + return arrow_dist + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + # Calculate a path as a segment of a circle passing through the three points start, bend, and end + center, radius = circle_from_three_points(start, bend, end) + length = get_arc_length(center, radius, start, end) + stroke = STROKES[style.color] + + ctx.save() + + arrow_bend = -arrow_bend + + curved_arrow_shaft(ctx, start, end, center, radius, arrow_bend) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + dash_array, dash_offset = get_perfect_dash_props( + abs(length), sw, style.dash, snap=2, outset=False + ) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + ctx.restore() + + # Arrowheads + arrow_head_len = min(arrow_dist / 3, stroke_width * 8) + + sweepFlag = ( + (end.x - start.x) * (bend.y - start.y) - (bend.x - start.x) * (end.y - start.y) + ) < 0 + + if deco_start is not Decoration.NONE: + curved_arrow_head(ctx, start, arrow_head_len, center, radius, sweepFlag) + if deco_end is not Decoration.NONE: + curved_arrow_head(ctx, end, arrow_head_len, center, radius, sweepFlag) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + + return abs(length) + + +def finalize_arrow_v2( + ctx: cairo.Context[CairoSomeSurface], + id: str, + shape: ArrowShape_v2, +) -> None: + print(f"\tTldraw: Finalizing Arrow (v2): {id}") + + apply_shape_rotation(ctx, shape) + + start = shape.handles.start + end = shape.handles.end + + is_straight_line = shape.bend == 0.0 + shape.handles.bend = get_midpoint(start, end, shape.bend) + + ctx.push_group() + if is_straight_line: + straight_arrow(ctx, shape) + else: + curved_arrow(ctx, shape) + + arrow_pattern = ctx.pop_group() + ctx.set_source(arrow_pattern) + ctx.paint() diff --git a/bbb_presentation_video/renderer/tldraw/shape/draw.py b/bbb_presentation_video/renderer/tldraw/shape/draw.py index f528d67..cb352bc 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/draw.py +++ b/bbb_presentation_video/renderer/tldraw/shape/draw.py @@ -17,10 +17,14 @@ FILLS, STROKE_WIDTHS, STROKES, + COLORS, DashStyle, + FillStyle, + ColorStyle, draw_smooth_path, draw_smooth_stroke_point_path, draw_stroke_points, + pattern_fill, ) CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) @@ -36,7 +40,6 @@ def finalize_draw( points = shape.points style = shape.style is_complete = shape.isComplete - stroke = STROKES[style.color] fill = FILLS[style.color] stroke_width = STROKE_WIDTHS[style.size] @@ -48,7 +51,7 @@ def finalize_draw( if very_small: sw = 1 + stroke_width ctx.arc(0, 0, sw, 0, tau) - ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) ctx.fill_preserve() ctx.set_line_cap(cairo.LineCap.ROUND) ctx.set_line_join(cairo.LineJoin.ROUND) @@ -60,7 +63,7 @@ def finalize_draw( style.isFilled and len(points) > 3 and vec.dist(points[0], points[-1]) < stroke_width * 2 - ) + ) or (style.isClosed and style.fill is not FillStyle.NONE) stroke_points = draw_stroke_points(shape.points, stroke_width, is_complete) @@ -68,7 +71,16 @@ def finalize_draw( # Shape is configured to be filled, and is fillable draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - ctx.set_source_rgb(fill.r, fill.g, fill.b) + if style.fill is FillStyle.SEMI: + fill = COLORS[ColorStyle.SEMI] + ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) + elif style.fill is FillStyle.PATTERN: + pattern = pattern_fill(fill) + ctx.set_source(pattern) + else: + # Solid fill + ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) + ctx.fill() if style.dash is DashStyle.DRAW: @@ -77,8 +89,7 @@ def finalize_draw( if len(shape.points[0]) == 2: simulate_pressure = True else: - # Work around python/mypy#1178 - first_point = cast(Tuple[float, float, float], shape.points[0]) + first_point = shape.points[0] if first_point[2] == 0.5: simulate_pressure = True @@ -94,13 +105,14 @@ def finalize_draw( draw_smooth_path(ctx, stroke_outline_points) - ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) ctx.fill_preserve() ctx.set_line_cap(cairo.LineCap.ROUND) ctx.set_line_join(cairo.LineJoin.ROUND) ctx.set_line_width(stroke_width / 2) ctx.stroke() + else: # Normal stroked path, possibly with dash or dot pattern if style.dash is DashStyle.DOTTED: @@ -113,5 +125,5 @@ def finalize_draw( ctx.set_line_cap(cairo.LineCap.ROUND) ctx.set_line_join(cairo.LineJoin.ROUND) ctx.set_line_width(1 + stroke_width * 1.5) - ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) ctx.stroke() diff --git a/bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py b/bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py new file mode 100644 index 0000000..8e670d3 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from math import tau +from typing import TypeVar + +import cairo + +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + DrawShape, + HighlighterShape, + apply_shape_rotation, +) +from bbb_presentation_video.renderer.tldraw.utils import ( + HIGHLIGHT_COLORS, + STROKE_WIDTHS, + draw_smooth_stroke_point_path, + draw_stroke_points, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def finalize_highlight( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: HighlighterShape +) -> None: + print(f"\tTldraw: Finalizing Highlight: {id}") + + apply_shape_rotation(ctx, shape) + + style = shape.style + is_complete = shape.isComplete + + stroke = HIGHLIGHT_COLORS[style.color] + stroke_width = STROKE_WIDTHS[style.size] * 5 + opacity = 0.7 + + # For very short lines, draw a point instead of a line + size = shape.size + very_small = size.width <= stroke_width / 2 and size.height <= stroke_width < 2 + + if very_small: + sw = 1 + stroke_width + ctx.arc(0, 0, sw, 0, tau) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.fill_preserve() + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_line_width(1) + ctx.stroke() + return + + stroke_points = draw_stroke_points(shape.points, stroke_width, is_complete) + + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_line_width(1 + stroke_width * 1.5) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.stroke() diff --git a/bbb_presentation_video/renderer/tldraw/shape/highlighter.py b/bbb_presentation_video/renderer/tldraw/shape/highlighter.py new file mode 100644 index 0000000..8e670d3 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/highlighter.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from math import tau +from typing import TypeVar + +import cairo + +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import ( + DrawShape, + HighlighterShape, + apply_shape_rotation, +) +from bbb_presentation_video.renderer.tldraw.utils import ( + HIGHLIGHT_COLORS, + STROKE_WIDTHS, + draw_smooth_stroke_point_path, + draw_stroke_points, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def finalize_highlight( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: HighlighterShape +) -> None: + print(f"\tTldraw: Finalizing Highlight: {id}") + + apply_shape_rotation(ctx, shape) + + style = shape.style + is_complete = shape.isComplete + + stroke = HIGHLIGHT_COLORS[style.color] + stroke_width = STROKE_WIDTHS[style.size] * 5 + opacity = 0.7 + + # For very short lines, draw a point instead of a line + size = shape.size + very_small = size.width <= stroke_width / 2 and size.height <= stroke_width < 2 + + if very_small: + sw = 1 + stroke_width + ctx.arc(0, 0, sw, 0, tau) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.fill_preserve() + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_line_width(1) + ctx.stroke() + return + + stroke_points = draw_stroke_points(shape.points, stroke_width, is_complete) + + draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) + + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_line_width(1 + stroke_width * 1.5) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) + ctx.stroke() diff --git a/bbb_presentation_video/renderer/tldraw/shape/line 2.py b/bbb_presentation_video/renderer/tldraw/shape/line 2.py new file mode 100644 index 0000000..03be5c5 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/line 2.py @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from enum import Enum + +from math import floor +from random import Random +from typing import Callable, List, Optional, Sequence, TypeVar + +import cairo +from perfect_freehand import get_stroke + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.easings import ( + ease_out_quad, +) +from bbb_presentation_video.renderer.tldraw.shape import ( + LineShape, + apply_shape_rotation, +) +from bbb_presentation_video.renderer.tldraw.shape.text import finalize_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + SplineType, + Style, + bezier_length, + draw_smooth_path, + get_perfect_dash_props, + lerp_angles, + rounded_rect, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def freehand_line_shaft( + ctx: cairo.Context[CairoSomeSurface], + id: str, + style: Style, + start: Position, + end: Position, +) -> None: + random = Random(id) + stroke_width = STROKE_WIDTHS[style.size] + + stroke_outline_points = get_stroke( + [start, end], + size=stroke_width, + thinning=0.618 + random.uniform(-0.2, 0.2), + easing=ease_out_quad, + simulate_pressure=True, + streamline=0, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + +def curved_freehand_line_shaft( + ctx: cairo.Context[CairoSomeSurface], + id: str, + style: Style, + start: Position, + end: Position, + center: Position, + radius: float, + length: float, + easing: Callable[[float], float], +) -> None: + + random = Random(id) + stroke_width = STROKE_WIDTHS[style.size] + start_angle = vec.angle(center, start) + end_angle = vec.angle(center, end) + + points: List[Sequence[float]] = [start] + count = 8 + floor((abs(length) / 20) * 1 + random.uniform(-0.5, 0.5)) + for i in range(count): + t = easing(i / count) + angle = lerp_angles(start_angle, end_angle, t) + points.append(vec.to_fixed(vec.nudge_at_angle(center, angle, radius))) + points.append(end) + + stroke_outline_points = get_stroke( + points, + size=1 + stroke_width, + thinning=0.618 + random.uniform(-0.2, 0.2), + easing=ease_out_quad, + simulate_pressure=False, + streamline=0, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + +def straight_line( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape +) -> float: + style = shape.style + start = shape.handles.start + end = shape.handles.end + + line_dist = vec.dist(start, end) + if line_dist < 2: + return line_dist + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + + stroke = STROKES[style.color] + + # Path between start and end points + ctx.save() + if style.dash is DashStyle.DRAW: + freehand_line_shaft(ctx, id, style, start, end) + + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.fill_preserve() + ctx.set_line_width(sw / 2) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + else: + ctx.move_to(start[0], start[1]) + ctx.line_to(end[0], end[1]) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + dash_array, dash_offset = get_perfect_dash_props( + line_dist, stroke_width * 1.618, style.dash, snap=2, outset=False + ) + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.stroke() + ctx.restore() + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.stroke() + + return line_dist + + +def bent_line(ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape) -> float: + style = shape.style + start = shape.handles.start + controlPoint = shape.handles.controlPoint + end = shape.handles.end + + # Calculate distances + line_dist_start_control = vec.dist(start, controlPoint) + line_dist_control_end = vec.dist(controlPoint, end) + + # Early return if both lines are too short + if line_dist_start_control < 2 and line_dist_control_end < 2: + return line_dist_start_control + line_dist_control_end + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + stroke = STROKES[style.color] + + ctx.save() + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + + if style.dash is DashStyle.DRAW: + freehand_line_shaft(ctx, id, style, start, controlPoint) + freehand_line_shaft(ctx, id, style, controlPoint, end) + + ctx.fill_preserve() + ctx.stroke() + + else: + if style.dash is DashStyle.DOTTED: + ctx.set_dash([0, stroke_width * 4]) + elif style.dash is DashStyle.DASHED: + ctx.set_dash([stroke_width * 4, stroke_width * 4]) + + # Draw first segment: start to control point + ctx.move_to(start.x, start.y) + ctx.line_to(controlPoint.x, controlPoint.y) + ctx.stroke() + + # Draw second segment: control point to end + ctx.move_to(controlPoint.x, controlPoint.y) + ctx.line_to(end.x, end.y) + ctx.stroke() + + ctx.restore() + + return line_dist_start_control + line_dist_control_end + + +def curved_line( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape +) -> float: + style = shape.style + start = shape.handles.start + controlPoint = shape.handles.controlPoint + end = shape.handles.end + + line_dist = vec.dist(start, end) + + if line_dist < 2: + return line_dist + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + + # Calculate a path passing through the control point + # t is fixed at 0.5 for the midpoint + t = 0.5 + + # Compute adjusted control points + b_x = (controlPoint.x - (1 - t) ** 3 * start.x - t**3 * end.x) / (3 * (1 - t) * t) + b_y = (controlPoint.y - (1 - t) ** 3 * start.y - t**3 * end.y) / (3 * (1 - t) * t) + c_x = b_x + c_y = b_y + + # Move to the start position + ctx.move_to(start.x, start.y) + + # Draw cubic Bézier curve + ctx.curve_to(b_x, b_y, c_x, c_y, end.x, end.y) + + # Get the length of the curve + length = bezier_length(start, controlPoint, end) + stroke = STROKES[style.color] + + ctx.save() + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + + dash_array, dash_offset = get_perfect_dash_props( + abs(length), sw, style.dash, snap=2, outset=False + ) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + + ctx.restore() + + return abs(length) + + +def finalize_line( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape +) -> None: + print(f"\tTldraw: Finalizing Line: {id}") + + apply_shape_rotation(ctx, shape) + + ctx.push_group() + + if shape.spline == SplineType.CUBIC: + curved_line(ctx, id, shape) + elif shape.spline == SplineType.LINE: + bent_line(ctx, id, shape) + else: + straight_line(ctx, id, shape) + + line_pattern = ctx.pop_group() + + ctx.set_source(line_pattern) + ctx.paint() diff --git a/bbb_presentation_video/renderer/tldraw/shape/line.py b/bbb_presentation_video/renderer/tldraw/shape/line.py new file mode 100644 index 0000000..03be5c5 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/line.py @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations +from enum import Enum + +from math import floor +from random import Random +from typing import Callable, List, Optional, Sequence, TypeVar + +import cairo +from perfect_freehand import get_stroke + +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.easings import ( + ease_out_quad, +) +from bbb_presentation_video.renderer.tldraw.shape import ( + LineShape, + apply_shape_rotation, +) +from bbb_presentation_video.renderer.tldraw.shape.text import finalize_label +from bbb_presentation_video.renderer.tldraw.utils import ( + STROKE_WIDTHS, + STROKES, + DashStyle, + SplineType, + Style, + bezier_length, + draw_smooth_path, + get_perfect_dash_props, + lerp_angles, + rounded_rect, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def freehand_line_shaft( + ctx: cairo.Context[CairoSomeSurface], + id: str, + style: Style, + start: Position, + end: Position, +) -> None: + random = Random(id) + stroke_width = STROKE_WIDTHS[style.size] + + stroke_outline_points = get_stroke( + [start, end], + size=stroke_width, + thinning=0.618 + random.uniform(-0.2, 0.2), + easing=ease_out_quad, + simulate_pressure=True, + streamline=0, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + +def curved_freehand_line_shaft( + ctx: cairo.Context[CairoSomeSurface], + id: str, + style: Style, + start: Position, + end: Position, + center: Position, + radius: float, + length: float, + easing: Callable[[float], float], +) -> None: + + random = Random(id) + stroke_width = STROKE_WIDTHS[style.size] + start_angle = vec.angle(center, start) + end_angle = vec.angle(center, end) + + points: List[Sequence[float]] = [start] + count = 8 + floor((abs(length) / 20) * 1 + random.uniform(-0.5, 0.5)) + for i in range(count): + t = easing(i / count) + angle = lerp_angles(start_angle, end_angle, t) + points.append(vec.to_fixed(vec.nudge_at_angle(center, angle, radius))) + points.append(end) + + stroke_outline_points = get_stroke( + points, + size=1 + stroke_width, + thinning=0.618 + random.uniform(-0.2, 0.2), + easing=ease_out_quad, + simulate_pressure=False, + streamline=0, + last=True, + ) + draw_smooth_path(ctx, stroke_outline_points, closed=True) + + +def straight_line( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape +) -> float: + style = shape.style + start = shape.handles.start + end = shape.handles.end + + line_dist = vec.dist(start, end) + if line_dist < 2: + return line_dist + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + + stroke = STROKES[style.color] + + # Path between start and end points + ctx.save() + if style.dash is DashStyle.DRAW: + freehand_line_shaft(ctx, id, style, start, end) + + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.fill_preserve() + ctx.set_line_width(sw / 2) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.stroke() + else: + ctx.move_to(start[0], start[1]) + ctx.line_to(end[0], end[1]) + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + dash_array, dash_offset = get_perfect_dash_props( + line_dist, stroke_width * 1.618, style.dash, snap=2, outset=False + ) + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.stroke() + ctx.restore() + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + ctx.stroke() + + return line_dist + + +def bent_line(ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape) -> float: + style = shape.style + start = shape.handles.start + controlPoint = shape.handles.controlPoint + end = shape.handles.end + + # Calculate distances + line_dist_start_control = vec.dist(start, controlPoint) + line_dist_control_end = vec.dist(controlPoint, end) + + # Early return if both lines are too short + if line_dist_start_control < 2 and line_dist_control_end < 2: + return line_dist_start_control + line_dist_control_end + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + stroke = STROKES[style.color] + + ctx.save() + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) + + if style.dash is DashStyle.DRAW: + freehand_line_shaft(ctx, id, style, start, controlPoint) + freehand_line_shaft(ctx, id, style, controlPoint, end) + + ctx.fill_preserve() + ctx.stroke() + + else: + if style.dash is DashStyle.DOTTED: + ctx.set_dash([0, stroke_width * 4]) + elif style.dash is DashStyle.DASHED: + ctx.set_dash([stroke_width * 4, stroke_width * 4]) + + # Draw first segment: start to control point + ctx.move_to(start.x, start.y) + ctx.line_to(controlPoint.x, controlPoint.y) + ctx.stroke() + + # Draw second segment: control point to end + ctx.move_to(controlPoint.x, controlPoint.y) + ctx.line_to(end.x, end.y) + ctx.stroke() + + ctx.restore() + + return line_dist_start_control + line_dist_control_end + + +def curved_line( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape +) -> float: + style = shape.style + start = shape.handles.start + controlPoint = shape.handles.controlPoint + end = shape.handles.end + + line_dist = vec.dist(start, end) + + if line_dist < 2: + return line_dist + + stroke_width = STROKE_WIDTHS[style.size] + sw = 1 + stroke_width * 1.618 + + # Calculate a path passing through the control point + # t is fixed at 0.5 for the midpoint + t = 0.5 + + # Compute adjusted control points + b_x = (controlPoint.x - (1 - t) ** 3 * start.x - t**3 * end.x) / (3 * (1 - t) * t) + b_y = (controlPoint.y - (1 - t) ** 3 * start.y - t**3 * end.y) / (3 * (1 - t) * t) + c_x = b_x + c_y = b_y + + # Move to the start position + ctx.move_to(start.x, start.y) + + # Draw cubic Bézier curve + ctx.curve_to(b_x, b_y, c_x, c_y, end.x, end.y) + + # Get the length of the curve + length = bezier_length(start, controlPoint, end) + stroke = STROKES[style.color] + + ctx.save() + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + + dash_array, dash_offset = get_perfect_dash_props( + abs(length), sw, style.dash, snap=2, outset=False + ) + + ctx.set_dash(dash_array, dash_offset) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + ctx.stroke() + + ctx.restore() + + return abs(length) + + +def finalize_line( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape +) -> None: + print(f"\tTldraw: Finalizing Line: {id}") + + apply_shape_rotation(ctx, shape) + + ctx.push_group() + + if shape.spline == SplineType.CUBIC: + curved_line(ctx, id, shape) + elif shape.spline == SplineType.LINE: + bent_line(ctx, id, shape) + else: + straight_line(ctx, id, shape) + + line_pattern = ctx.pop_group() + + ctx.set_source(line_pattern) + ctx.paint() diff --git a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py new file mode 100644 index 0000000..e369870 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import TypeVar + +import cairo + +from bbb_presentation_video.events.helpers import Size +from bbb_presentation_video.renderer.tldraw.shape import ( + StickyShape_v2, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_sticky_text_v2 +from bbb_presentation_video.renderer.tldraw.utils import ( + STICKY_FILLS, + ColorStyle, + rounded_rect, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def finalize_sticky_v2( + ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 +) -> None: + style = shape.style + + if style.color is ColorStyle.BLACK: + style.color = ColorStyle.YELLOW + + fill = STICKY_FILLS[style.color] + + # Shadow. Doing blurred shadow is hard, so this is a simple offset drop shadow + border instead + ctx.save() + ctx.translate(-1.0, -1.0) + blur_size = Size(shape.size.width + 3, shape.size.height + 3) + rounded_rect(ctx, blur_size, 5) + ctx.set_source_rgba(0, 0, 0, 0.15) + ctx.fill() + ctx.restore() + + rounded_rect(ctx, shape.size, 3) + ctx.set_source_rgba(0, 0, 0, 0.15) + ctx.set_line_width(2.0) + ctx.stroke_preserve() + + # And fill with sticky note background color + ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) + ctx.fill() + + finalize_sticky_text_v2(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py new file mode 100644 index 0000000..7dea602 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import TypeVar + +import cairo + +from bbb_presentation_video.events.helpers import Size +from bbb_presentation_video.renderer.tldraw.shape import ( + StickyShape_v2, +) +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_sticky_text_v2 +from bbb_presentation_video.renderer.tldraw.utils import ( + STICKY_FILLS, + ColorStyle, + rounded_rect, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def finalize_sticky_v2( + ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 +) -> None: + style = shape.style + + if style.color is ColorStyle.BLACK: + style.color = ColorStyle.YELLOW + + fill = STICKY_FILLS[style.color] + ctx.rotate(shape.rotation) + # Shadow. Doing blurred shadow is hard, so this is a simple offset drop shadow + border instead + ctx.save() + ctx.translate(-1.0, -1.0) + blur_size = Size(shape.size.width + 3, shape.size.height + 3) + rounded_rect(ctx, blur_size, 5) + ctx.set_source_rgba(0, 0, 0, 0.15) + ctx.fill() + ctx.restore() + + rounded_rect(ctx, shape.size, 3) + ctx.set_source_rgba(0, 0, 0, 0.15) + ctx.set_line_width(2.0) + ctx.stroke_preserve() + + # And fill with sticky note background color + ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) + ctx.fill() + + finalize_sticky_text_v2(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/shape/text.py b/bbb_presentation_video/renderer/tldraw/shape/text.py index 7861ea1..ca6e6c9 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text.py @@ -25,6 +25,7 @@ STICKY_TEXT_COLOR, STROKES, AlignStyle, + ColorStyle, Style, ) diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py new file mode 100644 index 0000000..172f707 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py @@ -0,0 +1,175 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import annotations +from typing import Optional, TypeVar + +import cairo +import gi + +from bbb_presentation_video.events.helpers import Position, Size +from bbb_presentation_video.renderer.tldraw.shape import ( + LabelledShapeProto, + StickyShape_v2, + TextShape_v2, +) +from bbb_presentation_video.renderer.tldraw.shape.text import ( + create_pango_layout, + get_layout_size, + show_layout_by_lines, +) +from bbb_presentation_video.renderer.tldraw.utils import ( + FONT_SIZES, + STICKY_FONT_SIZES, + STICKY_PADDING, + STICKY_TEXT_COLOR, + STROKES, + AlignStyle, + ColorStyle, +) + +gi.require_version("Pango", "1.0") +gi.require_version("PangoCairo", "1.0") + +# Set DPI to "72" so we're working directly in Pango point units. +DPI: float = 72.0 + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def finalize_v2_text( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TextShape_v2 +) -> None: + print(f"\tTldraw: Finalizing Text (v2): {id}") + + style = shape.style + ctx.rotate(shape.rotation) + + stroke = STROKES[style.color] + font_size = FONT_SIZES[style.size] + + layout = create_pango_layout(ctx, style, font_size) + layout.set_text(shape.text, -1) + + border_thickness = 2 + border_color = (1, 1, 1, 1) # White + # Draw the border by offsetting the text in several directions + offsets = [ + (-border_thickness, -border_thickness), + (border_thickness, -border_thickness), + (-border_thickness, border_thickness), + (border_thickness, border_thickness), + ] + + for dx, dy in offsets: + ctx.translate(dx, dy) + ctx.set_source_rgba(*border_color) + show_layout_by_lines(ctx, layout, padding=4) + ctx.translate(-dx, -dy) # Reset translation for next iteration + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + show_layout_by_lines(ctx, layout, padding=4) + + +def finalize_v2_label( + ctx: cairo.Context[CairoSomeSurface], + shape: LabelledShapeProto, + *, + offset: Optional[Position] = None, +) -> Size: + if shape.label is None or shape.label == "": + return Size(16, 32) + + print(f"\t\tFinalizing Label (v2)") + + style = shape.style + stroke = STROKES[ColorStyle.BLACK] # v2 labels are always black + border_color = (1, 1, 1, 1) # White + font_size = FONT_SIZES[style.size] + + ctx.save() + + # Create layout aligning the text horizontally within the shape + style.textAlign = shape.align + layout = create_pango_layout( + ctx, style, font_size, width=shape.size.width, padding=4 + ) + layout.set_text(shape.label, -1) + + label_size = get_layout_size(layout, padding=4) + bounds = shape.size + + if offset is None: + offset = shape.label_offset() + + x = offset.x + + # Align text vertically in the shape + if shape.verticalAlign == AlignStyle.START: + y = offset.y + elif shape.verticalAlign == AlignStyle.END: + y = bounds.height - label_size.height + offset.y + else: + y = bounds.height / 2 - label_size.height / 2 + offset.y + + border_thickness = 2 + + # Draw the border by offsetting the text in several directions + offsets = [ + (-border_thickness, -border_thickness), + (border_thickness, -border_thickness), + (-border_thickness, border_thickness), + (border_thickness, border_thickness), + ] + + for dx, dy in offsets: + ctx.translate(x + dx, y + dy) + ctx.set_source_rgba(*border_color) + show_layout_by_lines(ctx, layout, padding=4) + ctx.translate(-x - dx, -y - dy) # Reset translation for next iteration + + # Draw the original text on top + ctx.translate(x, y) + ctx.set_source_rgba( + stroke.r, stroke.g, stroke.b, style.opacity + ) # Set original text color + show_layout_by_lines(ctx, layout, padding=4) + + ctx.restore() + + return label_size + + +def finalize_sticky_text_v2( + ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 +) -> None: + if shape.text is None or shape.text == "": + return + + print(f"\t\tFinalizing Sticky Text (v2)") + + style = shape.style + + # Horizontal alignment + style.textAlign = shape.align + font_size = STICKY_FONT_SIZES[style.size] + + layout = create_pango_layout( + ctx, style, font_size, width=shape.size.width, padding=STICKY_PADDING + ) + layout.set_text(shape.text, -1) + + # Calculate vertical position to center the text + _, text_height = get_layout_size(layout, padding=STICKY_PADDING) + x, y = ctx.get_current_point() + + if shape.verticalAlign is AlignStyle.MIDDLE: + y = (shape.size.height - text_height) / 2 + elif shape.verticalAlign is AlignStyle.END: + y = shape.size.height - text_height + ctx.translate(x, y) + + ctx.set_source_rgba( + STICKY_TEXT_COLOR.r, STICKY_TEXT_COLOR.g, STICKY_TEXT_COLOR.b, style.opacity + ) + show_layout_by_lines(ctx, layout, padding=STICKY_PADDING) diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py new file mode 100644 index 0000000..172f707 --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py @@ -0,0 +1,175 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import annotations +from typing import Optional, TypeVar + +import cairo +import gi + +from bbb_presentation_video.events.helpers import Position, Size +from bbb_presentation_video.renderer.tldraw.shape import ( + LabelledShapeProto, + StickyShape_v2, + TextShape_v2, +) +from bbb_presentation_video.renderer.tldraw.shape.text import ( + create_pango_layout, + get_layout_size, + show_layout_by_lines, +) +from bbb_presentation_video.renderer.tldraw.utils import ( + FONT_SIZES, + STICKY_FONT_SIZES, + STICKY_PADDING, + STICKY_TEXT_COLOR, + STROKES, + AlignStyle, + ColorStyle, +) + +gi.require_version("Pango", "1.0") +gi.require_version("PangoCairo", "1.0") + +# Set DPI to "72" so we're working directly in Pango point units. +DPI: float = 72.0 + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def finalize_v2_text( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TextShape_v2 +) -> None: + print(f"\tTldraw: Finalizing Text (v2): {id}") + + style = shape.style + ctx.rotate(shape.rotation) + + stroke = STROKES[style.color] + font_size = FONT_SIZES[style.size] + + layout = create_pango_layout(ctx, style, font_size) + layout.set_text(shape.text, -1) + + border_thickness = 2 + border_color = (1, 1, 1, 1) # White + # Draw the border by offsetting the text in several directions + offsets = [ + (-border_thickness, -border_thickness), + (border_thickness, -border_thickness), + (-border_thickness, border_thickness), + (border_thickness, border_thickness), + ] + + for dx, dy in offsets: + ctx.translate(dx, dy) + ctx.set_source_rgba(*border_color) + show_layout_by_lines(ctx, layout, padding=4) + ctx.translate(-dx, -dy) # Reset translation for next iteration + + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + show_layout_by_lines(ctx, layout, padding=4) + + +def finalize_v2_label( + ctx: cairo.Context[CairoSomeSurface], + shape: LabelledShapeProto, + *, + offset: Optional[Position] = None, +) -> Size: + if shape.label is None or shape.label == "": + return Size(16, 32) + + print(f"\t\tFinalizing Label (v2)") + + style = shape.style + stroke = STROKES[ColorStyle.BLACK] # v2 labels are always black + border_color = (1, 1, 1, 1) # White + font_size = FONT_SIZES[style.size] + + ctx.save() + + # Create layout aligning the text horizontally within the shape + style.textAlign = shape.align + layout = create_pango_layout( + ctx, style, font_size, width=shape.size.width, padding=4 + ) + layout.set_text(shape.label, -1) + + label_size = get_layout_size(layout, padding=4) + bounds = shape.size + + if offset is None: + offset = shape.label_offset() + + x = offset.x + + # Align text vertically in the shape + if shape.verticalAlign == AlignStyle.START: + y = offset.y + elif shape.verticalAlign == AlignStyle.END: + y = bounds.height - label_size.height + offset.y + else: + y = bounds.height / 2 - label_size.height / 2 + offset.y + + border_thickness = 2 + + # Draw the border by offsetting the text in several directions + offsets = [ + (-border_thickness, -border_thickness), + (border_thickness, -border_thickness), + (-border_thickness, border_thickness), + (border_thickness, border_thickness), + ] + + for dx, dy in offsets: + ctx.translate(x + dx, y + dy) + ctx.set_source_rgba(*border_color) + show_layout_by_lines(ctx, layout, padding=4) + ctx.translate(-x - dx, -y - dy) # Reset translation for next iteration + + # Draw the original text on top + ctx.translate(x, y) + ctx.set_source_rgba( + stroke.r, stroke.g, stroke.b, style.opacity + ) # Set original text color + show_layout_by_lines(ctx, layout, padding=4) + + ctx.restore() + + return label_size + + +def finalize_sticky_text_v2( + ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 +) -> None: + if shape.text is None or shape.text == "": + return + + print(f"\t\tFinalizing Sticky Text (v2)") + + style = shape.style + + # Horizontal alignment + style.textAlign = shape.align + font_size = STICKY_FONT_SIZES[style.size] + + layout = create_pango_layout( + ctx, style, font_size, width=shape.size.width, padding=STICKY_PADDING + ) + layout.set_text(shape.text, -1) + + # Calculate vertical position to center the text + _, text_height = get_layout_size(layout, padding=STICKY_PADDING) + x, y = ctx.get_current_point() + + if shape.verticalAlign is AlignStyle.MIDDLE: + y = (shape.size.height - text_height) / 2 + elif shape.verticalAlign is AlignStyle.END: + y = shape.size.height - text_height + ctx.translate(x, y) + + ctx.set_source_rgba( + STICKY_TEXT_COLOR.r, STICKY_TEXT_COLOR.g, STICKY_TEXT_COLOR.b, style.opacity + ) + show_layout_by_lines(ctx, layout, padding=STICKY_PADDING) diff --git a/bbb_presentation_video/renderer/tldraw/shape/triangle.py b/bbb_presentation_video/renderer/tldraw/shape/triangle.py index db53fd4..d365d23 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/triangle.py +++ b/bbb_presentation_video/renderer/tldraw/shape/triangle.py @@ -58,7 +58,7 @@ def triangle_stroke_points(id: str, shape: TriangleShape) -> List[StrokePoint]: # Which side to start drawing first rm = random.randrange(0, 3) # Number of points per side - # Inset each line by the corner radii and let the freehand algo + # Insert each line by the corner radii and let the freehand algo # interpolate points for the corners. lines = [ vec.points_between(t, br, 32), diff --git a/bbb_presentation_video/renderer/tldraw/utils.py b/bbb_presentation_video/renderer/tldraw/utils.py index 1118fc1..f156b8a 100644 --- a/bbb_presentation_video/renderer/tldraw/utils.py +++ b/bbb_presentation_video/renderer/tldraw/utils.py @@ -6,8 +6,9 @@ import math from enum import Enum -from math import floor, hypot, pi, sqrt, tau +from math import cos, floor, hypot, pi, sin, sqrt, tau from typing import Dict, List, Sequence, Tuple, TypeVar, Union +from random import Random import attr import cairo @@ -19,7 +20,6 @@ DrawPoints = List[Union[Tuple[float, float], Tuple[float, float, float]]] - CANVAS: Color = Color.from_int(0xFAFAFA) STICKY_TEXT_COLOR: Color = Color.from_int(0x0D0D0D) @@ -28,26 +28,42 @@ class SizeStyle(Enum): SMALL: str = "small" + S: str = "s" MEDIUM: str = "medium" + M: str = "m" LARGE: str = "large" + L: str = "l" + XL: str = "xl" STROKE_WIDTHS: Dict[SizeStyle, float] = { SizeStyle.SMALL: 2.0, + SizeStyle.S: 2.0, SizeStyle.MEDIUM: 3.5, + SizeStyle.M: 3.5, SizeStyle.LARGE: 5.0, + SizeStyle.L: 5.0, + SizeStyle.XL: 6.5, } FONT_SIZES: Dict[SizeStyle, float] = { SizeStyle.SMALL: 28, + SizeStyle.S: 26, SizeStyle.MEDIUM: 48, + SizeStyle.M: 36, SizeStyle.LARGE: 96, + SizeStyle.L: 54, + SizeStyle.XL: 64, } STICKY_FONT_SIZES: Dict[SizeStyle, float] = { SizeStyle.SMALL: 24, + SizeStyle.S: 24, SizeStyle.MEDIUM: 36, + SizeStyle.M: 36, SizeStyle.LARGE: 48, + SizeStyle.L: 48, + SizeStyle.XL: 60, } LETTER_SPACING: float = -0.03 # em @@ -57,41 +73,73 @@ class ColorStyle(Enum): WHITE: str = "white" LIGHT_GRAY: str = "lightGray" GRAY: str = "gray" + GREY: str = "grey" BLACK: str = "black" GREEN: str = "green" + LIGHT_GREEN: str = "light-green" CYAN: str = "cyan" BLUE: str = "blue" + LIGHT_BLUE: str = "light-blue" INDIGO: str = "indigo" VIOLET: str = "violet" + LIGHT_VIOLET: str = "light-violet" RED: str = "red" + LIGHT_RED: str = "light-red" ORANGE: str = "orange" YELLOW: str = "yellow" + SEMI: str = "semi" COLORS: Dict[ColorStyle, Color] = { ColorStyle.WHITE: Color.from_int(0x1D1D1D), ColorStyle.LIGHT_GRAY: Color.from_int(0xC6CBD1), ColorStyle.GRAY: Color.from_int(0x788492), + ColorStyle.GREY: Color.from_int(0x9EA6B0), ColorStyle.BLACK: Color.from_int(0x1D1D1D), ColorStyle.GREEN: Color.from_int(0x36B24D), + ColorStyle.LIGHT_GREEN: Color.from_int(0x38B845), ColorStyle.CYAN: Color.from_int(0x0E98AD), ColorStyle.BLUE: Color.from_int(0x1C7ED6), + ColorStyle.LIGHT_BLUE: Color.from_int(0x4099F5), ColorStyle.INDIGO: Color.from_int(0x4263EB), ColorStyle.VIOLET: Color.from_int(0x7746F1), + ColorStyle.LIGHT_VIOLET: Color.from_int(0x9C1FBE), ColorStyle.RED: Color.from_int(0xFF2133), + ColorStyle.LIGHT_RED: Color.from_int(0xFC7075), ColorStyle.ORANGE: Color.from_int(0xFF9433), ColorStyle.YELLOW: Color.from_int(0xFFC936), + ColorStyle.SEMI: Color.from_int(0xF5F9F7), +} + +HIGHLIGHT_COLORS: Dict[ColorStyle, Color] = { + ColorStyle.BLACK: Color.from_int(0xFFF4A1), + ColorStyle.GREY: Color.from_int(0xEDF7FA), + ColorStyle.LIGHT_VIOLET: Color.from_int(0xFFD7FF), + ColorStyle.VIOLET: Color.from_int(0xECD3FF), + ColorStyle.BLUE: Color.from_int(0xB4E2FF), + ColorStyle.LIGHT_BLUE: Color.from_int(0xA2FCFF), + ColorStyle.YELLOW: Color.from_int(0xFFF4A1), + ColorStyle.ORANGE: Color.from_int(0xFFE2B5), + ColorStyle.GREEN: Color.from_int(0xA2FFEC), + ColorStyle.LIGHT_GREEN: Color.from_int(0xCCFCC1), + ColorStyle.LIGHT_RED: Color.from_int(0xFFD3DF), + ColorStyle.RED: Color.from_int(0xFFCACD), } + STICKY_FILLS: Dict[ColorStyle, Color] = dict( [ ( k, - Color.from_int(0xFFFFFF) - if k is ColorStyle.WHITE - else Color.from_int(0x3D3D3D) - if k is ColorStyle.BLACK - else color_blend(v, CANVAS, 0.45), + ( + Color.from_int(0xFFFFFF) + if k is ColorStyle.WHITE + else ( + Color.from_int(0x3D3D3D) + if k is ColorStyle.BLACK + else color_blend(v, CANVAS, 0.45) + ) + ), ) for k, v in COLORS.items() ] @@ -108,9 +156,11 @@ class ColorStyle(Enum): [ ( k, - Color.from_int(0xFEFEFE) - if k is ColorStyle.WHITE - else color_blend(v, CANVAS, 0.82), + ( + Color.from_int(0xFEFEFE) + if k is ColorStyle.WHITE + else color_blend(v, CANVAS, 0.82) + ), ) for k, v in COLORS.items() ] @@ -130,6 +180,7 @@ class FontStyle(Enum): ERIF: str = "erif" # Old tldraw versions had this spelling mistake SERIF: str = "serif" MONO: str = "mono" + DRAW: str = "draw" FONT_FACES: Dict[FontStyle, str] = { @@ -138,6 +189,7 @@ class FontStyle(Enum): FontStyle.ERIF: "Crimson Pro", FontStyle.SERIF: "Crimson Pro", FontStyle.MONO: "Source Code Pro", + FontStyle.DRAW: "Caveat Brush", } @@ -148,15 +200,25 @@ class AlignStyle(Enum): JUSTIFY: str = "justify" +class FillStyle(Enum): + NONE: str = "none" + SEMI: str = "semi" + SOLID: str = "solid" + PATTERN: str = "pattern" + + @attr.s(order=False, slots=True, auto_attribs=True) class Style: color: ColorStyle = ColorStyle.BLACK size: SizeStyle = SizeStyle.SMALL dash: DashStyle = DashStyle.DRAW isFilled: bool = False + isClosed: bool = False scale: float = 1 font: FontStyle = FontStyle.SCRIPT textAlign: AlignStyle = AlignStyle.MIDDLE + opacity: float = 1 + fill: FillStyle = FillStyle.NONE def update_from_data(self, data: StyleData) -> None: if "color" in data: @@ -173,10 +235,53 @@ def update_from_data(self, data: StyleData) -> None: self.font = FontStyle(data["font"]) if "textAlign" in data: self.textAlign = AlignStyle(data["textAlign"]) + if "opacity" in data: + self.opacity = data["opacity"] + + # Tldraw v2 style props not present in v1 + if "isClosed" in data: + self.isClosed = data["isClosed"] + if "fill" in data: + self.fill = FillStyle(data["fill"]) + if self.fill is not FillStyle.NONE: + self.isFilled = True class Decoration(Enum): ARROW: str = "arrow" + BAR: str = "bar" + DIAMOND: str = "diamond" + DOT: str = "dot" + INVERTED: str = "inverted" + NONE: str = "none" + SQUARE: str = "square" + TRIANGLE: str = "triangle" + + +class SplineType(Enum): + CUBIC: str = "cubic" + LINE: str = "line" + NONE: str = "none" + + +class GeoShape(Enum): + ARROW_DOWN: str = "arrow-down" + ARROW_LEFT: str = "arrow-left" + ARROW_RIGHT: str = "arrow-right" + ARROW_UP: str = "arrow-up" + CHECKBOX: str = "check-box" + CLOUD: str = "cloud" + DIAMOND: str = "diamond" + ELLIPSE: str = "ellipse" + HEXAGON: str = "hexagon" + NONE: str = "" + OVAL: str = "oval" + RECTANGLE: str = "rectangle" + RHOMBUS: str = "rhombus" + STAR: str = "star" + TRAPEZOID: str = "trapezoid" + TRIANGLE: str = "triangle" + XBOX: str = "x-box" def perimeter_of_ellipse(rx: float, ry: float) -> float: @@ -289,6 +394,37 @@ def bezier_quad_to_cube( ) +def bezier_length( + start: Position, control: Position, end: Position, num_segments: int = 10 +) -> float: + """Approximate the length of a cubic Bézier curve.""" + length = 0.0 + t_values = [i / num_segments for i in range(num_segments + 1)] + last_point = start + + for t in t_values[1:]: + # Calculate the next point on the curve + x = ( + (1 - t) ** 3 * start.x + + 3 * (1 - t) ** 2 * t * control.x + + 3 * (1 - t) * t**2 * control.x + + t**3 * end.x + ) + y = ( + (1 - t) ** 3 * start.y + + 3 * (1 - t) ** 2 * t * control.y + + 3 * (1 - t) * t**2 * control.y + + t**3 * end.y + ) + next_point = Position(x, y) + + # Add the distance from the last point to the current point + length += vec.dist(last_point, next_point) + last_point = next_point + + return length + + CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) @@ -349,3 +485,178 @@ def draw_smooth_stroke_point_path( ) -> None: outline_points = list(map(lambda p: p["point"], points)) draw_smooth_path(ctx, outline_points, closed) + + +def pattern_fill(fill: Color, opacity: float = 1) -> cairo.SurfacePattern: + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 8, 8) + ctx = cairo.Context(surface) + + # RGB for #fcfffe + ctx.set_source_rgba(252 / 255, 255 / 255, 254 / 255, opacity) + ctx.rectangle(0, 0, 8, 8) + ctx.fill() + + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + ctx.set_source_rgba(fill.r, fill.g, fill.b, opacity) + + lines = [ + (0.66, 2, 2, 0.66), + (3.33, 4.66, 4.66, 3.33), + (6, 7.33, 7.33, 6), + ] + + for x1, y1, x2, y2 in lines: + ctx.move_to(x1, y1) + ctx.line_to(x2, y2) + + ctx.set_line_width(2) + ctx.stroke() + + pattern = cairo.SurfacePattern(surface) + pattern.set_extend(cairo.EXTEND_REPEAT) + + return pattern + + +def get_arc_length(C: Position, r: float, A: Position, B: Position) -> float: + sweep = get_sweep(C, A, B) + return r * tau * (sweep / tau) + + +def apply_geo_fill( + ctx: cairo.Context[CairoSomeSurface], style: Style, preserve_path: bool = False +) -> None: + fill = FILLS[style.color] + + if style.fill is FillStyle.SEMI: + fill = COLORS[ColorStyle.SEMI] + ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) + + elif style.fill is FillStyle.PATTERN: + fill = FILLS[style.color] + pattern = pattern_fill(fill, style.opacity) + ctx.set_source(pattern) + else: + ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) + + if preserve_path: + ctx.fill_preserve() + else: + ctx.fill() + + +def finalize_geo_path( + ctx: cairo.Context[CairoSomeSurface], + points: List[Position], + style: Style, +) -> None: + + if style.isFilled: + ctx.move_to(points[0].x, points[0].y) + + for point in points[1:]: + ctx.line_to(point.x, point.y) + + ctx.close_path() + apply_geo_fill(ctx, style) + + stroke = STROKES[style.color] + stroke_width = STROKE_WIDTHS[style.size] * 1.618 + + sw = 1 + stroke_width + + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + + dist: float = 0 + ctx.move_to(points[0].x, points[0].y) + + for i in range(1, len(points)): + dist += vec.dist(points[i - 1], points[i]) + ctx.line_to(points[i].x, points[i].y) + + dist += vec.dist(points[-1], points[0]) + ctx.close_path() + + dash_array, dash_offset = get_perfect_dash_props(dist, stroke_width, style.dash) + + ctx.set_dash(dash_array, dash_offset) + ctx.stroke() + + +def get_polygon_strokes( + width: float, height: float, sides: int +) -> List[Tuple[Position, Position, float]]: + cx = width / 2 + cy = height / 2 + strokes = [] + + for i in range(sides): + step = tau / sides + t = -(tau / 4) + i * step + x = cx + cx * cos(t) + y = cy + cy * sin(t) + + next_t = -(tau / 4) + ((i + 1) % sides) * step + next_x = cx + cx * cos(next_t) + next_y = cy + cy * sin(next_t) + + pos1 = Position(x, y) + pos2 = Position(next_x, next_y) + distance = ((pos2.x - pos1.x) ** 2 + (pos2.y - pos1.y) ** 2) ** 0.5 + + strokes.append((pos1, pos2, distance)) + + # Adjust positions to ensure the polygon fits within the bounding box starting from (0,0) + min_x = min(stroke[0].x for stroke in strokes) + min_y = min(stroke[0].y for stroke in strokes) + + for i in range(len(strokes)): + stroke = strokes[i] + strokes[i] = ( + Position(stroke[0].x - min_x, stroke[0].y - min_y), + Position(stroke[1].x - min_x, stroke[1].y - min_y), + stroke[2], + ) + + return strokes + + +def get_polygon_draw_vertices( + strokes: List[Tuple[Position, Position, float]], stroke_width: float, id: str +) -> List[Tuple[float, float, float]]: + random = Random(id) + # Generate vertices with added variation + variation = stroke_width * 0.75 + v_points = [ + ( + stroke[0].x + random.uniform(-variation, variation), + stroke[0].y + random.uniform(-variation, variation), + ) + for stroke in strokes + ] + + # Determine the random start index for drawing + rm = random.randrange(0, len(v_points)) + + # Generate lines between points with added variation + lines = [ + vec.points_between(v_points[i], v_points[(i + 1) % len(v_points)], 32) + for i in range(len(v_points)) + ] + + lines = lines[rm:] + lines[:rm] + + # Flatten the list of lines to get a single list of points, ensuring the start point is duplicated at the end for closure + points = [] + for line in lines: + points.extend(line) + + points.extend(lines[0]) # Add start point again at the end + return points + + +def get_point_on_circle(center: Position, radius: float, angle: float) -> Position: + return Position(center[0] + radius * cos(angle), center[1] + radius * sin(angle)) diff --git a/bbb_presentation_video/renderer/tldraw/vec.py b/bbb_presentation_video/renderer/tldraw/vec.py index b825370..4512e42 100644 --- a/bbb_presentation_video/renderer/tldraw/vec.py +++ b/bbb_presentation_video/renderer/tldraw/vec.py @@ -6,6 +6,8 @@ from math import atan2, cos, hypot, sin from typing import List, Sequence, Tuple +from bbb_presentation_video.events.helpers import Position + S = Sequence[float] V = Tuple[float, float] @@ -117,3 +119,16 @@ def points_between(a: S, b: S, steps: int = 6) -> List[Tuple[float, float, float k = min(1, 0.5 + abs(0.5 - t)) points.append((*lrp(a, b, t), k)) return points + + +def to_position(a: S) -> Position: + """Convert a vector to a Position.""" + return Position(a[0], a[1]) + + +def from_angle(r: float, length: float) -> Tuple[float, float]: + return (cos(r) * length, sin(r) * length) + + +def is_left(a: S, b: S, c: S) -> bool: + return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]) > 0 diff --git a/tests/renderer/tldraw/test_shape.py b/tests/renderer/tldraw/test_shape.py index cd397ba..46cb2b6 100644 --- a/tests/renderer/tldraw/test_shape.py +++ b/tests/renderer/tldraw/test_shape.py @@ -1,12 +1,19 @@ from bbb_presentation_video.events.helpers import Position, Size from bbb_presentation_video.events.tldraw import ShapeData -from bbb_presentation_video.renderer.tldraw.shape import ArrowShape, DrawShape +from bbb_presentation_video.renderer.tldraw.shape import ( + ArrowShape, + DrawShape, + HighlighterShape, + LineShape, +) from bbb_presentation_video.renderer.tldraw.utils import ( + HIGHLIGHT_COLORS, ColorStyle, DashStyle, Decoration, SizeStyle, Style, + SplineType, ) @@ -190,3 +197,101 @@ def test_arrow_from_data_no_decorations() -> None: arrow = ArrowShape.from_data(data) assert arrow.decorations.start is None assert arrow.decorations.end is None + + +def test_line_from_data() -> None: + data: ShapeData = { + "x": 1250, + "isLocked": False, + "y": 207, + "rotation": 0, + "typeName": "shape", + "isModerator": True, + "opacity": 1, + "parentId": "page:1", + "index": "a3", + "id": "shape:O2QkpQBjAPe2V8hH6Co4X", + "meta": {"updatedBy": "w_b3rm8exhwsjf"}, + "type": "line", + "props": { + "size": "m", + "handles": { + "start": { + "x": 0, + "canSnap": True, + "y": 0, + "canBind": False, + "id": "start", + "type": "vertex", + "index": "a1", + }, + "end": { + "x": -229, + "canSnap": True, + "y": 377, + "canBind": False, + "id": "end", + "type": "vertex", + "index": "a2", + }, + "handle:a1V": { + "x": -71, + "y": 216, + "canBind": False, + "id": "handle:a1V", + "type": "vertex", + "index": "a1V", + }, + }, + "dash": "draw", + "color": "red", + "spline": "cubic", + }, + } + line = LineShape.from_data(data) + assert line.style == Style( + isFilled=False, + size=SizeStyle.M, + color=ColorStyle.RED, + dash=DashStyle.DRAW, + ) + + assert line.spline == SplineType.CUBIC + assert line.point == Position(1250, 207) + assert line.rotation == 0 + assert line.label is None + assert line.labelPoint == Position(0.5, 0.5) + + assert line.handles.start == Position(0, 0) + assert line.handles.controlPoint == Position(-71, 216) + assert line.handles.end == Position(-229, 377) + + +def test_highlight_from_data() -> None: + data: ShapeData = { + "x": 354, + "isLocked": False, + "y": 140, + "rotation": 0, + "typeName": "shape", + "isModerator": True, + "opacity": 1, + "parentId": "page:1", + "index": "a1", + "id": "shape:S_7PT3QSaUT6dzHcRV8Eb", + "meta": {"createdBy": "w_vxjirycsy2br"}, + "type": "highlight", + "props": { + "size": "xl", + "color": "red", + "isPen": False, + "segments": [{"type": "free", "points": [{"x": 0, "y": 0, "z": 0.5}]}], + "isComplete": False, + }, + } + + highlight = HighlighterShape.from_data(data) + assert highlight.style == Style(size=SizeStyle.XL, color=ColorStyle.RED) + + assert highlight.point == Position(354, 140) + assert highlight.rotation == 0 From d3b25e36268367a8e74e4c1cea1f84a8487c0b73 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 6 Mar 2024 01:57:33 +0100 Subject: [PATCH 02/25] Remove duplicate files from squash --- .../renderer/tldraw/fonts/.uuid | 1 + .../renderer/tldraw/geo/arrow_geo 2.py | 309 ------------------ .../renderer/tldraw/geo/checkbox 2.py | 146 --------- .../renderer/tldraw/geo/diamond 2.py | 146 --------- .../renderer/tldraw/geo/ellipse 2.py | 68 ---- .../renderer/tldraw/geo/rectangle 2.py | 161 --------- .../renderer/tldraw/geo/star 2.py | 136 -------- .../renderer/tldraw/geo/xbox 2.py | 118 ------- .../renderer/tldraw/shape/arrow_v2 2.py | 242 -------------- .../renderer/tldraw/shape/highlighter 2.py | 65 ---- .../renderer/tldraw/shape/line 2.py | 277 ---------------- .../renderer/tldraw/shape/sticky_v2 2.py | 53 --- .../renderer/tldraw/shape/text_v2 2.py | 175 ---------- 13 files changed, 1 insertion(+), 1896 deletions(-) create mode 100644 bbb_presentation_video/renderer/tldraw/fonts/.uuid delete mode 100644 bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/geo/diamond 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/geo/star 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/geo/xbox 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/shape/line 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py delete mode 100644 bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py diff --git a/bbb_presentation_video/renderer/tldraw/fonts/.uuid b/bbb_presentation_video/renderer/tldraw/fonts/.uuid new file mode 100644 index 0000000..152cb3c --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/fonts/.uuid @@ -0,0 +1 @@ +535b749d-af0a-4dbb-b547-0f8a4549b6e0 \ No newline at end of file diff --git a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py deleted file mode 100644 index 18a257d..0000000 --- a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo 2.py +++ /dev/null @@ -1,309 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations - -from math import floor -from random import Random -from typing import List, Tuple, TypeVar - -import cairo -import perfect_freehand -from bbb_presentation_video.events.helpers import Position - -from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ArrowGeo -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - GeoShape, - DashStyle, - apply_geo_fill, - draw_smooth_path, - draw_smooth_stroke_point_path, - finalize_geo_path, -) - - -def arrow_geo_stroke_points( - id: str, shape: ArrowGeo -) -> List[perfect_freehand.types.StrokePoint]: - random = Random(id) - sw = STROKE_WIDTHS[shape.style.size] - - # Dimensions - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - variation = sw * 0.75 - - v = [] - - if shape.geo is GeoShape.ARROW_DOWN: - oy = min(w, h) * 0.38 - ox = w * 0.16 - v = [ - ( - ox + random.uniform(-variation, variation), - random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ( - w + random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ( - w / 2 + random.uniform(-variation, variation), - h + random.uniform(-variation, variation), - ), - ( - random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ( - ox + random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ] - elif shape.geo is GeoShape.ARROW_LEFT: - ox = min(w, h) * 0.38 - oy = h * 0.16 - v = [ - ( - ox + random.uniform(-variation, variation), - random.uniform(-variation, variation), - ), - ( - ox + random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ( - w + random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ( - w + random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ( - ox + random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ( - ox + random.uniform(-variation, variation), - h + random.uniform(-variation, variation), - ), - ( - random.uniform(-variation, variation), - h / 2 + random.uniform(-variation, variation), - ), - ] - elif shape.geo is GeoShape.ARROW_UP: - oy = min(w, h) * 0.38 - ox = w * 0.16 - v = [ - ( - w / 2 + random.uniform(-variation, variation), - random.uniform(-variation, variation), - ), - ( - w + random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - h + random.uniform(-variation, variation), - ), - ( - ox + random.uniform(-variation, variation), - h + random.uniform(-variation, variation), - ), - ( - ox + random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ( - random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ] - else: - ox = min(w, h) * 0.38 - oy = h * 0.16 - v = [ - ( - random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - oy + random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - random.uniform(-variation, variation), - ), - ( - w + random.uniform(-variation, variation), - h / 2 + random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - h + random.uniform(-variation, variation), - ), - ( - w - ox + random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ( - random.uniform(-variation, variation), - h - oy + random.uniform(-variation, variation), - ), - ] - # Which side to start drawing first - rm = random.randrange(0, 4) - - # Number of points per side - p = max(8, floor(w / 16)) - - lines = [vec.points_between(v[i], v[(i + 1) % len(v)], p) for i in range(len(v))] - - lines = lines[rm:] + lines[0:rm] - - points: List[Tuple[float, float, float]] = [ - *lines[0], - *lines[1], - *lines[2], - *lines[3], - *lines[4], - *lines[5], - *lines[6], - *lines[0], - ] - - return perfect_freehand.get_stroke_points( - points[5 : floor(len(lines[0]) / -2) + 3], - size=sw, - streamline=0.3, - last=True, - ) - - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def draw_geo_arrow( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo -) -> None: - style = shape.style - is_filled = style.isFilled - stroke = STROKES[style.color] - stroke_width = STROKE_WIDTHS[style.size] - - stroke_points = arrow_geo_stroke_points(id, shape) - - if is_filled: - draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - apply_geo_fill(ctx, style) - - stroke_outline_points = perfect_freehand.get_stroke_outline_points( - stroke_points, - size=stroke_width, - thinning=0.65, - smoothing=1, - simulate_pressure=False, - last=True, - ) - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.fill_preserve() - ctx.set_line_width(stroke_width) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.stroke() - - -def dash_geo_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowGeo) -> None: - style = shape.style - - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - if shape.geo == GeoShape.ARROW_DOWN or shape.geo == GeoShape.ARROW_UP: - ox = w * 0.16 - oy = min(w, h) * 0.38 - else: - ox = min(w, h) * 0.38 - oy = h * 0.16 - - if shape.geo == GeoShape.ARROW_UP: - points = [ - Position(w / 2, 0), - Position(w, oy), - Position(w - ox, oy), - Position(w - ox, h), - Position(ox, h), - Position(ox, oy), - Position(0, oy), - ] - elif shape.geo == GeoShape.ARROW_DOWN: - points = [ - Position(ox, 0), - Position(w - ox, 0), - Position(w - ox, h - oy), - Position(w, h - oy), - Position(w / 2, h), - Position(0, h - oy), - Position(ox, h - oy), - ] - elif shape.geo == GeoShape.ARROW_LEFT: - points = [ - Position(ox, 0), - Position(ox, oy), - Position(w, oy), - Position(w, h - oy), - Position(ox, h - oy), - Position(ox, h), - Position(0, h / 2), - ] - else: - points = [ - Position(0, oy), - Position(w - ox, oy), - Position(w - ox, 0), - Position(w, h / 2), - Position(w - ox, h), - Position(w - ox, h - oy), - Position(0, h - oy), - ] - - finalize_geo_path(ctx, points, style) - - -def finalize_geo_arrow( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo -) -> None: - print(f"\tTldraw: Finalizing Arrow (geo): {id}") - - ctx.rotate(shape.rotation) - - if shape.style.dash is DashStyle.DRAW: - draw_geo_arrow(ctx, id, shape) - else: - dash_geo_arrow(ctx, shape) - - finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py b/bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py deleted file mode 100644 index 38ff5fe..0000000 --- a/bbb_presentation_video/renderer/tldraw/geo/checkbox 2.py +++ /dev/null @@ -1,146 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations -from typing import List, TypeVar - -import cairo -import perfect_freehand -from bbb_presentation_video.events.helpers import Position - -from bbb_presentation_video.renderer.tldraw.shape import ( - CheckBox, -) -from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( - rectangle_stroke_points, -) -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - DashStyle, - apply_geo_fill, - draw_smooth_path, - draw_smooth_stroke_point_path, - finalize_geo_path, -) - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def get_check_box_lines(w: float, h: float) -> List[List[List[float]]]: - size = min(w, h) * 0.82 - ox = (w - size) / 2 - oy = (h - size) / 2 - - def clamp_x(x: float) -> float: - return max(0, min(w, x)) - - def clamp_y(y: float) -> float: - return max(0, min(h, y)) - - return [ - [ - [clamp_x(ox + size * 0.25), clamp_y(oy + size * 0.52)], - [clamp_x(ox + size * 0.45), clamp_y(oy + size * 0.82)], - ], - [ - [clamp_x(ox + size * 0.45), clamp_y(oy + size * 0.82)], - [clamp_x(ox + size * 0.82), clamp_y(oy + size * 0.22)], - ], - ] - - -def overlay_checkmark(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: - sw = STROKE_WIDTHS[shape.style.size] - - # Calculate dimensions - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - # Get checkmark lines based on the dimensions - lines = get_check_box_lines(w, h) - - stroke = STROKES[shape.style.color] - - sw = 1 + sw - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, shape.style.opacity) - - # Set stroke width and other drawing properties - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - - # Draw each line of the checkmark - for start, end in lines: - for point in [start, end]: - ctx.line_to(*point) - ctx.stroke() - - -def draw_checkbox( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox -) -> None: - style = shape.style - is_filled = style.isFilled - stroke = STROKES[style.color] - stroke_width = STROKE_WIDTHS[style.size] - stroke_points = rectangle_stroke_points(id, shape) - - if is_filled: - draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - apply_geo_fill(ctx, style) - - stroke_outline_points = perfect_freehand.get_stroke_outline_points( - stroke_points, - size=stroke_width, - thinning=0.65, - smoothing=1, - simulate_pressure=False, - last=True, - ) - - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.fill_preserve() - ctx.set_line_width(stroke_width) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.stroke() - - overlay_checkmark(ctx, shape) - - -def dash_checkbox(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: - style = shape.style - - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - points = [ - Position(0, 0), - Position(w, 0), - Position(w, h), - Position(0, h), - ] - - overlay_checkmark(ctx, shape) - finalize_geo_path(ctx, points, style) - - -def finalize_checkmark( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox -) -> None: - print(f"\tTldraw: Finalizing checkmark: {id}") - - ctx.rotate(shape.rotation) - - if shape.style.dash is DashStyle.DRAW: - draw_checkbox(ctx, id, shape) - else: - dash_checkbox(ctx, shape) - - finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/diamond 2.py b/bbb_presentation_video/renderer/tldraw/geo/diamond 2.py deleted file mode 100644 index e85180e..0000000 --- a/bbb_presentation_video/renderer/tldraw/geo/diamond 2.py +++ /dev/null @@ -1,146 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations - -from random import Random -from typing import List, TypeVar - -import cairo -import perfect_freehand -from perfect_freehand.types import StrokePoint - -from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - Diamond, -) -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - DashStyle, - apply_geo_fill, - draw_smooth_path, - draw_smooth_stroke_point_path, - finalize_geo_path, -) - - -def diamond_stroke_points(id: str, shape: Diamond) -> List[StrokePoint]: - random = Random(id) - size = shape.size - - width = size.width - height = size.height - half_width = size.width / 2 - half_height = size.height / 2 - - stroke_width = STROKE_WIDTHS[shape.style.size] - - # Corners with random offsets - variation = stroke_width * 0.75 - - t = ( - half_width + random.uniform(-variation, variation), - random.uniform(-variation, variation), - ) - r = ( - width + random.uniform(-variation, variation), - half_height + random.uniform(-variation, variation), - ) - b = ( - half_width + random.uniform(-variation, variation), - height + random.uniform(-variation, variation), - ) - l = ( - random.uniform(-variation, variation), - half_height + random.uniform(-variation, variation), - ) - - # Which side to start drawing first - rm = random.randrange(0, 3) - - lines = [ - vec.points_between(t, r, 32), - vec.points_between(r, b, 32), - vec.points_between(b, l, 32), - vec.points_between(l, t, 32), - ] - - lines = lines[rm:] + lines[0:rm] - points = [*lines[0], *lines[1], *lines[2], *lines[3], *lines[0]] - - return perfect_freehand.get_stroke_points( - points, size=stroke_width, streamline=0.3, last=True - ) - - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def draw_diamond(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond) -> None: - style = shape.style - - stroke = STROKES[style.color] - stroke_width = STROKE_WIDTHS[style.size] - - stroke_points = diamond_stroke_points(id, shape) - - if style.isFilled: - draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - apply_geo_fill(ctx, style) - - stroke_outline_points = perfect_freehand.get_stroke_outline_points( - stroke_points, - size=stroke_width, - thinning=0.65, - smoothing=1, - simulate_pressure=False, - last=True, - ) - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.fill_preserve() - ctx.set_line_width(stroke_width) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.stroke() - - -def dash_diamond(ctx: cairo.Context[CairoSomeSurface], shape: Diamond) -> None: - style = shape.style - - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - half_width = w / 2 - half_height = h / 2 - - points = [ - Position(half_width, 0), - Position(w, half_height), - Position(half_width, h), - Position(0, half_height), - ] - - finalize_geo_path(ctx, points, style) - - -def finalize_diamond( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond -) -> None: - print(f"\tTldraw: Finalizing Diamond: {id}") - - style = shape.style - - ctx.rotate(shape.rotation) - - if style.dash is DashStyle.DRAW: - draw_diamond(ctx, id, shape) - else: - dash_diamond(ctx, shape) - - finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py b/bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py deleted file mode 100644 index bc8053e..0000000 --- a/bbb_presentation_video/renderer/tldraw/geo/ellipse 2.py +++ /dev/null @@ -1,68 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations - -from typing import TypeVar - -import cairo -from bbb_presentation_video.renderer.tldraw.shape import ( - EllipseGeo, -) -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - apply_geo_fill, - get_perfect_dash_props, - perimeter_of_ellipse, -) -from bbb_presentation_video.renderer.utils import cairo_draw_ellipse - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def dash_ellipse(ctx: cairo.Context[CairoSomeSurface], shape: EllipseGeo) -> None: - radius = (shape.size.width / 2, shape.size.height / 2) - style = shape.style - stroke = STROKES[style.color] - stroke_width = STROKE_WIDTHS[style.size] - - sw = 1 + stroke_width * 1.618 - rx = max(0, radius[0] - sw / 2) - ry = max(0, radius[1] - sw / 2) - perimeter = perimeter_of_ellipse(rx, ry) - dash_array, dash_offset = get_perfect_dash_props( - perimeter * 2 if perimeter < 64 else perimeter, - stroke_width * 1.618, - style.dash, - snap=4, - ) - - if style.isFilled: - cairo_draw_ellipse(ctx, radius[0], radius[1], radius[0], radius[1]) - apply_geo_fill(ctx, style) - - cairo_draw_ellipse(ctx, radius[0], radius[1], radius[0], radius[1]) - - ctx.set_dash(dash_array, dash_offset) - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.stroke() - - -def finalize_geo_ellipse( - ctx: cairo.Context[CairoSomeSurface], - id: str, - shape: EllipseGeo, -) -> None: - print(f"\tTldraw: Finalizing Ellipse (geo): {id}") - - ctx.rotate(shape.rotation) - - dash_ellipse(ctx, shape) - - finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py deleted file mode 100644 index 2b4cdc2..0000000 --- a/bbb_presentation_video/renderer/tldraw/geo/rectangle 2.py +++ /dev/null @@ -1,161 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations - -from math import floor -from random import Random -from typing import List, Tuple, TypeVar, Union - -import cairo -import perfect_freehand -from bbb_presentation_video.events.helpers import Position - -from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - CheckBox, - RectangleGeo, - XBox, -) -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - DashStyle, - apply_geo_fill, - draw_smooth_path, - draw_smooth_stroke_point_path, - finalize_geo_path, -) - - -def rectangle_stroke_points( - id: str, shape: Union[RectangleGeo, XBox, CheckBox] -) -> List[perfect_freehand.types.StrokePoint]: - random = Random(id) - sw = STROKE_WIDTHS[shape.style.size] - - # Dimensions - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - # Corners - variation = sw * 0.75 - tl = ( - sw / 2 + random.uniform(-variation, variation), - sw / 2 + random.uniform(-variation, variation), - ) - tr = ( - w - sw / 2 + random.uniform(-variation, variation), - sw / 2 + random.uniform(-variation, variation), - ) - br = ( - w - sw / 2 + random.uniform(-variation, variation), - h - sw / 2 + random.uniform(-variation, variation), - ) - bl = ( - sw / 2 + random.uniform(-variation, variation), - h - sw / 2 + random.uniform(-variation, variation), - ) - - # Which side to start drawing first - rm = random.randrange(0, 4) - - # Corner radii - rx = min(w / 4, sw * 2) - ry = min(h / 4, sw / 2) - - # Number of points per side - px = max(8, floor(w / 16)) - py = max(8, floor(h / 16)) - - # Insert each line by the corner radii and let the freehand algo - # interpolate points for the corners. - lines = [ - vec.points_between(vec.add(tl, (rx, 0)), vec.sub(tr, (rx, 0)), px), - vec.points_between(vec.add(tr, (0, ry)), vec.sub(br, (0, ry)), py), - vec.points_between(vec.sub(br, (rx, 0)), vec.add(bl, (rx, 0)), px), - vec.points_between(vec.sub(bl, (0, ry)), vec.add(tl, (0, ry)), py), - ] - lines = lines[rm:] + lines[0:rm] - - # For the final points, include the first half of the first line again, - # so that the line wraps around and avoids ending on a sharp corner. - # This has a bit of finesse and magic—if you change the points_between - # function, then you'll likely need to change this one too. - points: List[Tuple[float, float, float]] = [ - *lines[0], - *lines[1], - *lines[2], - *lines[3], - *lines[0], - ] - - return perfect_freehand.get_stroke_points( - points[5 : floor(len(lines[0]) / -2) + 3], - size=sw, - streamline=0.3, - last=True, - ) - - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def draw_rectangle( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo -) -> None: - style = shape.style - is_filled = style.isFilled - stroke = STROKES[style.color] - stroke_width = STROKE_WIDTHS[style.size] - - stroke_points = rectangle_stroke_points(id, shape) - - if is_filled: - draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - apply_geo_fill(ctx, style) - - stroke_outline_points = perfect_freehand.get_stroke_outline_points( - stroke_points, - size=stroke_width, - thinning=0.65, - smoothing=1, - simulate_pressure=False, - last=True, - ) - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.fill_preserve() - ctx.set_line_width(stroke_width) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.stroke() - - -def dash_rectangle(ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeo) -> None: - style = shape.style - - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - points = [Position(0, 0), Position(w, 0), Position(w, h), Position(0, h)] - - finalize_geo_path(ctx, points, style) - - -def finalize_geo_rectangle( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo -) -> None: - print(f"\tTldraw: Finalizing Rectangle (geo): {id}") - - ctx.rotate(shape.rotation) - - if shape.style.dash is DashStyle.DRAW: - draw_rectangle(ctx, id, shape) - else: - dash_rectangle(ctx, shape) - - finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/star 2.py b/bbb_presentation_video/renderer/tldraw/geo/star 2.py deleted file mode 100644 index b8915f2..0000000 --- a/bbb_presentation_video/renderer/tldraw/geo/star 2.py +++ /dev/null @@ -1,136 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations -from math import cos, sin, tau - -from typing import List, TypeVar - -import cairo -import perfect_freehand -from perfect_freehand.types import StrokePoint -from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import ( - Star, -) -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - DashStyle, - apply_geo_fill, - draw_smooth_path, - draw_smooth_stroke_point_path, - finalize_geo_path, - get_polygon_draw_vertices, -) - - -def get_star_points(w: float, h: float, n: int) -> List[Position]: - sides = n - step = tau / sides / 2 - - # Calculate the bounding box adjustments - cx, cy = w / 2, h / 2 - ratio = 1 - ox, oy = (w / 2, h / 2) - ix, iy = (ox * ratio) / 2, (oy * ratio) / 2 - - points = [ - Position( - cx + (ix if i % 2 else ox) * cos(-(tau / 4) + i * step), - cy + (iy if i % 2 else oy) * sin(-(tau / 4) + i * step), - ) - for i in range(sides * 2) - ] - - return points - - -def star_stroke_points(id: str, shape: Star) -> List[StrokePoint]: - size = shape.size - - width = size.width - height = size.height - - stroke_width = STROKE_WIDTHS[shape.style.size] - - width = max(0, shape.size.width) - height = max(0, shape.size.height) - - vertices = 5 - - star_points = get_star_points(width, height, vertices) - strokes = [] - - for i in range(len(star_points)): - pos1 = star_points[i] - pos2 = star_points[(i + 1) % len(star_points)] - distance = ((pos2.x - pos1.x) ** 2 + (pos2.y - pos1.y) ** 2) ** 0.5 - strokes.append((pos1, pos2, distance)) - - points = get_polygon_draw_vertices(strokes, stroke_width, id) - - return perfect_freehand.get_stroke_points( - points, size=stroke_width, streamline=0.3, last=True - ) - - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def draw_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: - style = shape.style - - stroke = STROKES[style.color] - stroke_width = STROKE_WIDTHS[style.size] - - stroke_points = star_stroke_points(id, shape) - - if style.isFilled: - draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - apply_geo_fill(ctx, style) - - stroke_outline_points = perfect_freehand.get_stroke_outline_points( - stroke_points, - size=stroke_width, - thinning=0.65, - smoothing=1, - simulate_pressure=False, - last=True, - ) - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.fill_preserve() - ctx.set_line_width(stroke_width) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.stroke() - - -def dash_star(ctx: cairo.Context[CairoSomeSurface], shape: Star) -> None: - style = shape.style - width = max(0, shape.size.width) - height = max(0, shape.size.height) - - vertices = 5 - points = get_star_points(width, height, vertices) - - finalize_geo_path(ctx, points, style) - - -def finalize_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: - print(f"\tTldraw: Finalizing Star: {id}") - - style = shape.style - - ctx.rotate(shape.rotation) - - if style.dash is DashStyle.DRAW: - draw_star(ctx, id, shape) - else: - dash_star(ctx, shape) - - finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/geo/xbox 2.py b/bbb_presentation_video/renderer/tldraw/geo/xbox 2.py deleted file mode 100644 index 590fe1a..0000000 --- a/bbb_presentation_video/renderer/tldraw/geo/xbox 2.py +++ /dev/null @@ -1,118 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations -from typing import TypeVar - -import cairo -import perfect_freehand -from bbb_presentation_video.events.helpers import Position - -from bbb_presentation_video.renderer.tldraw.shape import ( - XBox, -) -from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( - rectangle_stroke_points, -) -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - DashStyle, - apply_geo_fill, - draw_smooth_path, - draw_smooth_stroke_point_path, - finalize_geo_path, -) - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def overlay_x_cross(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: - sw = STROKE_WIDTHS[shape.style.size] - - # Dimensions - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - # The cross doesn't touch the vertices of the box to - # prevent opacities from adding up - x_offset = 2 * sw - y_offset = 2 * sw - - tl = (x_offset, y_offset) - tr = (w - x_offset, y_offset) - - br = (w - x_offset, h - y_offset) - bl = (x_offset, h - y_offset) - - ctx.move_to(*tl) - ctx.line_to(*br) - ctx.move_to(*tr) - ctx.line_to(*bl) - ctx.set_line_width(2 * sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.stroke() - - -def draw_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: - style = shape.style - is_filled = style.isFilled - stroke = STROKES[style.color] - stroke_width = STROKE_WIDTHS[style.size] - stroke_points = rectangle_stroke_points(id, shape) - - if is_filled: - draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - apply_geo_fill(ctx, style) - - stroke_outline_points = perfect_freehand.get_stroke_outline_points( - stroke_points, - size=stroke_width, - thinning=0.65, - smoothing=1, - simulate_pressure=False, - last=True, - ) - - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.fill_preserve() - ctx.set_line_width(stroke_width) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.stroke() - - overlay_x_cross(ctx, shape) - - -def dash_x_box(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: - style = shape.style - - w = max(0, shape.size.width) - h = max(0, shape.size.height) - - points = [ - Position(0, 0), - Position(w, 0), - Position(w, h), - Position(0, h), - ] - - finalize_geo_path(ctx, points, style) - overlay_x_cross(ctx, shape) - - -def finalize_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: - print(f"\tTldraw: Finalizing x-box: {id}") - - ctx.rotate(shape.rotation) - - if shape.style.dash is DashStyle.DRAW: - draw_x_box(ctx, id, shape) - else: - dash_x_box(ctx, shape) - - finalize_v2_label(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py deleted file mode 100644 index 868aae4..0000000 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2 2.py +++ /dev/null @@ -1,242 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations -from math import tau -from typing import TypeVar - -import cairo - -from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.intersect import ( - intersect_circle_circle, - intersect_circle_line_segment, -) -from bbb_presentation_video.renderer.tldraw.shape import ( - ArrowShape_v2, - apply_shape_rotation, -) -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - Decoration, - circle_from_three_points, - get_perfect_dash_props, - get_arc_length, -) - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def curved_arrow_shaft( - ctx: cairo.Context[CairoSomeSurface], - start: Position, - end: Position, - center: Position, - radius: float, - bend: float, -) -> None: - start_angle = vec.angle(center, start) - end_angle = vec.angle(center, end) - - ctx.new_sub_path() - - if bend > 0: - ctx.arc(center.x, center.y, radius, start_angle, end_angle) - else: - ctx.arc_negative(center.x, center.y, radius, start_angle, end_angle) - - -def straight_arrow_head( - ctx: cairo.Context[CairoSomeSurface], - a: Position, - b: Position, - r: float, -) -> None: - ints = intersect_circle_line_segment(a, r, a, b).points - if len(ints) == 0: - print("\t\tCould not find an intersection for the arrow head.") - left = a - right = a - else: - int = ints[0] - left = Position(vec.rot_with(int, a, tau / 12)) - right = Position(vec.rot_with(int, a, -tau / 12)) - - ctx.move_to(left.x, left.y) - ctx.line_to(a.x, a.y) - ctx.line_to(right.x, right.y) - - -def curved_arrow_head( - ctx: cairo.Context[CairoSomeSurface], - a: Position, - r1: float, - C: Position, - r2: float, - sweep: bool, -) -> None: - ints = intersect_circle_circle(a, r1 * 0.618, C, r2).points - if len(ints) == 0: - print("\t\tCould not find an intersection for the arrow head.") - left = a - right = a - else: - int = ints[0] if sweep else ints[1] - left = Position(vec.nudge(vec.rot_with(int, a, tau / 12), a, r1 * -0.382)) - right = Position(vec.nudge(vec.rot_with(int, a, -tau / 12), a, r1 * -0.382)) - - ctx.move_to(left.x, left.y) - ctx.line_to(a.x, a.y) - ctx.line_to(right.x, right.y) - - -def straight_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowShape_v2) -> float: - style = shape.style - start = shape.handles.start - end = shape.handles.end - deco_start = shape.decorations.start - deco_end = shape.decorations.end - opacity = style.opacity - arrow_dist = vec.dist(start, end) - if arrow_dist < 2: - return arrow_dist - stroke_width = STROKE_WIDTHS[style.size] - sw = 1 + stroke_width * 1.618 - - stroke = STROKES[style.color] - - # Path between start and end points - ctx.save() - - ctx.move_to(start[0], start[1]) - ctx.line_to(end[0], end[1]) - - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - dash_array, dash_offset = get_perfect_dash_props( - arrow_dist, stroke_width * 1.618, style.dash, snap=2, outset=False - ) - ctx.set_dash(dash_array, dash_offset) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) - ctx.stroke() - ctx.restore() - - # Arrowheads - arrow_head_len = min(arrow_dist / 3, stroke_width * 8) - if deco_start is Decoration.ARROW: - straight_arrow_head(ctx, start, end, arrow_head_len) - if deco_end is Decoration.ARROW: - straight_arrow_head(ctx, end, start, arrow_head_len) - - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) - ctx.stroke() - - return arrow_dist - - -def get_midpoint(start: Position, end: Position, bend: float) -> Position: - mid = [(start.x + end.x) / 2, (start.y + end.y) / 2] - - unit_vector = vec.uni([end.x - start.x, end.y - start.y]) - - unit_rotated = [unit_vector[1], -unit_vector[0]] - bend_offset = [unit_rotated[0] * -bend, unit_rotated[1] * -bend] - - middle = Position(mid[0] + bend_offset[0], mid[1] + bend_offset[1]) - - return middle - - -def curved_arrow( - ctx: cairo.Context[CairoSomeSurface], - shape: ArrowShape_v2, -) -> float: - style = shape.style - start = shape.handles.start - bend = shape.handles.bend - end = shape.handles.end - arrow_bend = shape.bend - deco_start = shape.decorations.start - deco_end = shape.decorations.end - - arrow_dist = vec.dist(start, end) - if arrow_dist < 2: - return arrow_dist - - stroke_width = STROKE_WIDTHS[style.size] - sw = 1 + stroke_width * 1.618 - # Calculate a path as a segment of a circle passing through the three points start, bend, and end - center, radius = circle_from_three_points(start, bend, end) - length = get_arc_length(center, radius, start, end) - stroke = STROKES[style.color] - - ctx.save() - - arrow_bend = -arrow_bend - - curved_arrow_shaft(ctx, start, end, center, radius, arrow_bend) - - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - dash_array, dash_offset = get_perfect_dash_props( - abs(length), sw, style.dash, snap=2, outset=False - ) - - ctx.set_dash(dash_array, dash_offset) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.stroke() - ctx.restore() - - # Arrowheads - arrow_head_len = min(arrow_dist / 3, stroke_width * 8) - - sweepFlag = ( - (end.x - start.x) * (bend.y - start.y) - (bend.x - start.x) * (end.y - start.y) - ) < 0 - - if deco_start is not Decoration.NONE: - curved_arrow_head(ctx, start, arrow_head_len, center, radius, sweepFlag) - if deco_end is not Decoration.NONE: - curved_arrow_head(ctx, end, arrow_head_len, center, radius, sweepFlag) - - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.stroke() - - return abs(length) - - -def finalize_arrow_v2( - ctx: cairo.Context[CairoSomeSurface], - id: str, - shape: ArrowShape_v2, -) -> None: - print(f"\tTldraw: Finalizing Arrow (v2): {id}") - - apply_shape_rotation(ctx, shape) - - start = shape.handles.start - end = shape.handles.end - - is_straight_line = shape.bend == 0.0 - shape.handles.bend = get_midpoint(start, end, shape.bend) - - ctx.push_group() - if is_straight_line: - straight_arrow(ctx, shape) - else: - curved_arrow(ctx, shape) - - arrow_pattern = ctx.pop_group() - ctx.set_source(arrow_pattern) - ctx.paint() diff --git a/bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py b/bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py deleted file mode 100644 index 8e670d3..0000000 --- a/bbb_presentation_video/renderer/tldraw/shape/highlighter 2.py +++ /dev/null @@ -1,65 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations - -from math import tau -from typing import TypeVar - -import cairo - -from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - DrawShape, - HighlighterShape, - apply_shape_rotation, -) -from bbb_presentation_video.renderer.tldraw.utils import ( - HIGHLIGHT_COLORS, - STROKE_WIDTHS, - draw_smooth_stroke_point_path, - draw_stroke_points, -) - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def finalize_highlight( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: HighlighterShape -) -> None: - print(f"\tTldraw: Finalizing Highlight: {id}") - - apply_shape_rotation(ctx, shape) - - style = shape.style - is_complete = shape.isComplete - - stroke = HIGHLIGHT_COLORS[style.color] - stroke_width = STROKE_WIDTHS[style.size] * 5 - opacity = 0.7 - - # For very short lines, draw a point instead of a line - size = shape.size - very_small = size.width <= stroke_width / 2 and size.height <= stroke_width < 2 - - if very_small: - sw = 1 + stroke_width - ctx.arc(0, 0, sw, 0, tau) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) - ctx.fill_preserve() - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.set_line_width(1) - ctx.stroke() - return - - stroke_points = draw_stroke_points(shape.points, stroke_width, is_complete) - - draw_smooth_stroke_point_path(ctx, stroke_points, closed=False) - - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.set_line_width(1 + stroke_width * 1.5) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, opacity) - ctx.stroke() diff --git a/bbb_presentation_video/renderer/tldraw/shape/line 2.py b/bbb_presentation_video/renderer/tldraw/shape/line 2.py deleted file mode 100644 index 03be5c5..0000000 --- a/bbb_presentation_video/renderer/tldraw/shape/line 2.py +++ /dev/null @@ -1,277 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations -from enum import Enum - -from math import floor -from random import Random -from typing import Callable, List, Optional, Sequence, TypeVar - -import cairo -from perfect_freehand import get_stroke - -from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.easings import ( - ease_out_quad, -) -from bbb_presentation_video.renderer.tldraw.shape import ( - LineShape, - apply_shape_rotation, -) -from bbb_presentation_video.renderer.tldraw.shape.text import finalize_label -from bbb_presentation_video.renderer.tldraw.utils import ( - STROKE_WIDTHS, - STROKES, - DashStyle, - SplineType, - Style, - bezier_length, - draw_smooth_path, - get_perfect_dash_props, - lerp_angles, - rounded_rect, -) - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def freehand_line_shaft( - ctx: cairo.Context[CairoSomeSurface], - id: str, - style: Style, - start: Position, - end: Position, -) -> None: - random = Random(id) - stroke_width = STROKE_WIDTHS[style.size] - - stroke_outline_points = get_stroke( - [start, end], - size=stroke_width, - thinning=0.618 + random.uniform(-0.2, 0.2), - easing=ease_out_quad, - simulate_pressure=True, - streamline=0, - last=True, - ) - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - -def curved_freehand_line_shaft( - ctx: cairo.Context[CairoSomeSurface], - id: str, - style: Style, - start: Position, - end: Position, - center: Position, - radius: float, - length: float, - easing: Callable[[float], float], -) -> None: - - random = Random(id) - stroke_width = STROKE_WIDTHS[style.size] - start_angle = vec.angle(center, start) - end_angle = vec.angle(center, end) - - points: List[Sequence[float]] = [start] - count = 8 + floor((abs(length) / 20) * 1 + random.uniform(-0.5, 0.5)) - for i in range(count): - t = easing(i / count) - angle = lerp_angles(start_angle, end_angle, t) - points.append(vec.to_fixed(vec.nudge_at_angle(center, angle, radius))) - points.append(end) - - stroke_outline_points = get_stroke( - points, - size=1 + stroke_width, - thinning=0.618 + random.uniform(-0.2, 0.2), - easing=ease_out_quad, - simulate_pressure=False, - streamline=0, - last=True, - ) - draw_smooth_path(ctx, stroke_outline_points, closed=True) - - -def straight_line( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape -) -> float: - style = shape.style - start = shape.handles.start - end = shape.handles.end - - line_dist = vec.dist(start, end) - if line_dist < 2: - return line_dist - - stroke_width = STROKE_WIDTHS[style.size] - sw = 1 + stroke_width * 1.618 - - stroke = STROKES[style.color] - - # Path between start and end points - ctx.save() - if style.dash is DashStyle.DRAW: - freehand_line_shaft(ctx, id, style, start, end) - - ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) - ctx.fill_preserve() - ctx.set_line_width(sw / 2) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.stroke() - else: - ctx.move_to(start[0], start[1]) - ctx.line_to(end[0], end[1]) - - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - dash_array, dash_offset = get_perfect_dash_props( - line_dist, stroke_width * 1.618, style.dash, snap=2, outset=False - ) - ctx.set_dash(dash_array, dash_offset) - ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) - ctx.stroke() - ctx.restore() - - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) - ctx.stroke() - - return line_dist - - -def bent_line(ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape) -> float: - style = shape.style - start = shape.handles.start - controlPoint = shape.handles.controlPoint - end = shape.handles.end - - # Calculate distances - line_dist_start_control = vec.dist(start, controlPoint) - line_dist_control_end = vec.dist(controlPoint, end) - - # Early return if both lines are too short - if line_dist_start_control < 2 and line_dist_control_end < 2: - return line_dist_start_control + line_dist_control_end - - stroke_width = STROKE_WIDTHS[style.size] - sw = 1 + stroke_width * 1.618 - stroke = STROKES[style.color] - - ctx.save() - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - ctx.set_source_rgb(stroke.r, stroke.g, stroke.b) - - if style.dash is DashStyle.DRAW: - freehand_line_shaft(ctx, id, style, start, controlPoint) - freehand_line_shaft(ctx, id, style, controlPoint, end) - - ctx.fill_preserve() - ctx.stroke() - - else: - if style.dash is DashStyle.DOTTED: - ctx.set_dash([0, stroke_width * 4]) - elif style.dash is DashStyle.DASHED: - ctx.set_dash([stroke_width * 4, stroke_width * 4]) - - # Draw first segment: start to control point - ctx.move_to(start.x, start.y) - ctx.line_to(controlPoint.x, controlPoint.y) - ctx.stroke() - - # Draw second segment: control point to end - ctx.move_to(controlPoint.x, controlPoint.y) - ctx.line_to(end.x, end.y) - ctx.stroke() - - ctx.restore() - - return line_dist_start_control + line_dist_control_end - - -def curved_line( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape -) -> float: - style = shape.style - start = shape.handles.start - controlPoint = shape.handles.controlPoint - end = shape.handles.end - - line_dist = vec.dist(start, end) - - if line_dist < 2: - return line_dist - - stroke_width = STROKE_WIDTHS[style.size] - sw = 1 + stroke_width * 1.618 - - # Calculate a path passing through the control point - # t is fixed at 0.5 for the midpoint - t = 0.5 - - # Compute adjusted control points - b_x = (controlPoint.x - (1 - t) ** 3 * start.x - t**3 * end.x) / (3 * (1 - t) * t) - b_y = (controlPoint.y - (1 - t) ** 3 * start.y - t**3 * end.y) / (3 * (1 - t) * t) - c_x = b_x - c_y = b_y - - # Move to the start position - ctx.move_to(start.x, start.y) - - # Draw cubic Bézier curve - ctx.curve_to(b_x, b_y, c_x, c_y, end.x, end.y) - - # Get the length of the curve - length = bezier_length(start, controlPoint, end) - stroke = STROKES[style.color] - - ctx.save() - - ctx.set_line_width(sw) - ctx.set_line_cap(cairo.LineCap.ROUND) - ctx.set_line_join(cairo.LineJoin.ROUND) - - dash_array, dash_offset = get_perfect_dash_props( - abs(length), sw, style.dash, snap=2, outset=False - ) - - ctx.set_dash(dash_array, dash_offset) - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - ctx.stroke() - - ctx.restore() - - return abs(length) - - -def finalize_line( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: LineShape -) -> None: - print(f"\tTldraw: Finalizing Line: {id}") - - apply_shape_rotation(ctx, shape) - - ctx.push_group() - - if shape.spline == SplineType.CUBIC: - curved_line(ctx, id, shape) - elif shape.spline == SplineType.LINE: - bent_line(ctx, id, shape) - else: - straight_line(ctx, id, shape) - - line_pattern = ctx.pop_group() - - ctx.set_source(line_pattern) - ctx.paint() diff --git a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py deleted file mode 100644 index e369870..0000000 --- a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2 2.py +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import annotations - -from typing import TypeVar - -import cairo - -from bbb_presentation_video.events.helpers import Size -from bbb_presentation_video.renderer.tldraw.shape import ( - StickyShape_v2, -) -from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_sticky_text_v2 -from bbb_presentation_video.renderer.tldraw.utils import ( - STICKY_FILLS, - ColorStyle, - rounded_rect, -) - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def finalize_sticky_v2( - ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 -) -> None: - style = shape.style - - if style.color is ColorStyle.BLACK: - style.color = ColorStyle.YELLOW - - fill = STICKY_FILLS[style.color] - - # Shadow. Doing blurred shadow is hard, so this is a simple offset drop shadow + border instead - ctx.save() - ctx.translate(-1.0, -1.0) - blur_size = Size(shape.size.width + 3, shape.size.height + 3) - rounded_rect(ctx, blur_size, 5) - ctx.set_source_rgba(0, 0, 0, 0.15) - ctx.fill() - ctx.restore() - - rounded_rect(ctx, shape.size, 3) - ctx.set_source_rgba(0, 0, 0, 0.15) - ctx.set_line_width(2.0) - ctx.stroke_preserve() - - # And fill with sticky note background color - ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) - ctx.fill() - - finalize_sticky_text_v2(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py deleted file mode 100644 index 172f707..0000000 --- a/bbb_presentation_video/renderer/tldraw/shape/text_v2 2.py +++ /dev/null @@ -1,175 +0,0 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors -# -# SPDX-License-Identifier: GPL-3.0-or-later -from __future__ import annotations -from typing import Optional, TypeVar - -import cairo -import gi - -from bbb_presentation_video.events.helpers import Position, Size -from bbb_presentation_video.renderer.tldraw.shape import ( - LabelledShapeProto, - StickyShape_v2, - TextShape_v2, -) -from bbb_presentation_video.renderer.tldraw.shape.text import ( - create_pango_layout, - get_layout_size, - show_layout_by_lines, -) -from bbb_presentation_video.renderer.tldraw.utils import ( - FONT_SIZES, - STICKY_FONT_SIZES, - STICKY_PADDING, - STICKY_TEXT_COLOR, - STROKES, - AlignStyle, - ColorStyle, -) - -gi.require_version("Pango", "1.0") -gi.require_version("PangoCairo", "1.0") - -# Set DPI to "72" so we're working directly in Pango point units. -DPI: float = 72.0 - -CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - - -def finalize_v2_text( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: TextShape_v2 -) -> None: - print(f"\tTldraw: Finalizing Text (v2): {id}") - - style = shape.style - ctx.rotate(shape.rotation) - - stroke = STROKES[style.color] - font_size = FONT_SIZES[style.size] - - layout = create_pango_layout(ctx, style, font_size) - layout.set_text(shape.text, -1) - - border_thickness = 2 - border_color = (1, 1, 1, 1) # White - # Draw the border by offsetting the text in several directions - offsets = [ - (-border_thickness, -border_thickness), - (border_thickness, -border_thickness), - (-border_thickness, border_thickness), - (border_thickness, border_thickness), - ] - - for dx, dy in offsets: - ctx.translate(dx, dy) - ctx.set_source_rgba(*border_color) - show_layout_by_lines(ctx, layout, padding=4) - ctx.translate(-dx, -dy) # Reset translation for next iteration - - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) - show_layout_by_lines(ctx, layout, padding=4) - - -def finalize_v2_label( - ctx: cairo.Context[CairoSomeSurface], - shape: LabelledShapeProto, - *, - offset: Optional[Position] = None, -) -> Size: - if shape.label is None or shape.label == "": - return Size(16, 32) - - print(f"\t\tFinalizing Label (v2)") - - style = shape.style - stroke = STROKES[ColorStyle.BLACK] # v2 labels are always black - border_color = (1, 1, 1, 1) # White - font_size = FONT_SIZES[style.size] - - ctx.save() - - # Create layout aligning the text horizontally within the shape - style.textAlign = shape.align - layout = create_pango_layout( - ctx, style, font_size, width=shape.size.width, padding=4 - ) - layout.set_text(shape.label, -1) - - label_size = get_layout_size(layout, padding=4) - bounds = shape.size - - if offset is None: - offset = shape.label_offset() - - x = offset.x - - # Align text vertically in the shape - if shape.verticalAlign == AlignStyle.START: - y = offset.y - elif shape.verticalAlign == AlignStyle.END: - y = bounds.height - label_size.height + offset.y - else: - y = bounds.height / 2 - label_size.height / 2 + offset.y - - border_thickness = 2 - - # Draw the border by offsetting the text in several directions - offsets = [ - (-border_thickness, -border_thickness), - (border_thickness, -border_thickness), - (-border_thickness, border_thickness), - (border_thickness, border_thickness), - ] - - for dx, dy in offsets: - ctx.translate(x + dx, y + dy) - ctx.set_source_rgba(*border_color) - show_layout_by_lines(ctx, layout, padding=4) - ctx.translate(-x - dx, -y - dy) # Reset translation for next iteration - - # Draw the original text on top - ctx.translate(x, y) - ctx.set_source_rgba( - stroke.r, stroke.g, stroke.b, style.opacity - ) # Set original text color - show_layout_by_lines(ctx, layout, padding=4) - - ctx.restore() - - return label_size - - -def finalize_sticky_text_v2( - ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 -) -> None: - if shape.text is None or shape.text == "": - return - - print(f"\t\tFinalizing Sticky Text (v2)") - - style = shape.style - - # Horizontal alignment - style.textAlign = shape.align - font_size = STICKY_FONT_SIZES[style.size] - - layout = create_pango_layout( - ctx, style, font_size, width=shape.size.width, padding=STICKY_PADDING - ) - layout.set_text(shape.text, -1) - - # Calculate vertical position to center the text - _, text_height = get_layout_size(layout, padding=STICKY_PADDING) - x, y = ctx.get_current_point() - - if shape.verticalAlign is AlignStyle.MIDDLE: - y = (shape.size.height - text_height) / 2 - elif shape.verticalAlign is AlignStyle.END: - y = shape.size.height - text_height - ctx.translate(x, y) - - ctx.set_source_rgba( - STICKY_TEXT_COLOR.r, STICKY_TEXT_COLOR.g, STICKY_TEXT_COLOR.b, style.opacity - ) - show_layout_by_lines(ctx, layout, padding=STICKY_PADDING) From 3cc81ae47eb22e2b18bd67403db8736b9475f1f7 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 27 Mar 2024 23:06:09 +0100 Subject: [PATCH 03/25] Add frame shape --- bbb_presentation_video/events/tldraw.py | 4 +- .../renderer/tldraw/__init__.py | 168 +++++++++++------- .../renderer/tldraw/shape/__init__.py | 38 +++- .../renderer/tldraw/shape/frame.py | 87 +++++++++ .../renderer/tldraw/shape/text_v2.py | 45 +++++ .../renderer/tldraw/utils.py | 2 + 6 files changed, 272 insertions(+), 72 deletions(-) create mode 100644 bbb_presentation_video/renderer/tldraw/shape/frame.py diff --git a/bbb_presentation_video/events/tldraw.py b/bbb_presentation_video/events/tldraw.py index 4afeb61..db1cbe8 100644 --- a/bbb_presentation_video/events/tldraw.py +++ b/bbb_presentation_video/events/tldraw.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import json -from typing import Collection, Dict, List, Optional, Sequence, TypedDict, Union +from typing import Any, Collection, Dict, List, Optional, Sequence, TypedDict, Union from lxml import etree @@ -53,6 +53,7 @@ class PropsData(StyleData, total=False): w: float h: float growY: float + name: str class ShapeData(TypedDict, total=False): @@ -84,6 +85,7 @@ class ShapeData(TypedDict, total=False): userId: str x: float y: float + children: List[Any] class AddShapeEvent(TypedDict): diff --git a/bbb_presentation_video/renderer/tldraw/__init__.py b/bbb_presentation_video/renderer/tldraw/__init__.py index f42ca0e..fce12e9 100644 --- a/bbb_presentation_video/renderer/tldraw/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Dict, Generic, Optional, TypeVar, cast +from typing import Any, Dict, Generic, List, Optional, TypeVar, cast import cairo from sortedcollections import ValueSortedDict @@ -39,6 +39,7 @@ DrawShape, EllipseShape, EllipseGeo, + FrameShape, GroupShape, Hexagon, HighlighterShape, @@ -64,6 +65,7 @@ from bbb_presentation_video.renderer.tldraw.shape.arrow_v2 import finalize_arrow_v2 from bbb_presentation_video.renderer.tldraw.shape.draw import finalize_draw from bbb_presentation_video.renderer.tldraw.shape.ellipse import finalize_ellipse +from bbb_presentation_video.renderer.tldraw.shape.frame import finalize_frame from bbb_presentation_video.renderer.tldraw.shape.highlighter import finalize_highlight from bbb_presentation_video.renderer.tldraw.shape.line import finalize_line from bbb_presentation_video.renderer.tldraw.shape.rectangle import finalize_rectangle @@ -240,6 +242,75 @@ def update(self, event: Event) -> None: elif event["name"] == "tldraw.delete_shape": self.delete_shape_event(cast(tldraw.DeleteShapeEvent, event)) + def finalize_shapes( + self, ctx: cairo.Context[CairoSomeSurface], id: str, shape: Shape + ) -> None: + ctx.push_group() + ctx.translate(*shape.point) + if isinstance(shape, ArrowShape): + finalize_arrow(ctx, id, shape) + elif isinstance(shape, ArrowShape_v2): + finalize_arrow_v2(ctx, id, shape) + elif isinstance(shape, CheckBox): + finalize_checkmark(ctx, id, shape) + elif isinstance(shape, Cloud): + finalize_cloud(ctx, id, shape) + elif isinstance(shape, Diamond): + finalize_diamond(ctx, id, shape) + elif isinstance(shape, DrawShape): + finalize_draw(ctx, id, shape) + elif isinstance(shape, ArrowGeo): + finalize_geo_arrow(ctx, id, shape) + elif isinstance(shape, EllipseGeo): + finalize_geo_ellipse(ctx, id, shape) + elif isinstance(shape, EllipseShape): + finalize_ellipse(ctx, id, shape) + elif isinstance(shape, FrameShape): + finalize_frame(self, ctx, id, shape) + elif isinstance(shape, Hexagon): + finalize_hexagon(ctx, id, shape) + elif isinstance(shape, HighlighterShape): + finalize_highlight(ctx, id, shape) + elif isinstance(shape, LineShape): + finalize_line(ctx, id, shape) + elif isinstance(shape, Oval): + finalize_oval(ctx, id, shape) + elif isinstance(shape, RectangleGeo): + finalize_geo_rectangle(ctx, id, shape) + elif isinstance(shape, RectangleShape): + finalize_rectangle(ctx, id, shape) + elif isinstance(shape, Rhombus): + finalize_rhombus(ctx, id, shape) + elif isinstance(shape, Star): + finalize_star(ctx, id, shape) + elif isinstance(shape, Trapezoid): + finalize_trapezoid(ctx, id, shape) + elif isinstance(shape, TriangleGeo): + finalize_geo_triangle(ctx, id, shape) + elif isinstance(shape, TriangleShape): + finalize_triangle(ctx, id, shape) + elif isinstance(shape, TextShape): + finalize_text(ctx, id, shape) + elif isinstance(shape, TextShape_v2): + finalize_v2_text(ctx, id, shape) + elif isinstance(shape, StickyShape): + finalize_sticky(ctx, shape) + elif isinstance(shape, StickyShape_v2): + finalize_sticky_v2(ctx, shape) + elif isinstance(shape, XBox): + finalize_x_box(ctx, id, shape) + + elif isinstance(shape, GroupShape): + # Nothing to do? All group-related updates seem to be propagated to the + # individual shapes in the group. + pass + else: + print(f"\tTldraw: Don't know how to render {shape}") + + self.shape_patterns[id] = ctx.pop_group() + ctx.set_source(self.shape_patterns[id]) + ctx.paint() + def finalize_frame(self, transform: Transform) -> bool: transform_changed = self.transform != transform if not self.shapes_changed and not transform_changed: @@ -267,76 +338,37 @@ def finalize_frame(self, transform: Transform) -> bool: apply_shapes_transform(ctx, transform) + # Map to store frame shapes and their children + frame_map: Dict[str, List[Shape]] = {} + + # First pass to identify frames and initialize them in the map for id, s in shapes.items(): shape = cast(Shape, s) - if id in self.shape_patterns: + if isinstance(s, FrameShape): + frame_map[id] = [] + + # Second pass to add children to frames + for id, s in shapes.items(): + parent_id = s.parentId + + # Check if the shape is in a frame + if parent_id in frame_map: + shape.id = id + frame_map[parent_id].append(s) + + for id, s in shapes.items(): + parent_id = s.parentId + shape = cast(Shape, s) + + if id in frame_map: + shape.children = frame_map[id] + + if id in self.shape_patterns and not id in frame_map: print(f"\tTldraw: Cached {shape.__class__.__name__}: {id}") - else: - ctx.push_group() - - ctx.translate(*shape.point) - if isinstance(shape, ArrowShape): - finalize_arrow(ctx, id, shape) - elif isinstance(shape, ArrowShape_v2): - finalize_arrow_v2(ctx, id, shape) - elif isinstance(shape, CheckBox): - finalize_checkmark(ctx, id, shape) - elif isinstance(shape, Cloud): - finalize_cloud(ctx, id, shape) - elif isinstance(shape, Diamond): - finalize_diamond(ctx, id, shape) - elif isinstance(shape, DrawShape): - finalize_draw(ctx, id, shape) - elif isinstance(shape, ArrowGeo): - finalize_geo_arrow(ctx, id, shape) - elif isinstance(shape, EllipseGeo): - finalize_geo_ellipse(ctx, id, shape) - elif isinstance(shape, EllipseShape): - finalize_ellipse(ctx, id, shape) - elif isinstance(shape, Hexagon): - finalize_hexagon(ctx, id, shape) - elif isinstance(shape, HighlighterShape): - finalize_highlight(ctx, id, shape) - elif isinstance(shape, LineShape): - finalize_line(ctx, id, shape) - elif isinstance(shape, Oval): - finalize_oval(ctx, id, shape) - elif isinstance(shape, RectangleGeo): - finalize_geo_rectangle(ctx, id, shape) - elif isinstance(shape, RectangleShape): - finalize_rectangle(ctx, id, shape) - elif isinstance(shape, Rhombus): - finalize_rhombus(ctx, id, shape) - elif isinstance(shape, Star): - finalize_star(ctx, id, shape) - elif isinstance(shape, Trapezoid): - finalize_trapezoid(ctx, id, shape) - elif isinstance(shape, TriangleGeo): - finalize_geo_triangle(ctx, id, shape) - elif isinstance(shape, TriangleShape): - finalize_triangle(ctx, id, shape) - elif isinstance(shape, TextShape): - finalize_text(ctx, id, shape) - elif isinstance(shape, TextShape_v2): - finalize_v2_text(ctx, id, shape) - elif isinstance(shape, StickyShape): - finalize_sticky(ctx, shape) - elif isinstance(shape, StickyShape_v2): - finalize_sticky_v2(ctx, shape) - elif isinstance(shape, XBox): - finalize_x_box(ctx, id, shape) - - elif isinstance(shape, GroupShape): - # Nothing to do? All group-related updates seem to be propagated to the - # individual shapes in the group. - pass - else: - print(f"\tTldraw: Don't know how to render {shape}") - - self.shape_patterns[id] = ctx.pop_group() - - ctx.set_source(self.shape_patterns[id]) - ctx.paint() + + # Don't render the shape if it is inside of a frame shape. + elif not parent_id in frame_map: + self.finalize_shapes(ctx, id, shape) self.pattern = ctx.pop_group() self.shapes_changed = False diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 5a94e51..96a6dd8 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Dict, Optional, Protocol, Tuple, Type, TypeVar, Union +from typing import Dict, List, Optional, Protocol, Tuple, Type, TypeVar, Union import attr import cairo @@ -29,6 +29,8 @@ class BaseShapeProto(Protocol): """The base class for all tldraw shapes.""" + id: str = "" + """ID of the shape.""" style: Style = attr.Factory(Style) """Style related properties, such as color, line size, font.""" childIndex: float = 1 @@ -37,6 +39,10 @@ class BaseShapeProto(Protocol): """Position of the origin of the shape.""" opacity: float = 1.0 """Opacity of the shape.""" + parentId: str = "" + """ID of the parent shape.""" + children: List[Shape] = [] + """List of children shapes.""" @classmethod def from_data(cls: Type[BaseShapeSelf], data: ShapeData) -> BaseShapeSelf: @@ -64,6 +70,9 @@ def update_from_data(self, data: ShapeData) -> None: if "opacity" in data: self.style.opacity = data["opacity"] + if "parentId" in data: + self.parentId = data["parentId"] + @attr.s(order=False, slots=True, auto_attribs=True) class SizedShapeProto(BaseShapeProto, Protocol): @@ -81,8 +90,13 @@ def update_from_data(self, data: ShapeData) -> None: if "props" in data: props = data["props"] - if "w" in props and "h" in props and "growY" in props: - self.size = Size(props["w"], props["h"] + props["growY"]) + if "w" in props and "h" in props: + growY = 0.0 + + if "growY" in props: + growY = props["growY"] + + self.size = Size(props["w"], props["h"] + growY) @attr.s(order=False, slots=True, auto_attribs=True) @@ -115,6 +129,9 @@ class LabelledShapeProto(RotatableShapeProto, Protocol): geo: GeoShape = GeoShape.NONE """Which geo type the shape is, if any.""" + children: List[Shape] = [] + """List of children shapes.""" + def label_offset(self) -> Position: """Calculate the offset needed when drawing the label for most shapes.""" return Position( @@ -129,6 +146,8 @@ def update_from_data(self, data: ShapeData) -> None: self.label = data["label"] if data["label"] != "" else None if "labelPoint" in data: self.labelPoint = Position(data["labelPoint"]) + if "children" in data: + self.children = data["children"] if "props" in data: props = data["props"] @@ -140,6 +159,9 @@ def update_from_data(self, data: ShapeData) -> None: self.verticalAlign = AlignStyle(props["verticalAlign"]) if "geo" in props: self.geo = GeoShape(props["geo"]) + if "w" in props and "h" in props and "name" in props: + if not self.label == "Frame": + self.label = props["name"] def shape_sort_key(shape: BaseShapeProto) -> float: @@ -248,6 +270,13 @@ class EllipseGeo(LabelledShapeProto): size: Size = Size(1.0, 1.0) +@attr.s(order=False, slots=True, auto_attribs=True) +class FrameShape(LabelledShapeProto, SizedShapeProto): + label: str = "Frame" + children: List[Shape] = [] + size: Size = Size(1.0, 1.0) + + @attr.s(order=False, slots=True, auto_attribs=True) class TriangleShape(LabelledShapeProto): # SizedShapeProto @@ -535,6 +564,7 @@ def update_from_data(self, data: ShapeData) -> None: DrawShape, EllipseGeo, EllipseShape, + FrameShape, GroupShape, HighlighterShape, LineShape, @@ -590,6 +620,8 @@ def parse_shape_from_data(data: ShapeData, bbb_version: Version) -> Shape: return LineShape.from_data(data) elif type == "highlight": return HighlighterShape.from_data(data) + elif type == "frame": + return FrameShape.from_data(data) elif type == "geo": if "geo" in data["props"]: geo_type = GeoShape(data["props"]["geo"]) diff --git a/bbb_presentation_video/renderer/tldraw/shape/frame.py b/bbb_presentation_video/renderer/tldraw/shape/frame.py new file mode 100644 index 0000000..6b44f8c --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/shape/frame.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import Any, TypeVar, TYPE_CHECKING + +if TYPE_CHECKING: + from bbb_presentation_video.renderer.tldraw import TldrawRenderer +import cairo +from bbb_presentation_video.events.helpers import Position + +from bbb_presentation_video.renderer.tldraw.shape import FrameShape +from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_frame_name +from bbb_presentation_video.renderer.tldraw.utils import ( + COLORS, + STROKES, + ColorStyle, +) + +CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) + + +def dash_frame( + self: TldrawRenderer[Any], ctx: cairo.Context[CairoSomeSurface], shape: FrameShape +) -> None: + style = shape.style + + w = max(0, shape.size.width) + h = max(0, shape.size.height) + + points = [Position(0, 0), Position(w, 0), Position(w, h), Position(0, h)] + + # Set up fill color + fill = COLORS[ColorStyle.SEMI] + ctx.set_source_rgba(fill.r, fill.g, fill.b, style.opacity) + + # Create path for both fill and stroke + ctx.move_to(points[0].x, points[0].y) + for point in points[1:]: + ctx.line_to(point.x, point.y) + ctx.close_path() + + # Fill the path with the fill color + ctx.fill_preserve() + + # Set up stroke + stroke = STROKES[ColorStyle.BLACK] + sw = 2 + ctx.set_line_width(sw) + ctx.set_line_cap(cairo.LineCap.ROUND) + ctx.set_line_join(cairo.LineJoin.ROUND) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + + # Stroke the path + ctx.stroke() + + # Define the clipping path (same as the frame shape) + ctx.new_path() + ctx.move_to(0, 0) + ctx.line_to(w, 0) + ctx.line_to(w, h) + ctx.line_to(0, h) + ctx.close_path() + ctx.clip() + + children = shape.children + + for child in children: + self.finalize_shapes(ctx, child.id, child) + + ctx.reset_clip() + + +def finalize_frame( + self: TldrawRenderer[Any], + ctx: cairo.Context[CairoSomeSurface], + id: str, + shape: FrameShape, +) -> None: + print(f"\tTldraw: Finalizing frame shape: {id}") + + ctx.rotate(shape.rotation) + dash_frame(self, ctx, shape) + + finalize_frame_name(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py index 172f707..9fd40a9 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py @@ -9,6 +9,7 @@ from bbb_presentation_video.events.helpers import Position, Size from bbb_presentation_video.renderer.tldraw.shape import ( + FrameShape, LabelledShapeProto, StickyShape_v2, TextShape_v2, @@ -26,6 +27,8 @@ STROKES, AlignStyle, ColorStyle, + FontStyle, + SizeStyle, ) gi.require_version("Pango", "1.0") @@ -140,6 +143,48 @@ def finalize_v2_label( return label_size +def finalize_frame_name( + ctx: cairo.Context[CairoSomeSurface], + shape: FrameShape, +) -> Size: + if shape.label is None or shape.label == "": + return Size(0, 0) + + print(f"\t\tFinalizing Frame name") + + style = shape.style + stroke = STROKES[ColorStyle.BLACK] + font_size = 15 + + ctx.save() + + # Create layout aligning the text to the top left + style.textAlign = AlignStyle.START + style.font = FontStyle.ARIAL + layout = create_pango_layout( + ctx, + style, + font_size, + width=shape.size.width, + padding=0, + ) + + layout.set_text(shape.label, -1) + + label_size = get_layout_size(layout, padding=4) + + x = 0 + y = -20 + ctx.translate(x, y) + ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + + show_layout_by_lines(ctx, layout, padding=4) + + ctx.restore() + + return label_size + + def finalize_sticky_text_v2( ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 ) -> None: diff --git a/bbb_presentation_video/renderer/tldraw/utils.py b/bbb_presentation_video/renderer/tldraw/utils.py index f156b8a..8d87b17 100644 --- a/bbb_presentation_video/renderer/tldraw/utils.py +++ b/bbb_presentation_video/renderer/tldraw/utils.py @@ -181,6 +181,7 @@ class FontStyle(Enum): SERIF: str = "serif" MONO: str = "mono" DRAW: str = "draw" + ARIAL: str = "arial" FONT_FACES: Dict[FontStyle, str] = { @@ -190,6 +191,7 @@ class FontStyle(Enum): FontStyle.SERIF: "Crimson Pro", FontStyle.MONO: "Source Code Pro", FontStyle.DRAW: "Caveat Brush", + FontStyle.ARIAL: "Arial 12px", } From 32d3249f58a94a8b53601983d23ea1acf1548512 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Tue, 2 Apr 2024 18:16:01 +0200 Subject: [PATCH 04/25] Run isort --- .../renderer/tldraw/__init__.py | 6 +++--- .../renderer/tldraw/geo/arrow_geo.py | 4 ++-- .../renderer/tldraw/geo/checkbox.py | 11 ++++------- .../renderer/tldraw/geo/cloud.py | 15 +++++++-------- .../renderer/tldraw/geo/diamond.py | 4 +--- .../renderer/tldraw/geo/ellipse.py | 5 ++--- .../renderer/tldraw/geo/hexagon.py | 4 +--- .../renderer/tldraw/geo/oval.py | 6 ++---- .../renderer/tldraw/geo/rectangle.py | 8 ++------ .../renderer/tldraw/geo/rhombus.py | 4 +--- .../renderer/tldraw/geo/star.py | 7 +++---- .../renderer/tldraw/geo/trapezoid.py | 4 +--- .../renderer/tldraw/geo/triangle.py | 4 +--- .../renderer/tldraw/geo/xbox.py | 11 ++++------- .../renderer/tldraw/shape/__init__.py | 5 ++--- .../renderer/tldraw/shape/arrow.py | 2 +- .../renderer/tldraw/shape/arrow_v2.py | 3 ++- .../renderer/tldraw/shape/draw.py | 4 ++-- .../renderer/tldraw/shape/frame.py | 11 ++++------- .../renderer/tldraw/shape/line.py | 11 +++-------- .../renderer/tldraw/shape/sticky_v2.py | 4 +--- .../renderer/tldraw/shape/text_v2.py | 1 + bbb_presentation_video/renderer/tldraw/utils.py | 2 +- tests/renderer/tldraw/test_shape.py | 2 +- 24 files changed, 52 insertions(+), 86 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/__init__.py b/bbb_presentation_video/renderer/tldraw/__init__.py index fce12e9..a5d2fc2 100644 --- a/bbb_presentation_video/renderer/tldraw/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/__init__.py @@ -7,6 +7,7 @@ from typing import Any, Dict, Generic, List, Optional, TypeVar, cast import cairo +from packaging.version import Version from sortedcollections import ValueSortedDict from bbb_presentation_video import events @@ -37,8 +38,8 @@ Cloud, Diamond, DrawShape, - EllipseShape, EllipseGeo, + EllipseShape, FrameShape, GroupShape, Hexagon, @@ -55,8 +56,8 @@ TextShape, TextShape_v2, Trapezoid, - TriangleShape, TriangleGeo, + TriangleShape, XBox, parse_shape_from_data, shape_sort_key, @@ -74,7 +75,6 @@ from bbb_presentation_video.renderer.tldraw.shape.text import finalize_text from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_text from bbb_presentation_video.renderer.tldraw.shape.triangle import finalize_triangle -from packaging.version import Version CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) diff --git a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py index 18a257d..08f820c 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py +++ b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py @@ -10,16 +10,16 @@ import cairo import perfect_freehand -from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec from bbb_presentation_video.renderer.tldraw.shape import ArrowGeo from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, STROKES, - GeoShape, DashStyle, + GeoShape, apply_geo_fill, draw_smooth_path, draw_smooth_stroke_point_path, diff --git a/bbb_presentation_video/renderer/tldraw/geo/checkbox.py b/bbb_presentation_video/renderer/tldraw/geo/checkbox.py index 38ff5fe..82c7018 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/checkbox.py +++ b/bbb_presentation_video/renderer/tldraw/geo/checkbox.py @@ -3,18 +3,15 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations + from typing import List, TypeVar import cairo import perfect_freehand -from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import ( - CheckBox, -) -from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( - rectangle_stroke_points, -) +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw.geo.rectangle import rectangle_stroke_points +from bbb_presentation_video.renderer.tldraw.shape import CheckBox from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/cloud.py b/bbb_presentation_video/renderer/tldraw/geo/cloud.py index d03e50c..7aeb123 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/cloud.py +++ b/bbb_presentation_video/renderer/tldraw/geo/cloud.py @@ -5,18 +5,18 @@ # Adapted from: https://github.com/tldraw/tldraw/blob/main/packages/tldraw/src/lib/shapes/geo/cloudOutline.ts from __future__ import annotations -from math import atan2, tau + import math -import attr -from typing import Any, Callable, List, Optional, Tuple, TypeVar, TypedDict, Union +from math import atan2, tau +from random import Random +from typing import Any, Callable, List, Optional, Tuple, TypedDict, TypeVar, Union +import attr import cairo -from random import Random from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import ( - Cloud, -) +from bbb_presentation_video.renderer.tldraw import vec +from bbb_presentation_video.renderer.tldraw.shape import Cloud from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -28,7 +28,6 @@ get_perfect_dash_props, get_point_on_circle, ) -from bbb_presentation_video.renderer.tldraw import vec CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) diff --git a/bbb_presentation_video/renderer/tldraw/geo/diamond.py b/bbb_presentation_video/renderer/tldraw/geo/diamond.py index e85180e..cd3fb45 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/diamond.py +++ b/bbb_presentation_video/renderer/tldraw/geo/diamond.py @@ -13,9 +13,7 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - Diamond, -) +from bbb_presentation_video.renderer.tldraw.shape import Diamond from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/ellipse.py b/bbb_presentation_video/renderer/tldraw/geo/ellipse.py index bc8053e..95942f9 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/ellipse.py +++ b/bbb_presentation_video/renderer/tldraw/geo/ellipse.py @@ -7,9 +7,8 @@ from typing import TypeVar import cairo -from bbb_presentation_video.renderer.tldraw.shape import ( - EllipseGeo, -) + +from bbb_presentation_video.renderer.tldraw.shape import EllipseGeo from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/hexagon.py b/bbb_presentation_video/renderer/tldraw/geo/hexagon.py index 8c4863b..a5d3b7e 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/hexagon.py +++ b/bbb_presentation_video/renderer/tldraw/geo/hexagon.py @@ -10,9 +10,7 @@ import perfect_freehand from perfect_freehand.types import StrokePoint -from bbb_presentation_video.renderer.tldraw.shape import ( - Hexagon, -) +from bbb_presentation_video.renderer.tldraw.shape import Hexagon from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/oval.py b/bbb_presentation_video/renderer/tldraw/geo/oval.py index 104dee6..e1c06bc 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/oval.py +++ b/bbb_presentation_video/renderer/tldraw/geo/oval.py @@ -8,13 +8,11 @@ from typing import List, TypeVar import cairo -from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw.shape import Oval from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label -from bbb_presentation_video.renderer.tldraw.utils import ( - finalize_geo_path, -) +from bbb_presentation_video.renderer.tldraw.utils import finalize_geo_path CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py index 2b4cdc2..1420d80 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py @@ -10,14 +10,10 @@ import cairo import perfect_freehand -from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - CheckBox, - RectangleGeo, - XBox, -) +from bbb_presentation_video.renderer.tldraw.shape import CheckBox, RectangleGeo, XBox from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/rhombus.py b/bbb_presentation_video/renderer/tldraw/geo/rhombus.py index b1cf2a8..6ed6b9e 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rhombus.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rhombus.py @@ -13,9 +13,7 @@ from bbb_presentation_video.events.helpers import Position, Size from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - Rhombus, -) +from bbb_presentation_video.renderer.tldraw.shape import Rhombus from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/star.py b/bbb_presentation_video/renderer/tldraw/geo/star.py index b8915f2..0001b21 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/star.py +++ b/bbb_presentation_video/renderer/tldraw/geo/star.py @@ -3,17 +3,16 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations -from math import cos, sin, tau +from math import cos, sin, tau from typing import List, TypeVar import cairo import perfect_freehand from perfect_freehand.types import StrokePoint + from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import ( - Star, -) +from bbb_presentation_video.renderer.tldraw.shape import Star from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py index cde094e..9dbe2c3 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py +++ b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py @@ -13,9 +13,7 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - Trapezoid, -) +from bbb_presentation_video.renderer.tldraw.shape import Trapezoid from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/triangle.py b/bbb_presentation_video/renderer/tldraw/geo/triangle.py index f0cc37b..cac93c1 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/triangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/triangle.py @@ -13,9 +13,7 @@ from bbb_presentation_video.events.helpers import Position, Size from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ( - TriangleGeo, -) +from bbb_presentation_video.renderer.tldraw.shape import TriangleGeo from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/geo/xbox.py b/bbb_presentation_video/renderer/tldraw/geo/xbox.py index 590fe1a..bca6c20 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/xbox.py +++ b/bbb_presentation_video/renderer/tldraw/geo/xbox.py @@ -3,18 +3,15 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations + from typing import TypeVar import cairo import perfect_freehand -from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import ( - XBox, -) -from bbb_presentation_video.renderer.tldraw.geo.rectangle import ( - rectangle_stroke_points, -) +from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.renderer.tldraw.geo.rectangle import rectangle_stroke_points +from bbb_presentation_video.renderer.tldraw.shape import XBox from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 96a6dd8..9d5f961 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -8,20 +8,19 @@ import attr import cairo +from packaging.version import Version from bbb_presentation_video.events.helpers import Position, Size from bbb_presentation_video.events.tldraw import HandleData, ShapeData from bbb_presentation_video.renderer.tldraw.utils import ( AlignStyle, - GeoShape, Decoration, DrawPoints, + GeoShape, SplineType, Style, ) -from packaging.version import Version - BaseShapeSelf = TypeVar("BaseShapeSelf", bound="BaseShapeProto") diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow.py b/bbb_presentation_video/renderer/tldraw/shape/arrow.py index 5887940..4dcbc9a 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow.py @@ -35,10 +35,10 @@ Style, circle_from_three_points, draw_smooth_path, + get_arc_length, get_perfect_dash_props, lerp_angles, rounded_rect, - get_arc_length, ) CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py index 868aae4..12dc5c9 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations + from math import tau from typing import TypeVar @@ -23,8 +24,8 @@ STROKES, Decoration, circle_from_three_points, - get_perfect_dash_props, get_arc_length, + get_perfect_dash_props, ) CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) diff --git a/bbb_presentation_video/renderer/tldraw/shape/draw.py b/bbb_presentation_video/renderer/tldraw/shape/draw.py index cb352bc..3eb6db6 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/draw.py +++ b/bbb_presentation_video/renderer/tldraw/shape/draw.py @@ -14,13 +14,13 @@ from bbb_presentation_video.renderer.tldraw.easings import ease_in_quad, ease_out_sine from bbb_presentation_video.renderer.tldraw.shape import DrawShape, apply_shape_rotation from bbb_presentation_video.renderer.tldraw.utils import ( + COLORS, FILLS, STROKE_WIDTHS, STROKES, - COLORS, + ColorStyle, DashStyle, FillStyle, - ColorStyle, draw_smooth_path, draw_smooth_stroke_point_path, draw_stroke_points, diff --git a/bbb_presentation_video/renderer/tldraw/shape/frame.py b/bbb_presentation_video/renderer/tldraw/shape/frame.py index 6b44f8c..cfa36ee 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/frame.py +++ b/bbb_presentation_video/renderer/tldraw/shape/frame.py @@ -4,20 +4,17 @@ from __future__ import annotations -from typing import Any, TypeVar, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar if TYPE_CHECKING: from bbb_presentation_video.renderer.tldraw import TldrawRenderer + import cairo -from bbb_presentation_video.events.helpers import Position +from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw.shape import FrameShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_frame_name -from bbb_presentation_video.renderer.tldraw.utils import ( - COLORS, - STROKES, - ColorStyle, -) +from bbb_presentation_video.renderer.tldraw.utils import COLORS, STROKES, ColorStyle CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) diff --git a/bbb_presentation_video/renderer/tldraw/shape/line.py b/bbb_presentation_video/renderer/tldraw/shape/line.py index 03be5c5..641cbe7 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/line.py +++ b/bbb_presentation_video/renderer/tldraw/shape/line.py @@ -3,8 +3,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations -from enum import Enum +from enum import Enum from math import floor from random import Random from typing import Callable, List, Optional, Sequence, TypeVar @@ -14,13 +14,8 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.easings import ( - ease_out_quad, -) -from bbb_presentation_video.renderer.tldraw.shape import ( - LineShape, - apply_shape_rotation, -) +from bbb_presentation_video.renderer.tldraw.easings import ease_out_quad +from bbb_presentation_video.renderer.tldraw.shape import LineShape, apply_shape_rotation from bbb_presentation_video.renderer.tldraw.shape.text import finalize_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, diff --git a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py index 7dea602..c2ac438 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py @@ -9,9 +9,7 @@ import cairo from bbb_presentation_video.events.helpers import Size -from bbb_presentation_video.renderer.tldraw.shape import ( - StickyShape_v2, -) +from bbb_presentation_video.renderer.tldraw.shape import StickyShape_v2 from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_sticky_text_v2 from bbb_presentation_video.renderer.tldraw.utils import ( STICKY_FILLS, diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py index 9fd40a9..1e2d629 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations + from typing import Optional, TypeVar import cairo diff --git a/bbb_presentation_video/renderer/tldraw/utils.py b/bbb_presentation_video/renderer/tldraw/utils.py index 8d87b17..f739a02 100644 --- a/bbb_presentation_video/renderer/tldraw/utils.py +++ b/bbb_presentation_video/renderer/tldraw/utils.py @@ -7,8 +7,8 @@ import math from enum import Enum from math import cos, floor, hypot, pi, sin, sqrt, tau -from typing import Dict, List, Sequence, Tuple, TypeVar, Union from random import Random +from typing import Dict, List, Sequence, Tuple, TypeVar, Union import attr import cairo diff --git a/tests/renderer/tldraw/test_shape.py b/tests/renderer/tldraw/test_shape.py index 46cb2b6..4323472 100644 --- a/tests/renderer/tldraw/test_shape.py +++ b/tests/renderer/tldraw/test_shape.py @@ -12,8 +12,8 @@ DashStyle, Decoration, SizeStyle, - Style, SplineType, + Style, ) From 7a33b082d1c0f1c5cb2ea426a6fc5734d8e447ad Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 3 Apr 2024 16:06:32 +0200 Subject: [PATCH 05/25] Update copyright/license year to 2024 --- .github/workflows/test.yml | 2 +- .reuse/dep5 | 2 +- bbb_presentation_video/__init__.py | 2 +- bbb_presentation_video/bindings/fontconfig.py | 2 +- bbb_presentation_video/events/__init__.py | 2 +- bbb_presentation_video/events/errors.py | 2 +- bbb_presentation_video/events/helpers.py | 26 ++++++++++++------- bbb_presentation_video/events/tldraw.py | 2 +- bbb_presentation_video/renderer/__init__.py | 2 +- bbb_presentation_video/renderer/cursor.py | 2 +- .../renderer/presentation.py | 2 +- .../renderer/tldraw/__init__.py | 2 +- .../renderer/tldraw/easings.py | 2 +- .../renderer/tldraw/fonts/__init__.py | 2 +- .../renderer/tldraw/geo/arrow_geo.py | 2 +- .../renderer/tldraw/geo/checkbox.py | 2 +- .../renderer/tldraw/geo/cloud.py | 2 +- .../renderer/tldraw/geo/diamond.py | 2 +- .../renderer/tldraw/geo/ellipse.py | 2 +- .../renderer/tldraw/geo/hexagon.py | 2 +- .../renderer/tldraw/geo/oval.py | 2 +- .../renderer/tldraw/geo/rectangle.py | 2 +- .../renderer/tldraw/geo/rhombus.py | 2 +- .../renderer/tldraw/geo/star.py | 2 +- .../renderer/tldraw/geo/trapezoid.py | 2 +- .../renderer/tldraw/geo/triangle.py | 2 +- .../renderer/tldraw/geo/xbox.py | 2 +- .../renderer/tldraw/intersect.py | 2 +- .../renderer/tldraw/shape/__init__.py | 2 +- .../renderer/tldraw/shape/arrow.py | 2 +- .../renderer/tldraw/shape/arrow_v2.py | 2 +- .../renderer/tldraw/shape/draw.py | 2 +- .../renderer/tldraw/shape/ellipse.py | 2 +- .../renderer/tldraw/shape/frame.py | 2 +- .../renderer/tldraw/shape/highlighter.py | 2 +- .../renderer/tldraw/shape/line.py | 2 +- .../renderer/tldraw/shape/rectangle.py | 2 +- .../renderer/tldraw/shape/sticky.py | 2 +- .../renderer/tldraw/shape/sticky_v2.py | 2 +- .../renderer/tldraw/shape/text.py | 2 +- .../renderer/tldraw/shape/text_v2.py | 2 +- .../renderer/tldraw/shape/triangle.py | 2 +- .../renderer/tldraw/utils.py | 2 +- bbb_presentation_video/renderer/tldraw/vec.py | 2 +- bbb_presentation_video/renderer/whiteboard.py | 2 +- debian/copyright | 2 +- pyproject.toml | 2 +- setup.cfg | 2 +- setup.py | 2 +- 49 files changed, 65 insertions(+), 57 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 946f33e..a41ed73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 Calvin Walton +# SPDX-FileCopyrightText: 2024 Calvin Walton # # SPDX-License-Identifier: CC0-1.0 diff --git a/.reuse/dep5 b/.reuse/dep5 index fb4b5e2..3659938 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -4,5 +4,5 @@ Upstream-Contact: Calvin Walton Source: https://github.com/blindsidenetworks/bbb-presentation-video Files: debian/* -Copyright: 2022 Calvin Walton +Copyright: 2024 Calvin Walton License: MIT diff --git a/bbb_presentation_video/__init__.py b/bbb_presentation_video/__init__.py index 7ce48a7..aba5c39 100644 --- a/bbb_presentation_video/__init__.py +++ b/bbb_presentation_video/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/bindings/fontconfig.py b/bbb_presentation_video/bindings/fontconfig.py index bee1e43..225be4f 100644 --- a/bbb_presentation_video/bindings/fontconfig.py +++ b/bbb_presentation_video/bindings/fontconfig.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/events/__init__.py b/bbb_presentation_video/events/__init__.py index 86a2b90..3f740a1 100644 --- a/bbb_presentation_video/events/__init__.py +++ b/bbb_presentation_video/events/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/events/errors.py b/bbb_presentation_video/events/errors.py index 88c8290..2602e82 100644 --- a/bbb_presentation_video/events/errors.py +++ b/bbb_presentation_video/events/errors.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/events/helpers.py b/bbb_presentation_video/events/helpers.py index 11b73e3..5872c7d 100644 --- a/bbb_presentation_video/events/helpers.py +++ b/bbb_presentation_video/events/helpers.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later @@ -65,10 +65,12 @@ class Position(Sequence[float]): y: float @overload - def __init__(self, iterable: Iterable[float], /) -> None: ... + def __init__(self, iterable: Iterable[float], /) -> None: + ... @overload - def __init__(self, x: float, y: float) -> None: ... + def __init__(self, x: float, y: float) -> None: + ... def __init__( self, x: Union[Iterable[float], float], y: Optional[float] = None @@ -98,10 +100,12 @@ def __truediv__(self: PositionSelf, other: float) -> PositionSelf: return self.__class__(self.x / other, self.y / other) @overload - def __getitem__(self, index: int) -> float: ... + def __getitem__(self, index: int) -> float: + ... @overload - def __getitem__(self, index: slice) -> Sequence[float]: ... + def __getitem__(self, index: slice) -> Sequence[float]: + ... def __getitem__(self, index: Union[int, slice]) -> Union[float, Sequence[float]]: return (self.x, self.y)[index] @@ -119,10 +123,12 @@ class Size(Sequence[float]): height: float @overload - def __init__(self, iterable: Iterable[float], /) -> None: ... + def __init__(self, iterable: Iterable[float], /) -> None: + ... @overload - def __init__(self, width: float, height: float) -> None: ... + def __init__(self, width: float, height: float) -> None: + ... def __init__( self, width: Union[Iterable[float], float], height: Optional[float] = None @@ -149,10 +155,12 @@ def __truediv__(self: SizeSelf, other: float) -> SizeSelf: return self.__class__(self.width / other, self.height / other) @overload - def __getitem__(self, index: int) -> float: ... + def __getitem__(self, index: int) -> float: + ... @overload - def __getitem__(self, index: slice) -> Sequence[float]: ... + def __getitem__(self, index: slice) -> Sequence[float]: + ... def __getitem__(self, index: Union[int, slice]) -> Union[float, Sequence[float]]: return (self.width, self.height)[index] diff --git a/bbb_presentation_video/events/tldraw.py b/bbb_presentation_video/events/tldraw.py index db1cbe8..d6b8d6f 100644 --- a/bbb_presentation_video/events/tldraw.py +++ b/bbb_presentation_video/events/tldraw.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/__init__.py b/bbb_presentation_video/renderer/__init__.py index 33715e8..5f5e312 100644 --- a/bbb_presentation_video/renderer/__init__.py +++ b/bbb_presentation_video/renderer/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/cursor.py b/bbb_presentation_video/renderer/cursor.py index 2ce9b08..b06c3ed 100644 --- a/bbb_presentation_video/renderer/cursor.py +++ b/bbb_presentation_video/renderer/cursor.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/presentation.py b/bbb_presentation_video/renderer/presentation.py index a86d462..ae9d225 100644 --- a/bbb_presentation_video/renderer/presentation.py +++ b/bbb_presentation_video/renderer/presentation.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations diff --git a/bbb_presentation_video/renderer/tldraw/__init__.py b/bbb_presentation_video/renderer/tldraw/__init__.py index a5d2fc2..455c1fa 100644 --- a/bbb_presentation_video/renderer/tldraw/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/easings.py b/bbb_presentation_video/renderer/tldraw/easings.py index acab98c..1568e52 100644 --- a/bbb_presentation_video/renderer/tldraw/easings.py +++ b/bbb_presentation_video/renderer/tldraw/easings.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2021 Stephen Ruiz Ltd -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: MIT diff --git a/bbb_presentation_video/renderer/tldraw/fonts/__init__.py b/bbb_presentation_video/renderer/tldraw/fonts/__init__.py index 739b761..b3ccab2 100644 --- a/bbb_presentation_video/renderer/tldraw/fonts/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/fonts/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py index 08f820c..0b27332 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py +++ b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/checkbox.py b/bbb_presentation_video/renderer/tldraw/geo/checkbox.py index 82c7018..4dd0dbe 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/checkbox.py +++ b/bbb_presentation_video/renderer/tldraw/geo/checkbox.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/cloud.py b/bbb_presentation_video/renderer/tldraw/geo/cloud.py index 7aeb123..93e8249 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/cloud.py +++ b/bbb_presentation_video/renderer/tldraw/geo/cloud.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/diamond.py b/bbb_presentation_video/renderer/tldraw/geo/diamond.py index cd3fb45..aa4746f 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/diamond.py +++ b/bbb_presentation_video/renderer/tldraw/geo/diamond.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/ellipse.py b/bbb_presentation_video/renderer/tldraw/geo/ellipse.py index 95942f9..15db177 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/ellipse.py +++ b/bbb_presentation_video/renderer/tldraw/geo/ellipse.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/hexagon.py b/bbb_presentation_video/renderer/tldraw/geo/hexagon.py index a5d3b7e..5c49e51 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/hexagon.py +++ b/bbb_presentation_video/renderer/tldraw/geo/hexagon.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/oval.py b/bbb_presentation_video/renderer/tldraw/geo/oval.py index e1c06bc..3aaa781 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/oval.py +++ b/bbb_presentation_video/renderer/tldraw/geo/oval.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py index 1420d80..495a7ab 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/rhombus.py b/bbb_presentation_video/renderer/tldraw/geo/rhombus.py index 6ed6b9e..9b2894c 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rhombus.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rhombus.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/star.py b/bbb_presentation_video/renderer/tldraw/geo/star.py index 0001b21..3e82ed9 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/star.py +++ b/bbb_presentation_video/renderer/tldraw/geo/star.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py index 9dbe2c3..e16defe 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py +++ b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/triangle.py b/bbb_presentation_video/renderer/tldraw/geo/triangle.py index cac93c1..5941ffb 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/triangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/triangle.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/geo/xbox.py b/bbb_presentation_video/renderer/tldraw/geo/xbox.py index bca6c20..9de879d 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/xbox.py +++ b/bbb_presentation_video/renderer/tldraw/geo/xbox.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/intersect.py b/bbb_presentation_video/renderer/tldraw/intersect.py index 2202366..20cc388 100644 --- a/bbb_presentation_video/renderer/tldraw/intersect.py +++ b/bbb_presentation_video/renderer/tldraw/intersect.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2021 Stephen Ruiz Ltd -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: MIT diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 9d5f961..05ad47a 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow.py b/bbb_presentation_video/renderer/tldraw/shape/arrow.py index 4dcbc9a..7d51c7a 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py index 12dc5c9..3dd4600 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/draw.py b/bbb_presentation_video/renderer/tldraw/shape/draw.py index 3eb6db6..b12cd54 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/draw.py +++ b/bbb_presentation_video/renderer/tldraw/shape/draw.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/ellipse.py b/bbb_presentation_video/renderer/tldraw/shape/ellipse.py index 09db663..e15db06 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/ellipse.py +++ b/bbb_presentation_video/renderer/tldraw/shape/ellipse.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/frame.py b/bbb_presentation_video/renderer/tldraw/shape/frame.py index cfa36ee..a23ed82 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/frame.py +++ b/bbb_presentation_video/renderer/tldraw/shape/frame.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/highlighter.py b/bbb_presentation_video/renderer/tldraw/shape/highlighter.py index 8e670d3..6428b77 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/highlighter.py +++ b/bbb_presentation_video/renderer/tldraw/shape/highlighter.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/line.py b/bbb_presentation_video/renderer/tldraw/shape/line.py index 641cbe7..d55ff77 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/line.py +++ b/bbb_presentation_video/renderer/tldraw/shape/line.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/rectangle.py b/bbb_presentation_video/renderer/tldraw/shape/rectangle.py index 5dd7cad..0e9868f 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/rectangle.py +++ b/bbb_presentation_video/renderer/tldraw/shape/rectangle.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 4 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/sticky.py b/bbb_presentation_video/renderer/tldraw/shape/sticky.py index 297d18a..e07893b 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/sticky.py +++ b/bbb_presentation_video/renderer/tldraw/shape/sticky.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py index c2ac438..697ca2e 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/shape/text.py b/bbb_presentation_video/renderer/tldraw/shape/text.py index ca6e6c9..32237bb 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py index 1e2d629..a56b264 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations diff --git a/bbb_presentation_video/renderer/tldraw/shape/triangle.py b/bbb_presentation_video/renderer/tldraw/shape/triangle.py index d365d23..2d0c2a6 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/triangle.py +++ b/bbb_presentation_video/renderer/tldraw/shape/triangle.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/utils.py b/bbb_presentation_video/renderer/tldraw/utils.py index f739a02..cc67fd1 100644 --- a/bbb_presentation_video/renderer/tldraw/utils.py +++ b/bbb_presentation_video/renderer/tldraw/utils.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/bbb_presentation_video/renderer/tldraw/vec.py b/bbb_presentation_video/renderer/tldraw/vec.py index 4512e42..0781609 100644 --- a/bbb_presentation_video/renderer/tldraw/vec.py +++ b/bbb_presentation_video/renderer/tldraw/vec.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2021 Stephen Ruiz Ltd -# SPDX-FileCopyrightText: 2022 Calvin Walton +# SPDX-FileCopyrightText: 2024 Calvin Walton # # SPDX-License-Identifier: MIT diff --git a/bbb_presentation_video/renderer/whiteboard.py b/bbb_presentation_video/renderer/whiteboard.py index 0cf2af4..f3d0222 100644 --- a/bbb_presentation_video/renderer/whiteboard.py +++ b/bbb_presentation_video/renderer/whiteboard.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations diff --git a/debian/copyright b/debian/copyright index fca202d..538b4ae 100644 --- a/debian/copyright +++ b/debian/copyright @@ -2,7 +2,7 @@ Format: http://dep.debian.net/deps/dep5 Upstream-Name: bbb-presentation-video Files: * -Copyright: 2022 BigBlueButton Inc. and by respective authors +Copyright: 2024 BigBlueButton Inc. and by respective authors License: GPL-3.0+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the diff --git a/pyproject.toml b/pyproject.toml index 6eb7dbb..4c815a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/setup.cfg b/setup.cfg index 7198bef..9ee2835 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/setup.py b/setup.py index b0736e4..548c5d0 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: LGPL-3.0-or-later From c3d451575588162747d5802647b2eed1677533de Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 8 Apr 2024 20:11:10 +0200 Subject: [PATCH 06/25] Sort PropsData and StyleData alphabetically --- bbb_presentation_video/events/tldraw.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bbb_presentation_video/events/tldraw.py b/bbb_presentation_video/events/tldraw.py index d6b8d6f..82c7539 100644 --- a/bbb_presentation_video/events/tldraw.py +++ b/bbb_presentation_video/events/tldraw.py @@ -11,18 +11,18 @@ class StyleData(TypedDict, total=False): + color: str dash: str + fill: str font: str + isClosed: bool + isComplete: bool isFilled: bool - scale: float - textAlign: str opacity: float - isComplete: bool - fill: str - color: str - size: str - isClosed: bool + scale: float segments: List[Dict[str, Sequence[Collection[str]]]] + size: str + textAlign: str class HandleData(TypedDict, total=False): @@ -44,16 +44,16 @@ class PropsData(StyleData, total=False): bend: float end: HandleData geo: str + growY: float + h: float handles: Dict[str, HandleData] isPen: bool + name: str spline: str start: HandleData text: str verticalAlign: str w: float - h: float - growY: float - name: str class ShapeData(TypedDict, total=False): From daa67c58f50678a31355f2dc40cd226dd5681ee8 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 8 Apr 2024 20:15:25 +0200 Subject: [PATCH 07/25] Delete .uuid file --- bbb_presentation_video/renderer/tldraw/fonts/.uuid | 1 - 1 file changed, 1 deletion(-) delete mode 100644 bbb_presentation_video/renderer/tldraw/fonts/.uuid diff --git a/bbb_presentation_video/renderer/tldraw/fonts/.uuid b/bbb_presentation_video/renderer/tldraw/fonts/.uuid deleted file mode 100644 index 152cb3c..0000000 --- a/bbb_presentation_video/renderer/tldraw/fonts/.uuid +++ /dev/null @@ -1 +0,0 @@ -535b749d-af0a-4dbb-b547-0f8a4549b6e0 \ No newline at end of file From a8708c120aeeab10b204341f5a152baa78e2081b Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 8 Apr 2024 20:28:49 +0200 Subject: [PATCH 08/25] Fix typo in the year --- bbb_presentation_video/renderer/tldraw/shape/rectangle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/rectangle.py b/bbb_presentation_video/renderer/tldraw/shape/rectangle.py index 0e9868f..4cc4c24 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/rectangle.py +++ b/bbb_presentation_video/renderer/tldraw/shape/rectangle.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 4 BigBlueButton Inc. and by respective authors +# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors # # SPDX-License-Identifier: GPL-3.0-or-later From d6c5b79ab5e70088158c941c9f361e76204d4dbc Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 8 Apr 2024 20:31:01 +0200 Subject: [PATCH 09/25] Revert insert to inset --- bbb_presentation_video/renderer/tldraw/geo/rectangle.py | 2 +- bbb_presentation_video/renderer/tldraw/geo/trapezoid.py | 2 +- bbb_presentation_video/renderer/tldraw/geo/triangle.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py index 495a7ab..962d6fa 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rectangle.py @@ -66,7 +66,7 @@ def rectangle_stroke_points( px = max(8, floor(w / 16)) py = max(8, floor(h / 16)) - # Insert each line by the corner radii and let the freehand algo + # Inset each line by the corner radii and let the freehand algo # interpolate points for the corners. lines = [ vec.points_between(vec.add(tl, (rx, 0)), vec.sub(tr, (rx, 0)), px), diff --git a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py index e16defe..effa18f 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py +++ b/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py @@ -61,7 +61,7 @@ def trapezoid_stroke_points(id: str, shape: Trapezoid) -> List[StrokePoint]: # Which side to start drawing first rm = random.randrange(0, 3) # Number of points per side - # Insert each line by the corner radii and let the freehand algo + # Inset each line by the corner radii and let the freehand algo # interpolate points for the corners. lines = [ vec.points_between(tl, tr, 32), diff --git a/bbb_presentation_video/renderer/tldraw/geo/triangle.py b/bbb_presentation_video/renderer/tldraw/geo/triangle.py index 5941ffb..0f5fe3e 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/triangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/triangle.py @@ -54,7 +54,7 @@ def triangle_stroke_points(id: str, shape: TriangleGeo) -> List[StrokePoint]: # Which side to start drawing first rm = random.randrange(0, 3) # Number of points per side - # Insert each line by the corner radii and let the freehand algo + # Inset each line by the corner radii and let the freehand algo # interpolate points for the corners. lines = [ vec.points_between(t, br, 32), From 3ea542bfe9b111d004f5f1ee892f5a6210b20690 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 15 Apr 2024 18:29:56 +0200 Subject: [PATCH 10/25] Set index to Union[str, float] --- bbb_presentation_video/events/tldraw.py | 4 ++-- bbb_presentation_video/renderer/tldraw/shape/triangle.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bbb_presentation_video/events/tldraw.py b/bbb_presentation_video/events/tldraw.py index 82c7539..ecf9a32 100644 --- a/bbb_presentation_video/events/tldraw.py +++ b/bbb_presentation_video/events/tldraw.py @@ -30,7 +30,7 @@ class HandleData(TypedDict, total=False): canBind: bool canSnap: bool id: str - index: Union[str, int] + index: Union[float, str] point: List[float] type: str x: float @@ -62,7 +62,7 @@ class ShapeData(TypedDict, total=False): decorations: Dict[str, Optional[str]] handles: Dict[str, HandleData] id: str - index: Union[int, str] + index: Union[float, str] isComplete: bool isLocked: bool isModerator: bool diff --git a/bbb_presentation_video/renderer/tldraw/shape/triangle.py b/bbb_presentation_video/renderer/tldraw/shape/triangle.py index 2d0c2a6..3bd5cf7 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/triangle.py +++ b/bbb_presentation_video/renderer/tldraw/shape/triangle.py @@ -58,7 +58,7 @@ def triangle_stroke_points(id: str, shape: TriangleShape) -> List[StrokePoint]: # Which side to start drawing first rm = random.randrange(0, 3) # Number of points per side - # Insert each line by the corner radii and let the freehand algo + # Inset each line by the corner radii and let the freehand algo # interpolate points for the corners. lines = [ vec.points_between(t, br, 32), From 364dfb3851162b0aab76c9acd55b7d5bfa8abdbc Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 15 Apr 2024 18:37:22 +0200 Subject: [PATCH 11/25] Use if instead of elif --- bbb_presentation_video/renderer/tldraw/shape/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 05ad47a..8426984 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -53,7 +53,7 @@ def update_from_data(self, data: ShapeData) -> None: if "style" in data: self.style.update_from_data(data["style"]) - elif "props" in data: + if "props" in data: self.style.update_from_data(data["props"]) if "childIndex" in data: From fe27a0e5696184cf14f0d9917a2233cd0e899382 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 15 Apr 2024 18:39:03 +0200 Subject: [PATCH 12/25] Remove unecessary children attribute --- bbb_presentation_video/renderer/tldraw/shape/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 8426984..a297fff 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -128,9 +128,6 @@ class LabelledShapeProto(RotatableShapeProto, Protocol): geo: GeoShape = GeoShape.NONE """Which geo type the shape is, if any.""" - children: List[Shape] = [] - """List of children shapes.""" - def label_offset(self) -> Position: """Calculate the offset needed when drawing the label for most shapes.""" return Position( From 5b281b49907b1f3bcce20afe644c8f2fcc082f6a Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Mon, 15 Apr 2024 18:57:16 +0200 Subject: [PATCH 13/25] Frame shape title --- bbb_presentation_video/renderer/tldraw/shape/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index a297fff..5f482d3 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -156,7 +156,7 @@ def update_from_data(self, data: ShapeData) -> None: if "geo" in props: self.geo = GeoShape(props["geo"]) if "w" in props and "h" in props and "name" in props: - if not self.label == "Frame": + if not props["name"] == "": self.label = props["name"] From b046281e1c2187bb358dd44b49a753a4dd00df5c Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Tue, 16 Apr 2024 15:31:48 +0200 Subject: [PATCH 14/25] Don't use multiple inheritance --- bbb_presentation_video/renderer/tldraw/shape/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 5f482d3..98d4dc8 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -267,7 +267,7 @@ class EllipseGeo(LabelledShapeProto): @attr.s(order=False, slots=True, auto_attribs=True) -class FrameShape(LabelledShapeProto, SizedShapeProto): +class FrameShape(LabelledShapeProto): label: str = "Frame" children: List[Shape] = [] size: Size = Size(1.0, 1.0) From 2841de2e2ff8ce1e23d8c1cec8b9c8d5998a268c Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Tue, 16 Apr 2024 15:34:47 +0200 Subject: [PATCH 15/25] Avoid using 'not' in arrow shape parsing logic --- .../renderer/tldraw/shape/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 98d4dc8..eb88d11 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -597,15 +597,15 @@ def parse_shape_from_data(data: ShapeData, bbb_version: Version) -> Shape: elif type == "triangle": return TriangleShape.from_data(data) elif type == "arrow": - if not is_tldraw_v2: - return ArrowShape.from_data(data) - else: + if is_tldraw_v2: return ArrowShape_v2.from_data(data) - elif type == "text": - if not is_tldraw_v2: - return TextShape.from_data(data) else: + return ArrowShape.from_data(data) + elif type == "text": + if is_tldraw_v2: return TextShape_v2.from_data(data) + else: + return TextShape.from_data(data) elif type == "group": return GroupShape.from_data(data) elif type == "sticky": From 6385c204eb25608575da33d949ee3ba52f7ee9f3 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Tue, 16 Apr 2024 18:05:11 +0200 Subject: [PATCH 16/25] Pattern fill background color as a constant --- bbb_presentation_video/renderer/tldraw/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/utils.py b/bbb_presentation_video/renderer/tldraw/utils.py index cc67fd1..03ecf4c 100644 --- a/bbb_presentation_video/renderer/tldraw/utils.py +++ b/bbb_presentation_video/renderer/tldraw/utils.py @@ -22,6 +22,7 @@ CANVAS: Color = Color.from_int(0xFAFAFA) +PATTERN_FILL_BACKGROUND_COLOR: Color = Color.from_int(0xFCFFFE) STICKY_TEXT_COLOR: Color = Color.from_int(0x0D0D0D) STICKY_PADDING: float = 16.0 @@ -493,8 +494,11 @@ def pattern_fill(fill: Color, opacity: float = 1) -> cairo.SurfacePattern: surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 8, 8) ctx = cairo.Context(surface) - # RGB for #fcfffe - ctx.set_source_rgba(252 / 255, 255 / 255, 254 / 255, opacity) + ctx.set_source_rgba( + PATTERN_FILL_BACKGROUND_COLOR.r, + PATTERN_FILL_BACKGROUND_COLOR.g, + PATTERN_FILL_BACKGROUND_COLOR.b, + opacity) ctx.rectangle(0, 0, 8, 8) ctx.fill() From 7a2e05c137aa5a2ab740aac91881feea6a22aa9d Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Tue, 16 Apr 2024 18:11:05 +0200 Subject: [PATCH 17/25] Remove font size from font type --- bbb_presentation_video/renderer/tldraw/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb_presentation_video/renderer/tldraw/utils.py b/bbb_presentation_video/renderer/tldraw/utils.py index 03ecf4c..69a450c 100644 --- a/bbb_presentation_video/renderer/tldraw/utils.py +++ b/bbb_presentation_video/renderer/tldraw/utils.py @@ -192,7 +192,7 @@ class FontStyle(Enum): FontStyle.SERIF: "Crimson Pro", FontStyle.MONO: "Source Code Pro", FontStyle.DRAW: "Caveat Brush", - FontStyle.ARIAL: "Arial 12px", + FontStyle.ARIAL: "Arial", } From 7a557f5213ea932a40b5f9aafb59cf29ca16be0b Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 17 Apr 2024 01:04:35 +0200 Subject: [PATCH 18/25] Fix disappearing shapes --- .../renderer/tldraw/__init__.py | 148 ++++++++++-------- .../renderer/tldraw/shape/frame.py | 15 +- .../renderer/tldraw/utils.py | 3 +- 3 files changed, 92 insertions(+), 74 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/__init__.py b/bbb_presentation_video/renderer/tldraw/__init__.py index 455c1fa..2727b1c 100644 --- a/bbb_presentation_video/renderer/tldraw/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/__init__.py @@ -243,71 +243,86 @@ def update(self, event: Event) -> None: self.delete_shape_event(cast(tldraw.DeleteShapeEvent, event)) def finalize_shapes( - self, ctx: cairo.Context[CairoSomeSurface], id: str, shape: Shape + self, + ctx: cairo.Context[CairoSomeSurface], + id: str, + shape: Shape, + frame_map: Dict[str, List[Shape]], ) -> None: - ctx.push_group() - ctx.translate(*shape.point) - if isinstance(shape, ArrowShape): - finalize_arrow(ctx, id, shape) - elif isinstance(shape, ArrowShape_v2): - finalize_arrow_v2(ctx, id, shape) - elif isinstance(shape, CheckBox): - finalize_checkmark(ctx, id, shape) - elif isinstance(shape, Cloud): - finalize_cloud(ctx, id, shape) - elif isinstance(shape, Diamond): - finalize_diamond(ctx, id, shape) - elif isinstance(shape, DrawShape): - finalize_draw(ctx, id, shape) - elif isinstance(shape, ArrowGeo): - finalize_geo_arrow(ctx, id, shape) - elif isinstance(shape, EllipseGeo): - finalize_geo_ellipse(ctx, id, shape) - elif isinstance(shape, EllipseShape): - finalize_ellipse(ctx, id, shape) - elif isinstance(shape, FrameShape): - finalize_frame(self, ctx, id, shape) - elif isinstance(shape, Hexagon): - finalize_hexagon(ctx, id, shape) - elif isinstance(shape, HighlighterShape): - finalize_highlight(ctx, id, shape) - elif isinstance(shape, LineShape): - finalize_line(ctx, id, shape) - elif isinstance(shape, Oval): - finalize_oval(ctx, id, shape) - elif isinstance(shape, RectangleGeo): - finalize_geo_rectangle(ctx, id, shape) - elif isinstance(shape, RectangleShape): - finalize_rectangle(ctx, id, shape) - elif isinstance(shape, Rhombus): - finalize_rhombus(ctx, id, shape) - elif isinstance(shape, Star): - finalize_star(ctx, id, shape) - elif isinstance(shape, Trapezoid): - finalize_trapezoid(ctx, id, shape) - elif isinstance(shape, TriangleGeo): - finalize_geo_triangle(ctx, id, shape) - elif isinstance(shape, TriangleShape): - finalize_triangle(ctx, id, shape) - elif isinstance(shape, TextShape): - finalize_text(ctx, id, shape) - elif isinstance(shape, TextShape_v2): - finalize_v2_text(ctx, id, shape) - elif isinstance(shape, StickyShape): - finalize_sticky(ctx, shape) - elif isinstance(shape, StickyShape_v2): - finalize_sticky_v2(ctx, shape) - elif isinstance(shape, XBox): - finalize_x_box(ctx, id, shape) - - elif isinstance(shape, GroupShape): - # Nothing to do? All group-related updates seem to be propagated to the - # individual shapes in the group. - pass + if id in self.shape_patterns and not id in frame_map: + print(f"\tTldraw: Cached {shape.__class__.__name__}: {id}") else: - print(f"\tTldraw: Don't know how to render {shape}") + ctx.push_group() + ctx.translate(*shape.point) + if isinstance(shape, ArrowShape): + finalize_arrow(ctx, id, shape) + elif isinstance(shape, ArrowShape_v2): + finalize_arrow_v2(ctx, id, shape) + elif isinstance(shape, CheckBox): + finalize_checkmark(ctx, id, shape) + elif isinstance(shape, Cloud): + finalize_cloud(ctx, id, shape) + elif isinstance(shape, Diamond): + finalize_diamond(ctx, id, shape) + elif isinstance(shape, DrawShape): + finalize_draw(ctx, id, shape) + elif isinstance(shape, ArrowGeo): + finalize_geo_arrow(ctx, id, shape) + elif isinstance(shape, EllipseGeo): + finalize_geo_ellipse(ctx, id, shape) + elif isinstance(shape, EllipseShape): + finalize_ellipse(ctx, id, shape) + elif isinstance(shape, FrameShape): + finalize_frame( + self, + ctx, + id, + shape, + frame_map, + ) + elif isinstance(shape, Hexagon): + finalize_hexagon(ctx, id, shape) + elif isinstance(shape, HighlighterShape): + finalize_highlight(ctx, id, shape) + elif isinstance(shape, LineShape): + finalize_line(ctx, id, shape) + elif isinstance(shape, Oval): + finalize_oval(ctx, id, shape) + elif isinstance(shape, RectangleGeo): + finalize_geo_rectangle(ctx, id, shape) + elif isinstance(shape, RectangleShape): + finalize_rectangle(ctx, id, shape) + elif isinstance(shape, Rhombus): + finalize_rhombus(ctx, id, shape) + elif isinstance(shape, Star): + finalize_star(ctx, id, shape) + elif isinstance(shape, Trapezoid): + finalize_trapezoid(ctx, id, shape) + elif isinstance(shape, TriangleGeo): + finalize_geo_triangle(ctx, id, shape) + elif isinstance(shape, TriangleShape): + finalize_triangle(ctx, id, shape) + elif isinstance(shape, TextShape): + finalize_text(ctx, id, shape) + elif isinstance(shape, TextShape_v2): + finalize_v2_text(ctx, id, shape) + elif isinstance(shape, StickyShape): + finalize_sticky(ctx, shape) + elif isinstance(shape, StickyShape_v2): + finalize_sticky_v2(ctx, shape) + elif isinstance(shape, XBox): + finalize_x_box(ctx, id, shape) + + elif isinstance(shape, GroupShape): + # Nothing to do? All group-related updates seem to be propagated to the + # individual shapes in the group. + pass + else: + print(f"\tTldraw: Don't know how to render {shape}") + + # Mark the finalized shape as cached + self.shape_patterns[id] = ctx.pop_group() - self.shape_patterns[id] = ctx.pop_group() ctx.set_source(self.shape_patterns[id]) ctx.paint() @@ -363,12 +378,9 @@ def finalize_frame(self, transform: Transform) -> bool: if id in frame_map: shape.children = frame_map[id] - if id in self.shape_patterns and not id in frame_map: - print(f"\tTldraw: Cached {shape.__class__.__name__}: {id}") - - # Don't render the shape if it is inside of a frame shape. - elif not parent_id in frame_map: - self.finalize_shapes(ctx, id, shape) + # Frames are responsible for finalizing their children. + if not parent_id in frame_map: + self.finalize_shapes(ctx, id, shape, frame_map) self.pattern = ctx.pop_group() self.shapes_changed = False diff --git a/bbb_presentation_video/renderer/tldraw/shape/frame.py b/bbb_presentation_video/renderer/tldraw/shape/frame.py index a23ed82..8559734 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/frame.py +++ b/bbb_presentation_video/renderer/tldraw/shape/frame.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Dict, List, TypeVar if TYPE_CHECKING: from bbb_presentation_video.renderer.tldraw import TldrawRenderer @@ -12,7 +12,7 @@ import cairo from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import FrameShape +from bbb_presentation_video.renderer.tldraw.shape import FrameShape, Shape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_frame_name from bbb_presentation_video.renderer.tldraw.utils import COLORS, STROKES, ColorStyle @@ -20,7 +20,10 @@ def dash_frame( - self: TldrawRenderer[Any], ctx: cairo.Context[CairoSomeSurface], shape: FrameShape + self: TldrawRenderer[Any], + ctx: cairo.Context[CairoSomeSurface], + shape: FrameShape, + frame_map: Dict[str, List[Shape]], ) -> None: style = shape.style @@ -64,8 +67,9 @@ def dash_frame( children = shape.children + # Recursively finalize the children. for child in children: - self.finalize_shapes(ctx, child.id, child) + self.finalize_shapes(ctx, child.id, child, frame_map) ctx.reset_clip() @@ -75,10 +79,11 @@ def finalize_frame( ctx: cairo.Context[CairoSomeSurface], id: str, shape: FrameShape, + frame_map: Dict[str, List[Shape]], ) -> None: print(f"\tTldraw: Finalizing frame shape: {id}") ctx.rotate(shape.rotation) - dash_frame(self, ctx, shape) + dash_frame(self, ctx, shape, frame_map) finalize_frame_name(ctx, shape) diff --git a/bbb_presentation_video/renderer/tldraw/utils.py b/bbb_presentation_video/renderer/tldraw/utils.py index 69a450c..47b614b 100644 --- a/bbb_presentation_video/renderer/tldraw/utils.py +++ b/bbb_presentation_video/renderer/tldraw/utils.py @@ -498,7 +498,8 @@ def pattern_fill(fill: Color, opacity: float = 1) -> cairo.SurfacePattern: PATTERN_FILL_BACKGROUND_COLOR.r, PATTERN_FILL_BACKGROUND_COLOR.g, PATTERN_FILL_BACKGROUND_COLOR.b, - opacity) + opacity, + ) ctx.rectangle(0, 0, 8, 8) ctx.fill() From d2f9163f62ccacf982355f7fe92d3c3945f3fb7c Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 17 Apr 2024 15:02:43 +0200 Subject: [PATCH 19/25] More consistent naming --- .../renderer/tldraw/__init__.py | 128 ++++++++++-------- .../geo/{arrow_geo.py => arrow_geo_shape.py} | 10 +- .../{checkbox.py => checkbox_geo_shape.py} | 18 ++- .../geo/{cloud.py => cloud_geo_shape.py} | 16 ++- .../geo/{diamond.py => diamond_geo_shape.py} | 12 +- .../geo/{ellipse.py => ellipse_geo_shape.py} | 6 +- .../geo/{hexagon.py => hexagon_geo_shape.py} | 12 +- .../tldraw/geo/{oval.py => oval_geo_shape.py} | 8 +- .../{rectangle.py => rectangle_geo_shape.py} | 14 +- .../geo/{rhombus.py => rhombus_geo_shape.py} | 14 +- .../tldraw/geo/{star.py => star_geo_shape.py} | 14 +- .../{trapezoid.py => trapezoid_geo_shape.py} | 12 +- .../{triangle.py => triangle_geo_shape.py} | 12 +- .../tldraw/geo/{xbox.py => xbox_geo_shape.py} | 18 ++- .../renderer/tldraw/shape/__init__.py | 96 ++++++------- .../renderer/tldraw/shape/arrow_v2.py | 8 +- .../renderer/tldraw/shape/sticky_v2.py | 4 +- .../renderer/tldraw/shape/text_v2.py | 8 +- 18 files changed, 232 insertions(+), 178 deletions(-) rename bbb_presentation_video/renderer/tldraw/geo/{arrow_geo.py => arrow_geo_shape.py} (99%) rename bbb_presentation_video/renderer/tldraw/geo/{checkbox.py => checkbox_geo_shape.py} (91%) rename bbb_presentation_video/renderer/tldraw/geo/{cloud.py => cloud_geo_shape.py} (96%) rename bbb_presentation_video/renderer/tldraw/geo/{diamond.py => diamond_geo_shape.py} (94%) rename bbb_presentation_video/renderer/tldraw/geo/{ellipse.py => ellipse_geo_shape.py} (96%) rename bbb_presentation_video/renderer/tldraw/geo/{hexagon.py => hexagon_geo_shape.py} (92%) rename bbb_presentation_video/renderer/tldraw/geo/{oval.py => oval_geo_shape.py} (87%) rename bbb_presentation_video/renderer/tldraw/geo/{rectangle.py => rectangle_geo_shape.py} (94%) rename bbb_presentation_video/renderer/tldraw/geo/{rhombus.py => rhombus_geo_shape.py} (92%) rename bbb_presentation_video/renderer/tldraw/geo/{star.py => star_geo_shape.py} (88%) rename bbb_presentation_video/renderer/tldraw/geo/{trapezoid.py => trapezoid_geo_shape.py} (95%) rename bbb_presentation_video/renderer/tldraw/geo/{triangle.py => triangle_geo_shape.py} (95%) rename bbb_presentation_video/renderer/tldraw/geo/{xbox.py => xbox_geo_shape.py} (87%) diff --git a/bbb_presentation_video/renderer/tldraw/__init__.py b/bbb_presentation_video/renderer/tldraw/__init__.py index 2727b1c..3227599 100644 --- a/bbb_presentation_video/renderer/tldraw/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/__init__.py @@ -17,48 +17,66 @@ apply_shapes_transform, ) from bbb_presentation_video.renderer.tldraw.fonts import add_fontconfig_app_font_dir -from bbb_presentation_video.renderer.tldraw.geo.arrow_geo import finalize_geo_arrow -from bbb_presentation_video.renderer.tldraw.geo.checkbox import finalize_checkmark -from bbb_presentation_video.renderer.tldraw.geo.cloud import finalize_cloud -from bbb_presentation_video.renderer.tldraw.geo.diamond import finalize_diamond -from bbb_presentation_video.renderer.tldraw.geo.ellipse import finalize_geo_ellipse -from bbb_presentation_video.renderer.tldraw.geo.hexagon import finalize_hexagon -from bbb_presentation_video.renderer.tldraw.geo.oval import finalize_oval -from bbb_presentation_video.renderer.tldraw.geo.rectangle import finalize_geo_rectangle -from bbb_presentation_video.renderer.tldraw.geo.rhombus import finalize_rhombus -from bbb_presentation_video.renderer.tldraw.geo.star import finalize_star -from bbb_presentation_video.renderer.tldraw.geo.trapezoid import finalize_trapezoid -from bbb_presentation_video.renderer.tldraw.geo.triangle import finalize_geo_triangle -from bbb_presentation_video.renderer.tldraw.geo.xbox import finalize_x_box +from bbb_presentation_video.renderer.tldraw.geo.arrow_geo_shape import ( + finalize_geo_arrow, +) +from bbb_presentation_video.renderer.tldraw.geo.checkbox_geo_shape import ( + finalize_checkmark, +) +from bbb_presentation_video.renderer.tldraw.geo.cloud_geo_shape import finalize_cloud +from bbb_presentation_video.renderer.tldraw.geo.diamond_geo_shape import ( + finalize_diamond, +) +from bbb_presentation_video.renderer.tldraw.geo.ellipse_geo_shape import ( + finalize_geo_ellipse, +) +from bbb_presentation_video.renderer.tldraw.geo.hexagon_geo_shape import ( + finalize_hexagon, +) +from bbb_presentation_video.renderer.tldraw.geo.oval_geo_shape import finalize_oval +from bbb_presentation_video.renderer.tldraw.geo.rectangle_geo_shape import ( + finalize_geo_rectangle, +) +from bbb_presentation_video.renderer.tldraw.geo.rhombus_geo_shape import ( + finalize_rhombus, +) +from bbb_presentation_video.renderer.tldraw.geo.star_geo_shape import finalize_star +from bbb_presentation_video.renderer.tldraw.geo.trapezoid_geo_shape import ( + finalize_trapezoid, +) +from bbb_presentation_video.renderer.tldraw.geo.triangle_geo_shape import ( + finalize_geo_triangle, +) +from bbb_presentation_video.renderer.tldraw.geo.xbox_geo_shape import finalize_x_box from bbb_presentation_video.renderer.tldraw.shape import ( - ArrowGeo, + ArrowGeoShape, ArrowShape, - ArrowShape_v2, - CheckBox, - Cloud, - Diamond, + ArrowShapeV2, + CheckBoxGeoShape, + CloudGeoShape, + DiamondGeoShape, DrawShape, - EllipseGeo, + EllipseGeoShape, EllipseShape, FrameShape, GroupShape, - Hexagon, + HexagonGeoShape, HighlighterShape, LineShape, - Oval, - RectangleGeo, + OvalGeoShape, + RectangleGeoShape, RectangleShape, - Rhombus, + RhombusGeoShape, Shape, - Star, + StarGeoShape, StickyShape, - StickyShape_v2, + StickyShapeV2, TextShape, - TextShape_v2, - Trapezoid, - TriangleGeo, + TextShapeV2, + TrapezoidGeoShape, + TriangleGeoShape, TriangleShape, - XBox, + XBoxGeoShape, parse_shape_from_data, shape_sort_key, ) @@ -254,24 +272,24 @@ def finalize_shapes( else: ctx.push_group() ctx.translate(*shape.point) - if isinstance(shape, ArrowShape): + if isinstance(shape, ArrowGeoShape): + finalize_geo_arrow(ctx, id, shape) + elif isinstance(shape, ArrowShape): finalize_arrow(ctx, id, shape) - elif isinstance(shape, ArrowShape_v2): + elif isinstance(shape, ArrowShapeV2): finalize_arrow_v2(ctx, id, shape) - elif isinstance(shape, CheckBox): + elif isinstance(shape, CheckBoxGeoShape): finalize_checkmark(ctx, id, shape) - elif isinstance(shape, Cloud): + elif isinstance(shape, CloudGeoShape): finalize_cloud(ctx, id, shape) - elif isinstance(shape, Diamond): + elif isinstance(shape, DiamondGeoShape): finalize_diamond(ctx, id, shape) elif isinstance(shape, DrawShape): finalize_draw(ctx, id, shape) - elif isinstance(shape, ArrowGeo): - finalize_geo_arrow(ctx, id, shape) - elif isinstance(shape, EllipseGeo): - finalize_geo_ellipse(ctx, id, shape) elif isinstance(shape, EllipseShape): finalize_ellipse(ctx, id, shape) + elif isinstance(shape, EllipseGeoShape): + finalize_geo_ellipse(ctx, id, shape) elif isinstance(shape, FrameShape): finalize_frame( self, @@ -280,47 +298,45 @@ def finalize_shapes( shape, frame_map, ) - elif isinstance(shape, Hexagon): + elif isinstance(shape, GroupShape): + # Nothing to do? All group-related updates seem to be propagated to the + # individual shapes in the group. + pass + elif isinstance(shape, HexagonGeoShape): finalize_hexagon(ctx, id, shape) elif isinstance(shape, HighlighterShape): finalize_highlight(ctx, id, shape) elif isinstance(shape, LineShape): finalize_line(ctx, id, shape) - elif isinstance(shape, Oval): + elif isinstance(shape, OvalGeoShape): finalize_oval(ctx, id, shape) - elif isinstance(shape, RectangleGeo): - finalize_geo_rectangle(ctx, id, shape) elif isinstance(shape, RectangleShape): finalize_rectangle(ctx, id, shape) - elif isinstance(shape, Rhombus): + elif isinstance(shape, RectangleGeoShape): + finalize_geo_rectangle(ctx, id, shape) + elif isinstance(shape, RhombusGeoShape): finalize_rhombus(ctx, id, shape) - elif isinstance(shape, Star): + elif isinstance(shape, StarGeoShape): finalize_star(ctx, id, shape) - elif isinstance(shape, Trapezoid): + elif isinstance(shape, TrapezoidGeoShape): finalize_trapezoid(ctx, id, shape) - elif isinstance(shape, TriangleGeo): - finalize_geo_triangle(ctx, id, shape) elif isinstance(shape, TriangleShape): finalize_triangle(ctx, id, shape) + elif isinstance(shape, TriangleGeoShape): + finalize_geo_triangle(ctx, id, shape) elif isinstance(shape, TextShape): finalize_text(ctx, id, shape) - elif isinstance(shape, TextShape_v2): + elif isinstance(shape, TextShapeV2): finalize_v2_text(ctx, id, shape) elif isinstance(shape, StickyShape): finalize_sticky(ctx, shape) - elif isinstance(shape, StickyShape_v2): + elif isinstance(shape, StickyShapeV2): finalize_sticky_v2(ctx, shape) - elif isinstance(shape, XBox): + elif isinstance(shape, XBoxGeoShape): finalize_x_box(ctx, id, shape) - - elif isinstance(shape, GroupShape): - # Nothing to do? All group-related updates seem to be propagated to the - # individual shapes in the group. - pass else: print(f"\tTldraw: Don't know how to render {shape}") - # Mark the finalized shape as cached self.shape_patterns[id] = ctx.pop_group() ctx.set_source(self.shape_patterns[id]) diff --git a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo_shape.py similarity index 99% rename from bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py rename to bbb_presentation_video/renderer/tldraw/geo/arrow_geo_shape.py index 0b27332..ca5e64a 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/arrow_geo.py +++ b/bbb_presentation_video/renderer/tldraw/geo/arrow_geo_shape.py @@ -13,7 +13,7 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import ArrowGeo +from bbb_presentation_video.renderer.tldraw.shape import ArrowGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -28,7 +28,7 @@ def arrow_geo_stroke_points( - id: str, shape: ArrowGeo + id: str, shape: ArrowGeoShape ) -> List[perfect_freehand.types.StrokePoint]: random = Random(id) sw = STROKE_WIDTHS[shape.style.size] @@ -206,7 +206,7 @@ def arrow_geo_stroke_points( def draw_geo_arrow( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo + ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeoShape ) -> None: style = shape.style is_filled = style.isFilled @@ -237,7 +237,7 @@ def draw_geo_arrow( ctx.stroke() -def dash_geo_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowGeo) -> None: +def dash_geo_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowGeoShape) -> None: style = shape.style w = max(0, shape.size.width) @@ -295,7 +295,7 @@ def dash_geo_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowGeo) -> Non def finalize_geo_arrow( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeo + ctx: cairo.Context[CairoSomeSurface], id: str, shape: ArrowGeoShape ) -> None: print(f"\tTldraw: Finalizing Arrow (geo): {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/checkbox.py b/bbb_presentation_video/renderer/tldraw/geo/checkbox_geo_shape.py similarity index 91% rename from bbb_presentation_video/renderer/tldraw/geo/checkbox.py rename to bbb_presentation_video/renderer/tldraw/geo/checkbox_geo_shape.py index 4dd0dbe..31c0250 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/checkbox.py +++ b/bbb_presentation_video/renderer/tldraw/geo/checkbox_geo_shape.py @@ -10,8 +10,10 @@ import perfect_freehand from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.geo.rectangle import rectangle_stroke_points -from bbb_presentation_video.renderer.tldraw.shape import CheckBox +from bbb_presentation_video.renderer.tldraw.geo.rectangle_geo_shape import ( + rectangle_stroke_points, +) +from bbb_presentation_video.renderer.tldraw.shape import CheckBoxGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -49,7 +51,9 @@ def clamp_y(y: float) -> float: ] -def overlay_checkmark(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: +def overlay_checkmark( + ctx: cairo.Context[CairoSomeSurface], shape: CheckBoxGeoShape +) -> None: sw = STROKE_WIDTHS[shape.style.size] # Calculate dimensions @@ -78,7 +82,7 @@ def overlay_checkmark(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> def draw_checkbox( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox + ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBoxGeoShape ) -> None: style = shape.style is_filled = style.isFilled @@ -111,7 +115,9 @@ def draw_checkbox( overlay_checkmark(ctx, shape) -def dash_checkbox(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None: +def dash_checkbox( + ctx: cairo.Context[CairoSomeSurface], shape: CheckBoxGeoShape +) -> None: style = shape.style w = max(0, shape.size.width) @@ -129,7 +135,7 @@ def dash_checkbox(ctx: cairo.Context[CairoSomeSurface], shape: CheckBox) -> None def finalize_checkmark( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBox + ctx: cairo.Context[CairoSomeSurface], id: str, shape: CheckBoxGeoShape ) -> None: print(f"\tTldraw: Finalizing checkmark: {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/cloud.py b/bbb_presentation_video/renderer/tldraw/geo/cloud_geo_shape.py similarity index 96% rename from bbb_presentation_video/renderer/tldraw/geo/cloud.py rename to bbb_presentation_video/renderer/tldraw/geo/cloud_geo_shape.py index 93e8249..1e66686 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/cloud.py +++ b/bbb_presentation_video/renderer/tldraw/geo/cloud_geo_shape.py @@ -9,14 +9,14 @@ import math from math import atan2, tau from random import Random -from typing import Any, Callable, List, Optional, Tuple, TypedDict, TypeVar, Union +from typing import Any, Callable, List, Optional, TypedDict, TypeVar, Union import attr import cairo from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import Cloud +from bbb_presentation_video.renderer.tldraw.shape import CloudGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -277,7 +277,9 @@ def calculate_angle(center: Position, point: Position) -> float: return angle -def dash_cloud(ctx: cairo.Context[CairoSomeSurface], shape: Cloud, id: str) -> None: +def dash_cloud( + ctx: cairo.Context[CairoSomeSurface], shape: CloudGeoShape, id: str +) -> None: style = shape.style w = max(0, shape.size.width) @@ -334,7 +336,9 @@ def dash_cloud(ctx: cairo.Context[CairoSomeSurface], shape: Cloud, id: str) -> N ctx.restore() -def draw_cloud(ctx: cairo.Context[CairoSomeSurface], shape: Cloud, id: str) -> None: +def draw_cloud( + ctx: cairo.Context[CairoSomeSurface], shape: CloudGeoShape, id: str +) -> None: style = shape.style random = Random(id) @@ -412,7 +416,9 @@ def draw_cloud(ctx: cairo.Context[CairoSomeSurface], shape: Cloud, id: str) -> N ctx.restore() -def finalize_cloud(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Cloud) -> None: +def finalize_cloud( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: CloudGeoShape +) -> None: print(f"\tTldraw: Finalizing Cloud: {id}") ctx.rotate(shape.rotation) diff --git a/bbb_presentation_video/renderer/tldraw/geo/diamond.py b/bbb_presentation_video/renderer/tldraw/geo/diamond_geo_shape.py similarity index 94% rename from bbb_presentation_video/renderer/tldraw/geo/diamond.py rename to bbb_presentation_video/renderer/tldraw/geo/diamond_geo_shape.py index aa4746f..0edd27e 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/diamond.py +++ b/bbb_presentation_video/renderer/tldraw/geo/diamond_geo_shape.py @@ -13,7 +13,7 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import Diamond +from bbb_presentation_video.renderer.tldraw.shape import DiamondGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -26,7 +26,7 @@ ) -def diamond_stroke_points(id: str, shape: Diamond) -> List[StrokePoint]: +def diamond_stroke_points(id: str, shape: DiamondGeoShape) -> List[StrokePoint]: random = Random(id) size = shape.size @@ -78,7 +78,9 @@ def diamond_stroke_points(id: str, shape: Diamond) -> List[StrokePoint]: CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) -def draw_diamond(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond) -> None: +def draw_diamond( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: DiamondGeoShape +) -> None: style = shape.style stroke = STROKES[style.color] @@ -108,7 +110,7 @@ def draw_diamond(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond) ctx.stroke() -def dash_diamond(ctx: cairo.Context[CairoSomeSurface], shape: Diamond) -> None: +def dash_diamond(ctx: cairo.Context[CairoSomeSurface], shape: DiamondGeoShape) -> None: style = shape.style w = max(0, shape.size.width) @@ -128,7 +130,7 @@ def dash_diamond(ctx: cairo.Context[CairoSomeSurface], shape: Diamond) -> None: def finalize_diamond( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: Diamond + ctx: cairo.Context[CairoSomeSurface], id: str, shape: DiamondGeoShape ) -> None: print(f"\tTldraw: Finalizing Diamond: {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/ellipse.py b/bbb_presentation_video/renderer/tldraw/geo/ellipse_geo_shape.py similarity index 96% rename from bbb_presentation_video/renderer/tldraw/geo/ellipse.py rename to bbb_presentation_video/renderer/tldraw/geo/ellipse_geo_shape.py index 15db177..6411594 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/ellipse.py +++ b/bbb_presentation_video/renderer/tldraw/geo/ellipse_geo_shape.py @@ -8,7 +8,7 @@ import cairo -from bbb_presentation_video.renderer.tldraw.shape import EllipseGeo +from bbb_presentation_video.renderer.tldraw.shape import EllipseGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -22,7 +22,7 @@ CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) -def dash_ellipse(ctx: cairo.Context[CairoSomeSurface], shape: EllipseGeo) -> None: +def dash_ellipse(ctx: cairo.Context[CairoSomeSurface], shape: EllipseGeoShape) -> None: radius = (shape.size.width / 2, shape.size.height / 2) style = shape.style stroke = STROKES[style.color] @@ -56,7 +56,7 @@ def dash_ellipse(ctx: cairo.Context[CairoSomeSurface], shape: EllipseGeo) -> Non def finalize_geo_ellipse( ctx: cairo.Context[CairoSomeSurface], id: str, - shape: EllipseGeo, + shape: EllipseGeoShape, ) -> None: print(f"\tTldraw: Finalizing Ellipse (geo): {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/hexagon.py b/bbb_presentation_video/renderer/tldraw/geo/hexagon_geo_shape.py similarity index 92% rename from bbb_presentation_video/renderer/tldraw/geo/hexagon.py rename to bbb_presentation_video/renderer/tldraw/geo/hexagon_geo_shape.py index 5c49e51..7938730 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/hexagon.py +++ b/bbb_presentation_video/renderer/tldraw/geo/hexagon_geo_shape.py @@ -10,7 +10,7 @@ import perfect_freehand from perfect_freehand.types import StrokePoint -from bbb_presentation_video.renderer.tldraw.shape import Hexagon +from bbb_presentation_video.renderer.tldraw.shape import HexagonGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -25,7 +25,7 @@ ) -def hexagon_stroke_points(id: str, shape: Hexagon) -> List[StrokePoint]: +def hexagon_stroke_points(id: str, shape: HexagonGeoShape) -> List[StrokePoint]: size = shape.size width = size.width @@ -49,7 +49,9 @@ def hexagon_stroke_points(id: str, shape: Hexagon) -> List[StrokePoint]: CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) -def draw_hexagon(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Hexagon) -> None: +def draw_hexagon( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: HexagonGeoShape +) -> None: style = shape.style stroke = STROKES[style.color] @@ -79,7 +81,7 @@ def draw_hexagon(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Hexagon) ctx.stroke() -def dash_hexagon(ctx: cairo.Context[CairoSomeSurface], shape: Hexagon) -> None: +def dash_hexagon(ctx: cairo.Context[CairoSomeSurface], shape: HexagonGeoShape) -> None: style = shape.style width = max(0, shape.size.width) height = max(0, shape.size.height) @@ -93,7 +95,7 @@ def dash_hexagon(ctx: cairo.Context[CairoSomeSurface], shape: Hexagon) -> None: def finalize_hexagon( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: Hexagon + ctx: cairo.Context[CairoSomeSurface], id: str, shape: HexagonGeoShape ) -> None: print(f"\tTldraw: Finalizing Hexagon: {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/oval.py b/bbb_presentation_video/renderer/tldraw/geo/oval_geo_shape.py similarity index 87% rename from bbb_presentation_video/renderer/tldraw/geo/oval.py rename to bbb_presentation_video/renderer/tldraw/geo/oval_geo_shape.py index 3aaa781..5bae2cc 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/oval.py +++ b/bbb_presentation_video/renderer/tldraw/geo/oval_geo_shape.py @@ -10,7 +10,7 @@ import cairo from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import Oval +from bbb_presentation_video.renderer.tldraw.shape import OvalGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import finalize_geo_path @@ -43,7 +43,7 @@ def oval_points(w: float, h: float, n_vertices: int = 25) -> List[Position]: return points -def dash_oval(ctx: cairo.Context[CairoSomeSurface], shape: Oval) -> None: +def dash_oval(ctx: cairo.Context[CairoSomeSurface], shape: OvalGeoShape) -> None: style = shape.style w = max(0, shape.size.width) @@ -55,7 +55,9 @@ def dash_oval(ctx: cairo.Context[CairoSomeSurface], shape: Oval) -> None: finalize_geo_path(ctx, points, style) -def finalize_oval(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Oval) -> None: +def finalize_oval( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: OvalGeoShape +) -> None: print(f"\tTldraw: Finalizing Oval: {id}") ctx.rotate(shape.rotation) diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle_geo_shape.py similarity index 94% rename from bbb_presentation_video/renderer/tldraw/geo/rectangle.py rename to bbb_presentation_video/renderer/tldraw/geo/rectangle_geo_shape.py index 962d6fa..bc643a3 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rectangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rectangle_geo_shape.py @@ -13,7 +13,11 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import CheckBox, RectangleGeo, XBox +from bbb_presentation_video.renderer.tldraw.shape import ( + CheckBoxGeoShape, + RectangleGeoShape, + XBoxGeoShape, +) from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -27,7 +31,7 @@ def rectangle_stroke_points( - id: str, shape: Union[RectangleGeo, XBox, CheckBox] + id: str, shape: Union[RectangleGeoShape, XBoxGeoShape, CheckBoxGeoShape] ) -> List[perfect_freehand.types.StrokePoint]: random = Random(id) sw = STROKE_WIDTHS[shape.style.size] @@ -100,7 +104,7 @@ def rectangle_stroke_points( def draw_rectangle( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeoShape ) -> None: style = shape.style is_filled = style.isFilled @@ -131,7 +135,7 @@ def draw_rectangle( ctx.stroke() -def dash_rectangle(ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeo) -> None: +def dash_rectangle(ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeoShape) -> None: style = shape.style w = max(0, shape.size.width) @@ -143,7 +147,7 @@ def dash_rectangle(ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeo) -> def finalize_geo_rectangle( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeo + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RectangleGeoShape ) -> None: print(f"\tTldraw: Finalizing Rectangle (geo): {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/rhombus.py b/bbb_presentation_video/renderer/tldraw/geo/rhombus_geo_shape.py similarity index 92% rename from bbb_presentation_video/renderer/tldraw/geo/rhombus.py rename to bbb_presentation_video/renderer/tldraw/geo/rhombus_geo_shape.py index 9b2894c..39a0e10 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rhombus.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rhombus_geo_shape.py @@ -11,9 +11,9 @@ import perfect_freehand from perfect_freehand.types import StrokePoint -from bbb_presentation_video.events.helpers import Position, Size +from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import Rhombus +from bbb_presentation_video.renderer.tldraw.shape import RhombusGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -26,7 +26,7 @@ ) -def rhombus_stroke_points(id: str, shape: Rhombus) -> List[StrokePoint]: +def rhombus_stroke_points(id: str, shape: RhombusGeoShape) -> List[StrokePoint]: random = Random(id) size = shape.size @@ -77,7 +77,9 @@ def rhombus_stroke_points(id: str, shape: Rhombus) -> List[StrokePoint]: CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) -def draw_rhombus(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Rhombus) -> None: +def draw_rhombus( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RhombusGeoShape +) -> None: style = shape.style stroke = STROKES[style.color] @@ -107,7 +109,7 @@ def draw_rhombus(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Rhombus) ctx.stroke() -def dash_rhombus(ctx: cairo.Context[CairoSomeSurface], shape: Rhombus) -> None: +def dash_rhombus(ctx: cairo.Context[CairoSomeSurface], shape: RhombusGeoShape) -> None: style = shape.style width = max(0, shape.size.width) height = max(0, shape.size.height) @@ -125,7 +127,7 @@ def dash_rhombus(ctx: cairo.Context[CairoSomeSurface], shape: Rhombus) -> None: def finalize_rhombus( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: Rhombus + ctx: cairo.Context[CairoSomeSurface], id: str, shape: RhombusGeoShape ) -> None: print(f"\tTldraw: Finalizing Rhombus: {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/star.py b/bbb_presentation_video/renderer/tldraw/geo/star_geo_shape.py similarity index 88% rename from bbb_presentation_video/renderer/tldraw/geo/star.py rename to bbb_presentation_video/renderer/tldraw/geo/star_geo_shape.py index 3e82ed9..14ba862 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/star.py +++ b/bbb_presentation_video/renderer/tldraw/geo/star_geo_shape.py @@ -12,7 +12,7 @@ from perfect_freehand.types import StrokePoint from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.shape import Star +from bbb_presentation_video.renderer.tldraw.shape import StarGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -47,7 +47,7 @@ def get_star_points(w: float, h: float, n: int) -> List[Position]: return points -def star_stroke_points(id: str, shape: Star) -> List[StrokePoint]: +def star_stroke_points(id: str, shape: StarGeoShape) -> List[StrokePoint]: size = shape.size width = size.width @@ -79,7 +79,9 @@ def star_stroke_points(id: str, shape: Star) -> List[StrokePoint]: CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) -def draw_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: +def draw_star( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: StarGeoShape +) -> None: style = shape.style stroke = STROKES[style.color] @@ -109,7 +111,7 @@ def draw_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> Non ctx.stroke() -def dash_star(ctx: cairo.Context[CairoSomeSurface], shape: Star) -> None: +def dash_star(ctx: cairo.Context[CairoSomeSurface], shape: StarGeoShape) -> None: style = shape.style width = max(0, shape.size.width) height = max(0, shape.size.height) @@ -120,7 +122,9 @@ def dash_star(ctx: cairo.Context[CairoSomeSurface], shape: Star) -> None: finalize_geo_path(ctx, points, style) -def finalize_star(ctx: cairo.Context[CairoSomeSurface], id: str, shape: Star) -> None: +def finalize_star( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: StarGeoShape +) -> None: print(f"\tTldraw: Finalizing Star: {id}") style = shape.style diff --git a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py b/bbb_presentation_video/renderer/tldraw/geo/trapezoid_geo_shape.py similarity index 95% rename from bbb_presentation_video/renderer/tldraw/geo/trapezoid.py rename to bbb_presentation_video/renderer/tldraw/geo/trapezoid_geo_shape.py index effa18f..98884e4 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/trapezoid.py +++ b/bbb_presentation_video/renderer/tldraw/geo/trapezoid_geo_shape.py @@ -13,7 +13,7 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import Trapezoid +from bbb_presentation_video.renderer.tldraw.shape import TrapezoidGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -26,7 +26,7 @@ ) -def trapezoid_stroke_points(id: str, shape: Trapezoid) -> List[StrokePoint]: +def trapezoid_stroke_points(id: str, shape: TrapezoidGeoShape) -> List[StrokePoint]: random = Random(id) size = shape.size @@ -87,7 +87,7 @@ def trapezoid_stroke_points(id: str, shape: Trapezoid) -> List[StrokePoint]: def draw_trapezoid( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: Trapezoid + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TrapezoidGeoShape ) -> None: style = shape.style @@ -118,7 +118,9 @@ def draw_trapezoid( ctx.stroke() -def dash_trapezoid(ctx: cairo.Context[CairoSomeSurface], shape: Trapezoid) -> None: +def dash_trapezoid( + ctx: cairo.Context[CairoSomeSurface], shape: TrapezoidGeoShape +) -> None: style = shape.style width = max(0, shape.size.width) height = max(0, shape.size.height) @@ -137,7 +139,7 @@ def dash_trapezoid(ctx: cairo.Context[CairoSomeSurface], shape: Trapezoid) -> No def finalize_trapezoid( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: Trapezoid + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TrapezoidGeoShape ) -> None: print(f"\tTldraw: Finalizing Trapezoid: {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/triangle.py b/bbb_presentation_video/renderer/tldraw/geo/triangle_geo_shape.py similarity index 95% rename from bbb_presentation_video/renderer/tldraw/geo/triangle.py rename to bbb_presentation_video/renderer/tldraw/geo/triangle_geo_shape.py index 0f5fe3e..e6174c0 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/triangle.py +++ b/bbb_presentation_video/renderer/tldraw/geo/triangle_geo_shape.py @@ -13,7 +13,7 @@ from bbb_presentation_video.events.helpers import Position, Size from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.shape import TriangleGeo +from bbb_presentation_video.renderer.tldraw.shape import TriangleGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -31,7 +31,7 @@ def triangle_centroid(size: Size) -> Position: return (Position(w / 2, 0) + Position(w, h) + Position(0, h)) / 3 -def triangle_stroke_points(id: str, shape: TriangleGeo) -> List[StrokePoint]: +def triangle_stroke_points(id: str, shape: TriangleGeoShape) -> List[StrokePoint]: random = Random(id) size = shape.size stroke_width = STROKE_WIDTHS[shape.style.size] @@ -79,7 +79,7 @@ def triangle_stroke_points(id: str, shape: TriangleGeo) -> List[StrokePoint]: def draw_triangle( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: TriangleGeo + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TriangleGeoShape ) -> None: style = shape.style @@ -110,7 +110,9 @@ def draw_triangle( ctx.stroke() -def dash_triangle(ctx: cairo.Context[CairoSomeSurface], shape: TriangleGeo) -> None: +def dash_triangle( + ctx: cairo.Context[CairoSomeSurface], shape: TriangleGeoShape +) -> None: style = shape.style w = max(0, shape.size.width) @@ -127,7 +129,7 @@ def dash_triangle(ctx: cairo.Context[CairoSomeSurface], shape: TriangleGeo) -> N def finalize_geo_triangle( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: TriangleGeo + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TriangleGeoShape ) -> None: print(f"\tTldraw: Finalizing Triangle (geo): {id}") diff --git a/bbb_presentation_video/renderer/tldraw/geo/xbox.py b/bbb_presentation_video/renderer/tldraw/geo/xbox_geo_shape.py similarity index 87% rename from bbb_presentation_video/renderer/tldraw/geo/xbox.py rename to bbb_presentation_video/renderer/tldraw/geo/xbox_geo_shape.py index 9de879d..c17638c 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/xbox.py +++ b/bbb_presentation_video/renderer/tldraw/geo/xbox_geo_shape.py @@ -10,8 +10,10 @@ import perfect_freehand from bbb_presentation_video.events.helpers import Position -from bbb_presentation_video.renderer.tldraw.geo.rectangle import rectangle_stroke_points -from bbb_presentation_video.renderer.tldraw.shape import XBox +from bbb_presentation_video.renderer.tldraw.geo.rectangle_geo_shape import ( + rectangle_stroke_points, +) +from bbb_presentation_video.renderer.tldraw.shape import XBoxGeoShape from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_v2_label from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, @@ -26,7 +28,7 @@ CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) -def overlay_x_cross(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: +def overlay_x_cross(ctx: cairo.Context[CairoSomeSurface], shape: XBoxGeoShape) -> None: sw = STROKE_WIDTHS[shape.style.size] # Dimensions @@ -53,7 +55,9 @@ def overlay_x_cross(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: ctx.stroke() -def draw_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: +def draw_x_box( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBoxGeoShape +) -> None: style = shape.style is_filled = style.isFilled stroke = STROKES[style.color] @@ -85,7 +89,7 @@ def draw_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> No overlay_x_cross(ctx, shape) -def dash_x_box(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: +def dash_x_box(ctx: cairo.Context[CairoSomeSurface], shape: XBoxGeoShape) -> None: style = shape.style w = max(0, shape.size.width) @@ -102,7 +106,9 @@ def dash_x_box(ctx: cairo.Context[CairoSomeSurface], shape: XBox) -> None: overlay_x_cross(ctx, shape) -def finalize_x_box(ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBox) -> None: +def finalize_x_box( + ctx: cairo.Context[CairoSomeSurface], id: str, shape: XBoxGeoShape +) -> None: print(f"\tTldraw: Finalizing x-box: {id}") ctx.rotate(shape.rotation) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index eb88d11..2ccc980 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -241,7 +241,7 @@ class RectangleShape(LabelledShapeProto): @attr.s(order=False, slots=True, auto_attribs=True) -class RectangleGeo(LabelledShapeProto): +class RectangleGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @@ -262,7 +262,7 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) -class EllipseGeo(LabelledShapeProto): +class EllipseGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @@ -280,17 +280,17 @@ class TriangleShape(LabelledShapeProto): @attr.s(order=False, slots=True, auto_attribs=True) -class TriangleGeo(LabelledShapeProto): +class TriangleGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class Diamond(LabelledShapeProto): +class DiamondGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class Trapezoid(LabelledShapeProto): +class TrapezoidGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @@ -306,7 +306,7 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) -class TextShape_v2(RotatableShapeProto): +class TextShapeV2(RotatableShapeProto): text: str = "" def update_from_data(self, data: ShapeData) -> None: @@ -318,42 +318,42 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) -class Rhombus(LabelledShapeProto): +class RhombusGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class Hexagon(LabelledShapeProto): +class HexagonGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class Cloud(LabelledShapeProto): +class CloudGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class Star(LabelledShapeProto): +class StarGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class Oval(LabelledShapeProto): +class OvalGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class XBox(LabelledShapeProto): +class XBoxGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class CheckBox(LabelledShapeProto): +class CheckBoxGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class ArrowGeo(LabelledShapeProto): +class ArrowGeoShape(LabelledShapeProto): size: Size = Size(1.0, 1.0) @@ -377,7 +377,7 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) -class StickyShape_v2(RotatableShapeProto): +class StickyShapeV2(RotatableShapeProto): text: str = "" align: AlignStyle = AlignStyle.MIDDLE verticalAlign: AlignStyle = AlignStyle.MIDDLE @@ -500,7 +500,7 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) -class ArrowShape_v2(LabelledShapeProto): +class ArrowShapeV2(LabelledShapeProto): bend: float = 0.0 handles: ArrowHandles = attr.Factory(ArrowHandles) """Locations of the line start, end, and bend points.""" @@ -554,33 +554,33 @@ def update_from_data(self, data: ShapeData) -> None: Shape = Union[ + ArrowGeoShape, ArrowShape, - ArrowShape_v2, - Diamond, + ArrowShapeV2, + CheckBoxGeoShape, + CloudGeoShape, + DiamondGeoShape, DrawShape, - EllipseGeo, + EllipseGeoShape, EllipseShape, FrameShape, GroupShape, + HexagonGeoShape, HighlighterShape, LineShape, - RectangleGeo, + OvalGeoShape, + RectangleGeoShape, RectangleShape, + RhombusGeoShape, + StarGeoShape, StickyShape, - StickyShape_v2, + StickyShapeV2, TextShape, - TextShape_v2, - Trapezoid, - TriangleGeo, + TextShapeV2, + TrapezoidGeoShape, + TriangleGeoShape, TriangleShape, - Rhombus, - Hexagon, - Cloud, - Star, - Oval, - XBox, - CheckBox, - ArrowGeo, + XBoxGeoShape, ] @@ -598,12 +598,12 @@ def parse_shape_from_data(data: ShapeData, bbb_version: Version) -> Shape: return TriangleShape.from_data(data) elif type == "arrow": if is_tldraw_v2: - return ArrowShape_v2.from_data(data) + return ArrowShapeV2.from_data(data) else: return ArrowShape.from_data(data) elif type == "text": if is_tldraw_v2: - return TextShape_v2.from_data(data) + return TextShapeV2.from_data(data) else: return TextShape.from_data(data) elif type == "group": @@ -611,7 +611,7 @@ def parse_shape_from_data(data: ShapeData, bbb_version: Version) -> Shape: elif type == "sticky": return StickyShape.from_data(data) elif type == "note": - return StickyShape_v2.from_data(data) + return StickyShapeV2.from_data(data) elif type == "line": return LineShape.from_data(data) elif type == "highlight": @@ -623,36 +623,36 @@ def parse_shape_from_data(data: ShapeData, bbb_version: Version) -> Shape: geo_type = GeoShape(data["props"]["geo"]) if geo_type is GeoShape.DIAMOND: - return Diamond.from_data(data) + return DiamondGeoShape.from_data(data) if geo_type is GeoShape.ELLIPSE: - return EllipseGeo.from_data(data) + return EllipseGeoShape.from_data(data) if geo_type is GeoShape.RECTANGLE: - return RectangleGeo.from_data(data) + return RectangleGeoShape.from_data(data) if geo_type is GeoShape.TRIANGLE: - return TriangleGeo.from_data(data) + return TriangleGeoShape.from_data(data) if geo_type is GeoShape.TRAPEZOID: - return Trapezoid.from_data(data) + return TrapezoidGeoShape.from_data(data) if geo_type is GeoShape.RHOMBUS: - return Rhombus.from_data(data) + return RhombusGeoShape.from_data(data) if geo_type is GeoShape.HEXAGON: - return Hexagon.from_data(data) + return HexagonGeoShape.from_data(data) if geo_type is GeoShape.CLOUD: - return Cloud.from_data(data) + return CloudGeoShape.from_data(data) if geo_type is GeoShape.STAR: - return Star.from_data(data) + return StarGeoShape.from_data(data) if geo_type is GeoShape.OVAL: - return Oval.from_data(data) + return OvalGeoShape.from_data(data) if geo_type is GeoShape.CHECKBOX: - return CheckBox.from_data(data) + return CheckBoxGeoShape.from_data(data) if geo_type is GeoShape.XBOX: - return XBox.from_data(data) + return XBoxGeoShape.from_data(data) if geo_type in [ GeoShape.ARROW_DOWN, GeoShape.ARROW_LEFT, GeoShape.ARROW_RIGHT, GeoShape.ARROW_UP, ]: - return ArrowGeo.from_data(data) + return ArrowGeoShape.from_data(data) raise Exception(f"Unknown geo shape: {type}") else: raise Exception(f"Unknown shape type: {type}") diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py index 3dd4600..2591618 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py @@ -16,7 +16,7 @@ intersect_circle_line_segment, ) from bbb_presentation_video.renderer.tldraw.shape import ( - ArrowShape_v2, + ArrowShapeV2, apply_shape_rotation, ) from bbb_presentation_video.renderer.tldraw.utils import ( @@ -94,7 +94,7 @@ def curved_arrow_head( ctx.line_to(right.x, right.y) -def straight_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowShape_v2) -> float: +def straight_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowShapeV2) -> float: style = shape.style start = shape.handles.start end = shape.handles.end @@ -157,7 +157,7 @@ def get_midpoint(start: Position, end: Position, bend: float) -> Position: def curved_arrow( ctx: cairo.Context[CairoSomeSurface], - shape: ArrowShape_v2, + shape: ArrowShapeV2, ) -> float: style = shape.style start = shape.handles.start @@ -220,7 +220,7 @@ def curved_arrow( def finalize_arrow_v2( ctx: cairo.Context[CairoSomeSurface], id: str, - shape: ArrowShape_v2, + shape: ArrowShapeV2, ) -> None: print(f"\tTldraw: Finalizing Arrow (v2): {id}") diff --git a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py index 697ca2e..d7bedf7 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/sticky_v2.py @@ -9,7 +9,7 @@ import cairo from bbb_presentation_video.events.helpers import Size -from bbb_presentation_video.renderer.tldraw.shape import StickyShape_v2 +from bbb_presentation_video.renderer.tldraw.shape import StickyShapeV2 from bbb_presentation_video.renderer.tldraw.shape.text_v2 import finalize_sticky_text_v2 from bbb_presentation_video.renderer.tldraw.utils import ( STICKY_FILLS, @@ -21,7 +21,7 @@ def finalize_sticky_v2( - ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 + ctx: cairo.Context[CairoSomeSurface], shape: StickyShapeV2 ) -> None: style = shape.style diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py index a56b264..f0bcd62 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py @@ -12,8 +12,8 @@ from bbb_presentation_video.renderer.tldraw.shape import ( FrameShape, LabelledShapeProto, - StickyShape_v2, - TextShape_v2, + StickyShapeV2, + TextShapeV2, ) from bbb_presentation_video.renderer.tldraw.shape.text import ( create_pango_layout, @@ -42,7 +42,7 @@ def finalize_v2_text( - ctx: cairo.Context[CairoSomeSurface], id: str, shape: TextShape_v2 + ctx: cairo.Context[CairoSomeSurface], id: str, shape: TextShapeV2 ) -> None: print(f"\tTldraw: Finalizing Text (v2): {id}") @@ -187,7 +187,7 @@ def finalize_frame_name( def finalize_sticky_text_v2( - ctx: cairo.Context[CairoSomeSurface], shape: StickyShape_v2 + ctx: cairo.Context[CairoSomeSurface], shape: StickyShapeV2 ) -> None: if shape.text is None or shape.text == "": return From a55812d11cd22de62e763a810a1a5a108581424b Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 17 Apr 2024 15:38:39 +0200 Subject: [PATCH 20/25] Document properties that are inherited from superclasses --- .../renderer/tldraw/shape/__init__.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 2ccc980..653b737 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -59,6 +59,9 @@ def update_from_data(self, data: ShapeData) -> None: if "childIndex" in data: self.childIndex = data["childIndex"] + if "children" in data: + self.children = data["children"] + if "point" in data: point = data["point"] self.point = Position(point[0], point[1]) @@ -142,8 +145,6 @@ def update_from_data(self, data: ShapeData) -> None: self.label = data["label"] if data["label"] != "" else None if "labelPoint" in data: self.labelPoint = Position(data["labelPoint"]) - if "children" in data: - self.children = data["children"] if "props" in data: props = data["props"] @@ -242,6 +243,7 @@ class RectangleShape(LabelledShapeProto): @attr.s(order=False, slots=True, auto_attribs=True) class RectangleGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @@ -263,13 +265,17 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) class EllipseGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class FrameShape(LabelledShapeProto): - label: str = "Frame" + # BaseShapeProto children: List[Shape] = [] + # LabelledShapeProto + label: str = "Frame" + # SizedShapeProto size: Size = Size(1.0, 1.0) @@ -281,16 +287,19 @@ class TriangleShape(LabelledShapeProto): @attr.s(order=False, slots=True, auto_attribs=True) class TriangleGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class DiamondGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class TrapezoidGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @@ -319,41 +328,49 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) class RhombusGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class HexagonGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class CloudGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class StarGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class OvalGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class XBoxGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class CheckBoxGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) class ArrowGeoShape(LabelledShapeProto): + # SizedShapeProto size: Size = Size(1.0, 1.0) From b5361a3e59f47db225a8c73386f770d0e9e92592 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 17 Apr 2024 17:12:28 +0200 Subject: [PATCH 21/25] Delete duplicate arrow code --- .../renderer/tldraw/shape/arrow.py | 4 +- .../renderer/tldraw/shape/arrow_v2.py | 46 +------------------ 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow.py b/bbb_presentation_video/renderer/tldraw/shape/arrow.py index 7d51c7a..e9d31e0 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow.py @@ -169,8 +169,8 @@ def curved_arrow_head( right = a else: int = ints[0] if sweep else ints[1] - left = Position(vec.nudge(vec.rot_with(int, a, pi / 6), a, r1 * -0.382)) - right = Position(vec.nudge(vec.rot_with(int, a, -pi / 6), a, r1 * -0.382)) + left = Position(vec.nudge(vec.rot_with(int, a, tau / 12), a, r1 * -0.382)) + right = Position(vec.nudge(vec.rot_with(int, a, -tau / 12), a, r1 * -0.382)) ctx.move_to(left.x, left.y) ctx.line_to(a.x, a.y) diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py index 2591618..1aa7faf 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py @@ -12,13 +12,13 @@ from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec from bbb_presentation_video.renderer.tldraw.intersect import ( - intersect_circle_circle, intersect_circle_line_segment, ) from bbb_presentation_video.renderer.tldraw.shape import ( ArrowShapeV2, apply_shape_rotation, ) +from bbb_presentation_video.renderer.tldraw.shape.arrow import curved_arrow_head, curved_arrow_shaft from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, STROKES, @@ -30,26 +30,6 @@ CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) - -def curved_arrow_shaft( - ctx: cairo.Context[CairoSomeSurface], - start: Position, - end: Position, - center: Position, - radius: float, - bend: float, -) -> None: - start_angle = vec.angle(center, start) - end_angle = vec.angle(center, end) - - ctx.new_sub_path() - - if bend > 0: - ctx.arc(center.x, center.y, radius, start_angle, end_angle) - else: - ctx.arc_negative(center.x, center.y, radius, start_angle, end_angle) - - def straight_arrow_head( ctx: cairo.Context[CairoSomeSurface], a: Position, @@ -70,30 +50,6 @@ def straight_arrow_head( ctx.line_to(a.x, a.y) ctx.line_to(right.x, right.y) - -def curved_arrow_head( - ctx: cairo.Context[CairoSomeSurface], - a: Position, - r1: float, - C: Position, - r2: float, - sweep: bool, -) -> None: - ints = intersect_circle_circle(a, r1 * 0.618, C, r2).points - if len(ints) == 0: - print("\t\tCould not find an intersection for the arrow head.") - left = a - right = a - else: - int = ints[0] if sweep else ints[1] - left = Position(vec.nudge(vec.rot_with(int, a, tau / 12), a, r1 * -0.382)) - right = Position(vec.nudge(vec.rot_with(int, a, -tau / 12), a, r1 * -0.382)) - - ctx.move_to(left.x, left.y) - ctx.line_to(a.x, a.y) - ctx.line_to(right.x, right.y) - - def straight_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowShapeV2) -> float: style = shape.style start = shape.handles.start From 43e304559d4775757489a021d7337ace59f1f786 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 17 Apr 2024 17:16:04 +0200 Subject: [PATCH 22/25] Delete duplicate arrow code --- .../renderer/tldraw/fonts/.uuid | 1 + .../tldraw/geo/rectangle_geo_shape.py | 4 ++- .../renderer/tldraw/shape/arrow.py | 4 +-- .../renderer/tldraw/shape/arrow_v2.py | 30 ++++--------------- 4 files changed, 11 insertions(+), 28 deletions(-) create mode 100644 bbb_presentation_video/renderer/tldraw/fonts/.uuid diff --git a/bbb_presentation_video/renderer/tldraw/fonts/.uuid b/bbb_presentation_video/renderer/tldraw/fonts/.uuid new file mode 100644 index 0000000..d34b1ed --- /dev/null +++ b/bbb_presentation_video/renderer/tldraw/fonts/.uuid @@ -0,0 +1 @@ +a039a842-7060-4fb6-9461-3ebe1f2013f5 \ No newline at end of file diff --git a/bbb_presentation_video/renderer/tldraw/geo/rectangle_geo_shape.py b/bbb_presentation_video/renderer/tldraw/geo/rectangle_geo_shape.py index bc643a3..0502366 100644 --- a/bbb_presentation_video/renderer/tldraw/geo/rectangle_geo_shape.py +++ b/bbb_presentation_video/renderer/tldraw/geo/rectangle_geo_shape.py @@ -135,7 +135,9 @@ def draw_rectangle( ctx.stroke() -def dash_rectangle(ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeoShape) -> None: +def dash_rectangle( + ctx: cairo.Context[CairoSomeSurface], shape: RectangleGeoShape +) -> None: style = shape.style w = max(0, shape.size.width) diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow.py b/bbb_presentation_video/renderer/tldraw/shape/arrow.py index e9d31e0..332f284 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow.py @@ -146,8 +146,8 @@ def straight_arrow_head( right = a else: int = ints[0] - left = Position(vec.rot_with(int, a, pi / 6)) - right = Position(vec.rot_with(int, a, -pi / 6)) + left = Position(vec.rot_with(int, a, tau / 12)) + right = Position(vec.rot_with(int, a, -tau / 12)) ctx.move_to(left.x, left.y) ctx.line_to(a.x, a.y) diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py index 1aa7faf..b9d04f9 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py @@ -3,22 +3,21 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations - -from math import tau from typing import TypeVar import cairo from bbb_presentation_video.events.helpers import Position from bbb_presentation_video.renderer.tldraw import vec -from bbb_presentation_video.renderer.tldraw.intersect import ( - intersect_circle_line_segment, -) from bbb_presentation_video.renderer.tldraw.shape import ( ArrowShapeV2, apply_shape_rotation, ) -from bbb_presentation_video.renderer.tldraw.shape.arrow import curved_arrow_head, curved_arrow_shaft +from bbb_presentation_video.renderer.tldraw.shape.arrow import ( + curved_arrow_head, + curved_arrow_shaft, + straight_arrow_head, +) from bbb_presentation_video.renderer.tldraw.utils import ( STROKE_WIDTHS, STROKES, @@ -30,25 +29,6 @@ CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) -def straight_arrow_head( - ctx: cairo.Context[CairoSomeSurface], - a: Position, - b: Position, - r: float, -) -> None: - ints = intersect_circle_line_segment(a, r, a, b).points - if len(ints) == 0: - print("\t\tCould not find an intersection for the arrow head.") - left = a - right = a - else: - int = ints[0] - left = Position(vec.rot_with(int, a, tau / 12)) - right = Position(vec.rot_with(int, a, -tau / 12)) - - ctx.move_to(left.x, left.y) - ctx.line_to(a.x, a.y) - ctx.line_to(right.x, right.y) def straight_arrow(ctx: cairo.Context[CairoSomeSurface], shape: ArrowShapeV2) -> float: style = shape.style From 28a5307579c1c059a90f4aa245dced0d30238f30 Mon Sep 17 00:00:00 2001 From: danielpetri1 Date: Wed, 17 Apr 2024 17:52:46 +0200 Subject: [PATCH 23/25] Add GeoShapeProto --- .../renderer/tldraw/shape/__init__.py | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/bbb_presentation_video/renderer/tldraw/shape/__init__.py b/bbb_presentation_video/renderer/tldraw/shape/__init__.py index 653b737..77a59fb 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/__init__.py +++ b/bbb_presentation_video/renderer/tldraw/shape/__init__.py @@ -128,9 +128,6 @@ class LabelledShapeProto(RotatableShapeProto, Protocol): verticalAlign: AlignStyle = AlignStyle.MIDDLE """Vertical alignment of the label.""" - geo: GeoShape = GeoShape.NONE - """Which geo type the shape is, if any.""" - def label_offset(self) -> Position: """Calculate the offset needed when drawing the label for most shapes.""" return Position( @@ -154,13 +151,26 @@ def update_from_data(self, data: ShapeData) -> None: self.align = AlignStyle(props["align"]) if "verticalAlign" in props: self.verticalAlign = AlignStyle(props["verticalAlign"]) - if "geo" in props: - self.geo = GeoShape(props["geo"]) if "w" in props and "h" in props and "name" in props: if not props["name"] == "": self.label = props["name"] +@attr.s(order=False, slots=True, auto_attribs=True) +class GeoShapeProto(LabelledShapeProto, Protocol): + geo: GeoShape = GeoShape.NONE + """Which geo type the shape is""" + + def update_from_data(self, data: ShapeData) -> None: + super().update_from_data(data) + + if "props" in data: + props = data["props"] + + if "geo" in props: + self.geo = GeoShape(props["geo"]) + + def shape_sort_key(shape: BaseShapeProto) -> float: return shape.childIndex @@ -242,7 +252,7 @@ class RectangleShape(LabelledShapeProto): @attr.s(order=False, slots=True, auto_attribs=True) -class RectangleGeoShape(LabelledShapeProto): +class RectangleGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @@ -264,7 +274,7 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) -class EllipseGeoShape(LabelledShapeProto): +class EllipseGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @@ -286,19 +296,19 @@ class TriangleShape(LabelledShapeProto): @attr.s(order=False, slots=True, auto_attribs=True) -class TriangleGeoShape(LabelledShapeProto): +class TriangleGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class DiamondGeoShape(LabelledShapeProto): +class DiamondGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class TrapezoidGeoShape(LabelledShapeProto): +class TrapezoidGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @@ -327,49 +337,49 @@ def update_from_data(self, data: ShapeData) -> None: @attr.s(order=False, slots=True, auto_attribs=True) -class RhombusGeoShape(LabelledShapeProto): +class RhombusGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class HexagonGeoShape(LabelledShapeProto): +class HexagonGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class CloudGeoShape(LabelledShapeProto): +class CloudGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class StarGeoShape(LabelledShapeProto): +class StarGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class OvalGeoShape(LabelledShapeProto): +class OvalGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class XBoxGeoShape(LabelledShapeProto): +class XBoxGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class CheckBoxGeoShape(LabelledShapeProto): +class CheckBoxGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) @attr.s(order=False, slots=True, auto_attribs=True) -class ArrowGeoShape(LabelledShapeProto): +class ArrowGeoShape(GeoShapeProto): # SizedShapeProto size: Size = Size(1.0, 1.0) From f195461ca3d69095927c9f3de6475d073d68760a Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha <33319791+danielpetri1@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:27:47 +0200 Subject: [PATCH 24/25] Fix mypy CI error --- bbb_presentation_video/bindings/fontconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb_presentation_video/bindings/fontconfig.py b/bbb_presentation_video/bindings/fontconfig.py index 225be4f..c2c4716 100644 --- a/bbb_presentation_video/bindings/fontconfig.py +++ b/bbb_presentation_video/bindings/fontconfig.py @@ -15,7 +15,7 @@ class FontconfigError(Exception): def _FcBool_errcheck( - result: Optional[Type[ctypes._CData]], + result: Optional[ctypes._CData], _func: Any, _arguments: Any, ) -> Any: From 52c70e6c7c6c82cfb1bbfa6e9d00df369ccd103c Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 10 Jun 2024 16:25:36 -0400 Subject: [PATCH 25/25] Rework tldraw text v2 to use proper outline instead of multiple shadows --- .../renderer/tldraw/fonts/.uuid | 1 - .../renderer/tldraw/shape/arrow_v2.py | 1 + .../renderer/tldraw/shape/text.py | 11 ++- .../renderer/tldraw/shape/text_v2.py | 82 +++++++++---------- typings/gi/repository/PangoCairo.pyi | 10 +++ 5 files changed, 58 insertions(+), 47 deletions(-) delete mode 100644 bbb_presentation_video/renderer/tldraw/fonts/.uuid diff --git a/bbb_presentation_video/renderer/tldraw/fonts/.uuid b/bbb_presentation_video/renderer/tldraw/fonts/.uuid deleted file mode 100644 index d34b1ed..0000000 --- a/bbb_presentation_video/renderer/tldraw/fonts/.uuid +++ /dev/null @@ -1 +0,0 @@ -a039a842-7060-4fb6-9461-3ebe1f2013f5 \ No newline at end of file diff --git a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py index b9d04f9..75de454 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/arrow_v2.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations + from typing import TypeVar import cairo diff --git a/bbb_presentation_video/renderer/tldraw/shape/text.py b/bbb_presentation_video/renderer/tldraw/shape/text.py index 32237bb..f0f2aaa 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text.py @@ -94,7 +94,11 @@ def create_pango_layout( def show_layout_by_lines( - ctx: cairo.Context[CairoSomeSurface], layout: Pango.Layout, *, padding: float = 0 + ctx: cairo.Context[CairoSomeSurface], + layout: Pango.Layout, + *, + padding: float = 0, + do_path: bool = False, ) -> None: """Show a Pango Layout line by line to manually handle CSS-style line height.""" # TODO: With Pango 1.50 this can be replaced with Pango.attr_line_height_new_absolute @@ -128,7 +132,10 @@ def show_layout_by_lines( ctx.save() ctx.translate(offset_x, offset_y) - PangoCairo.show_layout_line(ctx, line) + if do_path: + PangoCairo.layout_line_path(ctx, line) + else: + PangoCairo.show_layout_line(ctx, line) ctx.restore() ctx.translate(0, line_height) diff --git a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py index f0bcd62..2fb45f4 100644 --- a/bbb_presentation_video/renderer/tldraw/shape/text_v2.py +++ b/bbb_presentation_video/renderer/tldraw/shape/text_v2.py @@ -21,6 +21,7 @@ show_layout_by_lines, ) from bbb_presentation_video.renderer.tldraw.utils import ( + CANVAS, FONT_SIZES, STICKY_FONT_SIZES, STICKY_PADDING, @@ -32,11 +33,7 @@ SizeStyle, ) -gi.require_version("Pango", "1.0") -gi.require_version("PangoCairo", "1.0") - -# Set DPI to "72" so we're working directly in Pango point units. -DPI: float = 72.0 +TEXT_OUTLINE_WIDTH: float = 2.0 CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface) @@ -47,33 +44,35 @@ def finalize_v2_text( print(f"\tTldraw: Finalizing Text (v2): {id}") style = shape.style - ctx.rotate(shape.rotation) - stroke = STROKES[style.color] font_size = FONT_SIZES[style.size] + ctx.rotate(shape.rotation) + + # A group is used so the text border and fill can be drawn opaque (to avoid over-draw issues), then + # be blended with alpha afterwards + ctx.push_group() + layout = create_pango_layout(ctx, style, font_size) layout.set_text(shape.text, -1) - border_thickness = 2 - border_color = (1, 1, 1, 1) # White - # Draw the border by offsetting the text in several directions - offsets = [ - (-border_thickness, -border_thickness), - (border_thickness, -border_thickness), - (-border_thickness, border_thickness), - (border_thickness, border_thickness), - ] - - for dx, dy in offsets: - ctx.translate(dx, dy) - ctx.set_source_rgba(*border_color) - show_layout_by_lines(ctx, layout, padding=4) - ctx.translate(-dx, -dy) # Reset translation for next iteration + # Draw text border (outside stroke) + ctx.save() + ctx.set_source_rgb(*CANVAS) + ctx.set_line_width(TEXT_OUTLINE_WIDTH * 2) + ctx.set_line_join(cairo.LINE_JOIN_ROUND) + show_layout_by_lines(ctx, layout, padding=4, do_path=True) + ctx.stroke() + ctx.restore() - ctx.set_source_rgba(stroke.r, stroke.g, stroke.b, style.opacity) + # Draw text + ctx.set_source_rgb(*stroke) show_layout_by_lines(ctx, layout, padding=4) + # Composite result with opacity applied + ctx.pop_group_to_source() + ctx.paint_with_alpha(style.opacity) + def finalize_v2_label( ctx: cairo.Context[CairoSomeSurface], @@ -88,10 +87,11 @@ def finalize_v2_label( style = shape.style stroke = STROKES[ColorStyle.BLACK] # v2 labels are always black - border_color = (1, 1, 1, 1) # White font_size = FONT_SIZES[style.size] - ctx.save() + # A group is used so the text border and fill can be drawn opaque (to avoid over-draw issues), then + # be blended with alpha afterwards + ctx.push_group() # Create layout aligning the text horizontally within the shape style.textAlign = shape.align @@ -116,30 +116,24 @@ def finalize_v2_label( else: y = bounds.height / 2 - label_size.height / 2 + offset.y - border_thickness = 2 - - # Draw the border by offsetting the text in several directions - offsets = [ - (-border_thickness, -border_thickness), - (border_thickness, -border_thickness), - (-border_thickness, border_thickness), - (border_thickness, border_thickness), - ] + ctx.translate(x, y) - for dx, dy in offsets: - ctx.translate(x + dx, y + dy) - ctx.set_source_rgba(*border_color) - show_layout_by_lines(ctx, layout, padding=4) - ctx.translate(-x - dx, -y - dy) # Reset translation for next iteration + # Draw text border (outside stroke) + ctx.save() + ctx.set_source_rgb(*CANVAS) + ctx.set_line_width(TEXT_OUTLINE_WIDTH * 2) + ctx.set_line_join(cairo.LINE_JOIN_ROUND) + show_layout_by_lines(ctx, layout, padding=4, do_path=True) + ctx.stroke() + ctx.restore() # Draw the original text on top - ctx.translate(x, y) - ctx.set_source_rgba( - stroke.r, stroke.g, stroke.b, style.opacity - ) # Set original text color + ctx.set_source_rgb(*stroke) show_layout_by_lines(ctx, layout, padding=4) - ctx.restore() + # Composite result with opacity applied + ctx.pop_group_to_source() + ctx.paint_with_alpha(style.opacity) return label_size diff --git a/typings/gi/repository/PangoCairo.pyi b/typings/gi/repository/PangoCairo.pyi index 2322d3c..03767de 100644 --- a/typings/gi/repository/PangoCairo.pyi +++ b/typings/gi/repository/PangoCairo.pyi @@ -42,6 +42,16 @@ def create_context(cr: cairo.Context[cairo._SomeSurface]) -> Pango.Context: directly, you can use :func:`create_layout` instead. """ +def layout_line_path( + cr: cairo.Context[cairo._SomeSurface], line: Pango.LayoutLine +) -> None: + """Adds the text in :class:`Pango.LayoutLine` to the current path in the specified cairo context. + + The origin of the glyphs (the left edge of the line) will be at the current point of the cairo context. + + Since: 1.10 + """ + def show_layout( cr: cairo.Context[cairo._SomeSurface], layout: Pango.Layout ) -> None: ...