From 60fed54aca4821895130e194aba08f7ec3d42c70 Mon Sep 17 00:00:00 2001 From: mozman Date: Sun, 24 Mar 2024 08:41:21 +0100 Subject: [PATCH] refactor and rename designer.py to pipeline.py This implements the previous designer class a render pipeline between the frontend and the output backend. Clipping and linetype rendering will be separated into independent stages in the future. --- src/ezdxf/addons/drawing/frontend.py | 88 ++-- .../drawing/{designer.py => pipeline.py} | 415 +++++++++++------- 2 files changed, 307 insertions(+), 196 deletions(-) rename src/ezdxf/addons/drawing/{designer.py => pipeline.py} (72%) diff --git a/src/ezdxf/addons/drawing/frontend.py b/src/ezdxf/addons/drawing/frontend.py index ea9fcfd3d..31e71a911 100644 --- a/src/ezdxf/addons/drawing/frontend.py +++ b/src/ezdxf/addons/drawing/frontend.py @@ -91,7 +91,7 @@ from .type_hints import Color if TYPE_CHECKING: - from .designer import Designer + from .pipeline import AbstractPipeline __all__ = ["Frontend", "UniversalFrontend"] @@ -126,7 +126,7 @@ class UniversalFrontend: def __init__( self, ctx: RenderContext, - designer: Designer, + pipeline: AbstractPipeline, config: Configuration = Configuration(), bbox_cache: Optional[ezdxf.bbox.Cache] = None, ): @@ -134,10 +134,10 @@ def __init__( # specific DXF document. self.ctx = ctx # the designer is the connection between frontend and backend - self.designer = designer - designer.set_draw_entities_callback(self.draw_entities_callback) + self.pipeline = pipeline + pipeline.set_draw_entities_callback(self.draw_entities_callback) self.config = ctx.update_configuration(config) - designer.set_config(self.config) + pipeline.set_config(self.config) if self.config.pdsize is None or self.config.pdsize <= 0: self.log_message("relative point size is not supported") @@ -171,7 +171,7 @@ def __init__( @property def text_engine(self): - return self.designer.text_engine + return self.pipeline.text_engine def _build_dispatch_table(self) -> TDispatchTable: dispatch_table: TDispatchTable = { @@ -302,7 +302,7 @@ def draw_layout( filter_func=filter_func, ) if finalize: - self.designer.finalize() + self.pipeline.finalize() def set_background(self, color: Color) -> None: policy = self.config.background_policy @@ -323,7 +323,7 @@ def set_background(self, color: Color) -> None: color = self.config.custom_bg_color if override: self.ctx.current_layout_properties.set_colors(color) - self.designer.set_background(color) + self.pipeline.set_background(color) def draw_entities( self, @@ -350,10 +350,10 @@ def draw_entity(self, entity: DXFGraphic, properties: Properties) -> None: properties: resolved entity properties """ - self.designer.enter_entity(entity, properties) + self.pipeline.enter_entity(entity, properties) if not entity.is_virtual: # top level entity - self.designer.set_current_entity_handle(entity.dxf.handle) + self.pipeline.set_current_entity_handle(entity.dxf.handle) if ( entity.proxy_graphic and self.config.proxy_graphic_policy == ProxyGraphicPolicy.PREFER @@ -384,22 +384,22 @@ def draw_entity(self, entity: DXFGraphic, properties: Properties) -> None: else: self.skip_entity(entity, "unsupported") - self.designer.exit_entity(entity) + self.pipeline.exit_entity(entity) def draw_line_entity(self, entity: DXFGraphic, properties: Properties) -> None: d, dxftype = entity.dxf, entity.dxftype() if dxftype == "LINE": - self.designer.draw_line(d.start, d.end, properties) + self.pipeline.draw_line(d.start, d.end, properties) elif dxftype in ("XLINE", "RAY"): start = d.start delta = d.unit_vector * self.config.infinite_line_length if dxftype == "XLINE": - self.designer.draw_line( + self.pipeline.draw_line( start - delta / 2, start + delta / 2, properties ) elif dxftype == "RAY": - self.designer.draw_line(start, start + delta, properties) + self.pipeline.draw_line(start, start + delta, properties) else: raise TypeError(dxftype) @@ -417,7 +417,7 @@ def draw_text_entity(self, entity: DXFGraphic, properties: Properties) -> None: def get_font_face(self, properties: Properties) -> fonts.FontFace: font_face = properties.font if font_face is None: - return self.designer.default_font_face + return self.pipeline.default_font_face return font_face def draw_text_entity_2d(self, entity: DXFGraphic, properties: Properties) -> None: @@ -425,7 +425,7 @@ def draw_text_entity_2d(self, entity: DXFGraphic, properties: Properties) -> Non for line, transform, cap_height in simplified_text_chunks( entity, self.text_engine, font_face=self.get_font_face(properties) ): - self.designer.draw_text( + self.pipeline.draw_text( line, transform, properties, cap_height, entity.dxftype() ) else: @@ -454,20 +454,20 @@ def draw_simple_mtext(self, mtext: MText, properties: Properties) -> None: for line, transform, cap_height in simplified_text_chunks( mtext, self.text_engine, font_face=self.get_font_face(properties) ): - self.designer.draw_text( + self.pipeline.draw_text( line, transform, properties, cap_height, mtext.dxftype() ) def draw_complex_mtext(self, mtext: MText, properties: Properties) -> None: """Draw the content of a MTEXT entity including inline formatting codes.""" - complex_mtext_renderer(self.ctx, self.designer, mtext, properties) + complex_mtext_renderer(self.ctx, self.pipeline, mtext, properties) def draw_curve_entity(self, entity: DXFGraphic, properties: Properties) -> None: try: path = make_path(entity) except AttributeError: # API usage error raise TypeError(f"Unsupported DXF type {entity.dxftype()}") - self.designer.draw_path(path, properties) + self.pipeline.draw_path(path, properties) def draw_point_entity(self, entity: DXFGraphic, properties: Properties) -> None: point = cast(Point, entity) @@ -484,7 +484,7 @@ def draw_point_entity(self, entity: DXFGraphic, properties: Properties) -> None: pdmode = 0 if pdmode == 0: - self.designer.draw_point(entity.dxf.location, properties) + self.pipeline.draw_point(entity.dxf.location, properties) else: for entity in point.virtual_entities(pdsize, pdmode): dxftype = entity.dxftype() @@ -492,9 +492,9 @@ def draw_point_entity(self, entity: DXFGraphic, properties: Properties) -> None: start = entity.dxf.start end = entity.dxf.end if start.isclose(end): - self.designer.draw_point(start, properties) + self.pipeline.draw_point(start, properties) else: # direct draw by backend is OK! - self.designer.draw_line(start, end, properties) + self.pipeline.draw_line(start, end, properties) pass elif dxftype == "CIRCLE": self.draw_curve_entity(entity, properties) @@ -517,16 +517,16 @@ def draw_solid_entity(self, entity: DXFGraphic, properties: Properties) -> None: return edge_visibility = entity.get_edges_visibility() if all(edge_visibility): - self.designer.draw_path(from_vertices(points), properties) + self.pipeline.draw_path(from_vertices(points), properties) else: for a, b, visible in zip(points, points[1:], edge_visibility): if visible: - self.designer.draw_line(a, b, properties) + self.pipeline.draw_line(a, b, properties) elif isinstance(entity, Solid): # set solid fill type for SOLID and TRACE properties.filling = Filling() - self.designer.draw_filled_polygon( + self.pipeline.draw_filled_polygon( entity.wcs_vertices(close=False), properties ) else: @@ -566,7 +566,7 @@ def timeout() -> bool: (e.x, e.y, elevation) ) lines.append((s, e)) - self.designer.draw_solid_lines(lines, properties) + self.pipeline.draw_solid_lines(lines, properties) def draw_hatch_entity( self, @@ -616,10 +616,10 @@ def draw_hatch_entity( paths = closed_loops(boundary_paths, ocs, elevation) if show_only_outline: for p in ignore_text_boxes(paths): - self.designer.draw_path(p, properties) + self.pipeline.draw_path(p, properties) return if paths: - self.designer.draw_filled_paths(ignore_text_boxes(paths), properties) + self.pipeline.draw_filled_paths(ignore_text_boxes(paths), properties) def draw_mpolygon_entity(self, entity: DXFGraphic, properties: Properties): def resolve_fill_color() -> str: @@ -654,14 +654,14 @@ def resolve_fill_color() -> str: properties.color = line_color # draw boundary paths as lines for loop in loops: - self.designer.draw_path(loop, properties) + self.pipeline.draw_path(loop, properties) def draw_wipeout_entity(self, entity: DXFGraphic, properties: Properties) -> None: wipeout = cast(Wipeout, entity) properties.filling = Filling() properties.color = self.ctx.current_layout_properties.background_color clipping_polygon = wipeout.boundary_path_wcs() - self.designer.draw_filled_polygon(clipping_polygon, properties) + self.pipeline.draw_filled_polygon(clipping_polygon, properties) def draw_viewport(self, vp: Viewport) -> None: # the "active" viewport and invisible viewports should be filtered at this @@ -672,7 +672,7 @@ def draw_viewport(self, vp: Viewport) -> None: if not vp.is_top_view: self.log_message("Cannot render non top-view viewports") return - self.designer.draw_viewport(vp, self.ctx, self._bbox_cache) + self.pipeline.draw_viewport(vp, self.ctx, self._bbox_cache) def draw_ole2frame_entity(self, entity: DXFGraphic, properties: Properties) -> None: ole2frame = cast(OLE2Frame, entity) @@ -756,12 +756,12 @@ def draw_image_entity(self, entity: DXFGraphic, properties: Properties) -> None: use_clipping_boundary=image.dxf.flags & Image.USE_CLIPPING_BOUNDARY, remove_outside=image.dxf.clip_mode == 0, ) - self.designer.draw_image(image_data, properties) + self.pipeline.draw_image(image_data, properties) elif show_filename_if_missing: default_cap_height = 20 text = image_def.dxf.filename - font = self.designer.text_engine.get_font( + font = self.pipeline.text_engine.get_font( self.get_font_face(properties) ) text_width = font.text_width_ex(text, default_cap_height) @@ -776,7 +776,7 @@ def draw_image_entity(self, entity: DXFGraphic, properties: Properties) -> None: transform = ( Matrix44.scale(scale) @ translate @ image.get_wcs_transform() ) - self.designer.draw_text( + self.pipeline.draw_text( text, transform, properties, @@ -784,7 +784,7 @@ def draw_image_entity(self, entity: DXFGraphic, properties: Properties) -> None: ) points = [v.vec2 for v in image.boundary_path_wcs()] - self.designer.draw_solid_lines(list(zip(points, points[1:])), properties) + self.pipeline.draw_solid_lines(list(zip(points, points[1:])), properties) elif self.config.image_policy == ImagePolicy.PROXY: self.draw_proxy_graphic(entity.proxy_graphic, entity.doc) @@ -804,7 +804,7 @@ def _draw_filled_rect( props.color = color # default SOLID filling props.filling = Filling() - self.designer.draw_filled_polygon(points, props) + self.pipeline.draw_filled_polygon(points, props) def draw_mesh_entity(self, entity: DXFGraphic, properties: Properties) -> None: builder = MeshBuilder.from_mesh(entity) # type: ignore @@ -814,7 +814,7 @@ def draw_mesh_builder_entity( self, builder: MeshBuilder, properties: Properties ) -> None: for face in builder.faces_as_vertices(): - self.designer.draw_path( + self.pipeline.draw_path( from_vertices(face, close=True), properties=properties ) @@ -855,10 +855,10 @@ def draw_polyline_entity(self, entity: DXFGraphic, properties: Properties) -> No points = polygon # type: ignore # Set default SOLID filling for LWPOLYLINE properties.filling = Filling() - self.designer.draw_filled_polygon(points, properties) + self.pipeline.draw_filled_polygon(points, properties) return - self.designer.draw_path(make_path(entity), properties) + self.pipeline.draw_path(make_path(entity), properties) def draw_composite_entity(self, entity: DXFGraphic, properties: Properties) -> None: def draw_insert(insert: Insert): @@ -878,7 +878,7 @@ def draw_insert(insert: Insert): boundary_path.inner_polygon(), outer_bounds=boundary_path.outer_bounds(), ) - self.designer.push_clipping_shape(clipping_shape, None) + self.pipeline.push_clipping_shape(clipping_shape, None) # draw_entities() includes the visibility check: self.draw_entities( @@ -889,11 +889,11 @@ def draw_insert(insert: Insert): ) if is_clipping_active and clip.get_xclip_frame_policy(): - self.designer.draw_path( + self.pipeline.draw_path( path=from_vertices(boundary_path.inner_polygon(), close=True), properties=properties, ) - self.designer.pop_clipping_shape() + self.pipeline.pop_clipping_shape() if isinstance(entity, Insert): self.ctx.push_state(properties) @@ -949,9 +949,9 @@ def __init__( config: Configuration = Configuration(), bbox_cache: Optional[ezdxf.bbox.Cache] = None, ): - from .designer import Designer2d + from .pipeline import RenderPipeline2d - super().__init__(ctx, Designer2d(out), config, bbox_cache) + super().__init__(ctx, RenderPipeline2d(out), config, bbox_cache) def is_spatial_text(extrusion: Vec3) -> bool: diff --git a/src/ezdxf/addons/drawing/designer.py b/src/ezdxf/addons/drawing/pipeline.py similarity index 72% rename from src/ezdxf/addons/drawing/designer.py rename to src/ezdxf/addons/drawing/pipeline.py index 43e241ad6..f037e148b 100644 --- a/src/ezdxf/addons/drawing/designer.py +++ b/src/ezdxf/addons/drawing/pipeline.py @@ -47,14 +47,20 @@ PatternKey: TypeAlias = Tuple[str, float] DrawEntitiesCallback: TypeAlias = Callable[[RenderContext, Iterable[DXFGraphic]], None] +__all__ = ["AbstractPipeline", "RenderPipeline2d"] -class Designer(abc.ABC): - """The designer separates the frontend from the backend and adds this features: + +class AbstractPipeline(abc.ABC): + """This drawing pipeline separates the frontend from the backend and implements + these features: - automatically linetype rendering + - font rendering - VIEWPORT rendering - foreground color mapping according Frontend.config.color_policy + The pipeline is organized as concatenated render stages. + """ text_engine = UnifiedTextRenderer() @@ -62,26 +68,21 @@ class Designer(abc.ABC): draw_entities: DrawEntitiesCallback @abc.abstractmethod - def set_draw_entities_callback(self, callback: DrawEntitiesCallback) -> None: - ... + def set_draw_entities_callback(self, callback: DrawEntitiesCallback) -> None: ... @abc.abstractmethod - def set_config(self, config: Configuration) -> None: - ... + def set_config(self, config: Configuration) -> None: ... @abc.abstractmethod - def set_current_entity_handle(self, handle: str) -> None: - ... + def set_current_entity_handle(self, handle: str) -> None: ... @abc.abstractmethod def push_clipping_shape( self, shape: ClippingShape, transform: Matrix44 | None - ) -> None: - ... + ) -> None: ... @abc.abstractmethod - def pop_clipping_shape(self) -> None: - ... + def pop_clipping_shape(self) -> None: ... @abc.abstractmethod def draw_viewport( @@ -94,36 +95,30 @@ def draw_viewport( ... @abc.abstractmethod - def draw_point(self, pos: AnyVec, properties: Properties) -> None: - ... + def draw_point(self, pos: AnyVec, properties: Properties) -> None: ... @abc.abstractmethod - def draw_line(self, start: AnyVec, end: AnyVec, properties: Properties): - ... + def draw_line(self, start: AnyVec, end: AnyVec, properties: Properties): ... @abc.abstractmethod def draw_solid_lines( self, lines: Iterable[tuple[AnyVec, AnyVec]], properties: Properties - ) -> None: - ... + ) -> None: ... @abc.abstractmethod - def draw_path(self, path: Path, properties: Properties): - ... + def draw_path(self, path: Path, properties: Properties): ... @abc.abstractmethod def draw_filled_paths( self, paths: Iterable[Path], properties: Properties, - ) -> None: - ... + ) -> None: ... @abc.abstractmethod def draw_filled_polygon( self, points: Iterable[AnyVec], properties: Properties - ) -> None: - ... + ) -> None: ... @abc.abstractmethod def draw_text( @@ -133,20 +128,16 @@ def draw_text( properties: Properties, cap_height: float, dxftype: str = "TEXT", - ) -> None: - ... + ) -> None: ... @abc.abstractmethod - def draw_image(self, image_data: ImageData, properties: Properties) -> None: - ... + def draw_image(self, image_data: ImageData, properties: Properties) -> None: ... @abc.abstractmethod - def finalize(self) -> None: - ... + def finalize(self) -> None: ... @abc.abstractmethod - def set_background(self, color: Color) -> None: - ... + def set_background(self, color: Color) -> None: ... @abc.abstractmethod def enter_entity(self, entity: DXFGraphic, properties: Properties) -> None: @@ -154,12 +145,44 @@ def enter_entity(self, entity: DXFGraphic, properties: Properties) -> None: ... @abc.abstractmethod - def exit_entity(self, entity: DXFGraphic) -> None: - ... + def exit_entity(self, entity: DXFGraphic) -> None: ... + + +class RenderStage2d(abc.ABC): + next_stage: RenderStage2d + + @abc.abstractmethod + def draw_point(self, pos: Vec2, properties: Properties) -> None: ... + + @abc.abstractmethod + def draw_line(self, start: Vec2, end: Vec2, properties: Properties): ... + + @abc.abstractmethod + def draw_solid_lines( + self, lines: list[tuple[Vec2, Vec2]], properties: Properties + ) -> None: ... + + @abc.abstractmethod + def draw_path(self, path: BkPath2d, properties: Properties): ... + + @abc.abstractmethod + def draw_filled_paths( + self, + paths: list[BkPath2d], + properties: Properties, + ) -> None: ... + + @abc.abstractmethod + def draw_filled_polygon( + self, points: BkPoints2d, properties: Properties + ) -> None: ... + + @abc.abstractmethod + def draw_image(self, image_data: ImageData, properties: Properties) -> None: ... -class Designer2d(Designer): - """Designer class for 2D backends.""" +class RenderPipeline2d(AbstractPipeline): + """Render pipeline for 2D backends.""" def __init__(self, backend: BackendInterface): self.backend = backend @@ -174,6 +197,13 @@ def __init__(self, backend: BackendInterface): self.current_vp_scale = 1.0 self._current_entity_handle: str = "" self._color_mapping: dict[str, str] = dict() + self._pipeline = self.build_render_pipeline() + + def build_render_pipeline(self) -> RenderStage2d: + backend_stage = BackendStage2d( + self.backend, converter=self.get_backend_properties + ) + return AllInOneStage2d(pipeline=self, next_stage=backend_stage) @property def vp_ltype_scale(self) -> float: @@ -258,105 +288,6 @@ def exit_viewport(self): ), "This assumption is no longer valid!" self.current_vp_scale = 1.0 - def draw_point(self, pos: AnyVec, properties: Properties) -> None: - point = Vec2(pos) - if self.clipping_portal.is_active: - point = self.clipping_portal.clip_point(point) - if point is None: - return - self.backend.draw_point(point, self.get_backend_properties(properties)) - - def draw_line(self, start: AnyVec, end: AnyVec, properties: Properties): - s = Vec2(start) - e = Vec2(end) - if ( - self.config.line_policy == LinePolicy.SOLID - or len(properties.linetype_pattern) < 2 # CONTINUOUS - ): - bk_properties = self.get_backend_properties(properties) - if self.clipping_portal.is_active: - for segment in self.clipping_portal.clip_line(s, e): - self.backend.draw_line(segment[0], segment[1], bk_properties) - else: - self.backend.draw_line(s, e, bk_properties) - else: - renderer = linetypes.LineTypeRenderer(self.pattern(properties)) - self.draw_solid_lines( # includes transformation - ((s, e) for s, e in renderer.line_segment(s, e)), - properties, - ) - - def draw_solid_lines( - self, lines: Iterable[tuple[AnyVec, AnyVec]], properties: Properties - ) -> None: - lines2d: list[tuple[Vec2, Vec2]] = [(Vec2(s), Vec2(e)) for s, e in lines] - if self.clipping_portal.is_active: - cropped_lines: list[tuple[Vec2, Vec2]] = [] - for start, end in lines2d: - cropped_lines.extend(self.clipping_portal.clip_line(start, end)) - lines2d = cropped_lines - self.backend.draw_solid_lines(lines2d, self.get_backend_properties(properties)) - - def draw_path(self, path: Path, properties: Properties): - self._draw_path(BkPath2d(path), properties) - - def _draw_path(self, path: BkPath2d, properties: Properties): - if ( - self.config.line_policy == LinePolicy.SOLID - or len(properties.linetype_pattern) < 2 # CONTINUOUS - ): - if self.clipping_portal.is_active: - for clipped_path in self.clipping_portal.clip_paths( - [path], self.config.max_flattening_distance - ): - self.backend.draw_path( - clipped_path, self.get_backend_properties(properties) - ) - return - self.backend.draw_path(path, self.get_backend_properties(properties)) - else: - renderer = linetypes.LineTypeRenderer(self.pattern(properties)) - vertices = path.flattening(self.config.max_flattening_distance, segments=16) - - self.draw_solid_lines( - ((Vec2(s), Vec2(e)) for s, e in renderer.line_segments(vertices)), - properties, - ) - - def draw_filled_paths( - self, - paths: Iterable[Path], - properties: Properties, - ) -> None: - self._draw_filled_paths(map(BkPath2d, paths), properties) - - def _draw_filled_paths( - self, - paths: Iterable[BkPath2d], - properties: Properties, - ) -> None: - if self.clipping_portal.is_active: - max_sagitta = self.config.max_flattening_distance - paths = self.clipping_portal.clip_filled_paths(paths, max_sagitta) - _paths = list(paths) - if len(_paths) == 0: - return - self.backend.draw_filled_paths(_paths, self.get_backend_properties(properties)) - - def draw_filled_polygon( - self, points: Iterable[AnyVec], properties: Properties - ) -> None: - self._draw_filled_polygon(BkPoints2d(points), properties) - - def _draw_filled_polygon(self, points: BkPoints2d, properties: Properties) -> None: - bk_properties = self.get_backend_properties(properties) - if self.clipping_portal.is_active: - for points in self.clipping_portal.clip_polygon(points): - if len(points) > 0: - self.backend.draw_filled_polygon(points, bk_properties) - elif len(points) > 0: - self.backend.draw_filled_polygon(points, bk_properties) - def pattern(self, properties: Properties) -> Sequence[float]: """Get pattern - implements pattern caching.""" if self.config.line_policy == LinePolicy.SOLID: @@ -393,7 +324,10 @@ def draw_text( cap_height: float, dxftype: str = "TEXT", ) -> None: + """Render text as filled paths.""" text_policy = self.config.text_policy + pipeline = self._pipeline + if not text.strip() or text_policy == TextPolicy.IGNORE: return # no point rendering empty strings text = prepare_string_for_rendering(text, dxftype) @@ -419,7 +353,7 @@ def draw_text( if len(points) < 2: return rect = BkPath2d.from_vertices(BoundingBox2d(points).rect_vertices()) - self._draw_path(rect, properties) + pipeline.draw_path(rect, properties) return if text_policy == TextPolicy.REPLACE_FILL: points = [] @@ -430,7 +364,7 @@ def draw_text( polygon = BkPoints2d(BoundingBox2d(points).rect_vertices()) if properties.filling is None: properties.filling = Filling() - self._draw_filled_polygon(polygon, properties) + pipeline.draw_filled_polygon(polygon, properties) return if ( @@ -438,24 +372,166 @@ def draw_text( or text_policy == TextPolicy.OUTLINE ): for text_path in transformed_paths: - self._draw_path(text_path, properties) + pipeline.draw_path(text_path, properties) return if properties.filling is None: properties.filling = Filling() - self._draw_filled_paths(transformed_paths, properties) + pipeline.draw_filled_paths(transformed_paths, properties) + + def finalize(self) -> None: + self.backend.finalize() + + def set_background(self, color: Color) -> None: + self.backend.set_background(color) + + def enter_entity(self, entity: DXFGraphic, properties: Properties) -> None: + self.backend.enter_entity(entity, properties) + + def exit_entity(self, entity: DXFGraphic) -> None: + self.backend.exit_entity(entity) + + # Enter render pipeline: + def draw_point(self, pos: AnyVec, properties: Properties) -> None: + self._pipeline.draw_point(Vec2(pos), properties) + + def draw_line(self, start: AnyVec, end: AnyVec, properties: Properties): + self._pipeline.draw_line(Vec2(start), Vec2(end), properties) + + def draw_solid_lines( + self, lines: Iterable[tuple[AnyVec, AnyVec]], properties: Properties + ) -> None: + self._pipeline.draw_solid_lines( + [(Vec2(s), Vec2(e)) for s, e in lines], properties + ) + + def draw_path(self, path: Path, properties: Properties): + self._pipeline.draw_path(BkPath2d(path), properties) + + def draw_filled_paths( + self, + paths: Iterable[Path], + properties: Properties, + ) -> None: + self._pipeline.draw_filled_paths(list(map(BkPath2d, paths)), properties) + + def draw_filled_polygon( + self, points: Iterable[AnyVec], properties: Properties + ) -> None: + self._pipeline.draw_filled_polygon(BkPoints2d(points), properties) + + def draw_image(self, image_data: ImageData, properties: Properties) -> None: + self._pipeline.draw_image(image_data, properties) + + +# First step to separated render stages: extract rendering into a single render stage +class AllInOneStage2d(RenderStage2d): + def __init__(self, pipeline: RenderPipeline2d, next_stage: RenderStage2d): + self.next_stage = next_stage + self.pipeline = pipeline + + def draw_point(self, pos: Vec2, properties: Properties) -> None: + if self.pipeline.clipping_portal.is_active: + pos = self.pipeline.clipping_portal.clip_point(pos) + if pos is None: + return + self.next_stage.draw_point(pos, properties) + + def draw_line(self, start: Vec2, end: Vec2, properties: Properties): + s = Vec2(start) + e = Vec2(end) + pipeline = self.pipeline + next_stage = self.next_stage + + if ( + pipeline.config.line_policy == LinePolicy.SOLID + or len(properties.linetype_pattern) < 2 # CONTINUOUS + ): + if pipeline.clipping_portal.is_active: + for segment in pipeline.clipping_portal.clip_line(s, e): + next_stage.draw_line(segment[0], segment[1], properties) + else: + next_stage.draw_line(s, e, properties) + else: + renderer = linetypes.LineTypeRenderer(pipeline.pattern(properties)) + self.draw_solid_lines( + [(s, e) for s, e in renderer.line_segment(s, e)], + properties, + ) + + def draw_solid_lines( + self, lines: list[tuple[Vec2, Vec2]], properties: Properties + ) -> None: + clipping_portal = self.pipeline.clipping_portal + if clipping_portal.is_active: + cropped_lines: list[tuple[Vec2, Vec2]] = [] + for start, end in lines: + cropped_lines.extend(clipping_portal.clip_line(start, end)) + lines = cropped_lines + self.next_stage.draw_solid_lines(lines, properties) + + def draw_path(self, path: BkPath2d, properties: Properties): + pipeline = self.pipeline + clipping_portal = pipeline.clipping_portal + next_stage = self.next_stage + max_flattening_distance = pipeline.config.max_flattening_distance + + if ( + pipeline.config.line_policy == LinePolicy.SOLID + or len(properties.linetype_pattern) < 2 # CONTINUOUS + ): + if clipping_portal.is_active: + for clipped_path in clipping_portal.clip_paths( + [path], max_flattening_distance + ): + next_stage.draw_path(clipped_path, properties) + return + next_stage.draw_path(path, properties) + else: + renderer = linetypes.LineTypeRenderer(pipeline.pattern(properties)) + vertices = path.flattening(max_flattening_distance, segments=16) + self.draw_solid_lines( + [(Vec2(s), Vec2(e)) for s, e in renderer.line_segments(vertices)], + properties, + ) + + def draw_filled_paths( + self, + paths: list[BkPath2d], + properties: Properties, + ) -> None: + pipeline = self.pipeline + clipping_portal = pipeline.clipping_portal + if clipping_portal.is_active: + max_sagitta = pipeline.config.max_flattening_distance + paths = clipping_portal.clip_filled_paths(paths, max_sagitta) + _paths = list(paths) + if len(_paths) == 0: + return + self.next_stage.draw_filled_paths(_paths, properties) + + def draw_filled_polygon(self, points: BkPoints2d, properties: Properties) -> None: + clipping_portal = self.pipeline.clipping_portal + next_stage = self.next_stage + if clipping_portal.is_active: + for points in clipping_portal.clip_polygon(points): + if len(points) > 0: + next_stage.draw_filled_polygon(points, properties) + elif len(points) > 0: + next_stage.draw_filled_polygon(points, properties) def draw_image(self, image_data: ImageData, properties: Properties) -> None: # the outer bounds contain the visible parts of the image for the # clip mode "remove inside" outer_bounds: list[BkPoints2d] = [] + clipping_portal = self.pipeline.clipping_portal - if self.clipping_portal.is_active: + if clipping_portal.is_active: # the pixel boundary path can be split into multiple paths transform = image_data.flip_matrix() * image_data.transform pixel_boundary_path = image_data.pixel_boundary_path clipping_paths = _clip_image_polygon( - self.clipping_portal, pixel_boundary_path, transform + clipping_portal, pixel_boundary_path, transform ) if not image_data.remove_outside: # remove inside: @@ -466,9 +542,9 @@ def draw_image(self, image_data: ImageData, properties: Properties) -> None: Vec2.generate([(0, 0), (width, 0), (width, height), (0, height)]) ) outer_bounds = _clip_image_polygon( - self.clipping_portal, outer_boundary, transform + clipping_portal, outer_boundary, transform ) - image_data.transform = self.clipping_portal.transform_matrix( + image_data.transform = clipping_portal.transform_matrix( image_data.transform ) if len(clipping_paths) == 1: @@ -504,19 +580,54 @@ def _draw_image( ) -> None: if image_data.use_clipping_boundary: _mask_image(image_data, outer_bounds) - self.backend.draw_image(image_data, self.get_backend_properties(properties)) + self.next_stage.draw_image(image_data, properties) - def finalize(self) -> None: - self.backend.finalize() - def set_background(self, color: Color) -> None: - self.backend.set_background(color) +class ClippingStage2d(RenderStage2d): + pass - def enter_entity(self, entity: DXFGraphic, properties: Properties) -> None: - self.backend.enter_entity(entity, properties) - def exit_entity(self, entity: DXFGraphic) -> None: - self.backend.exit_entity(entity) +class LinetypeStage2d(RenderStage2d): + pass + + +class BackendStage2d(RenderStage2d): + """Send data to the output backend.""" + + def __init__( + self, + backend: BackendInterface, + converter: Callable[[Properties], BackendProperties], + ): + self.backend = backend + self.converter = converter + + def draw_point(self, pos: Vec2, properties: Properties) -> None: + self.backend.draw_point(pos, self.converter(properties)) + + def draw_line(self, start: Vec2, end: Vec2, properties: Properties): + self.backend.draw_line(start, end, self.converter(properties)) + + def draw_solid_lines( + self, lines: list[tuple[Vec2, Vec2]], properties: Properties + ) -> None: + self.backend.draw_solid_lines(lines, self.converter(properties)) + + def draw_path(self, path: BkPath2d, properties: Properties): + self.backend.draw_path(path, self.converter(properties)) + + def draw_filled_paths( + self, + paths: list[BkPath2d], + properties: Properties, + ) -> None: + self.backend.draw_filled_paths(paths, self.converter(properties)) + + def draw_filled_polygon(self, points: BkPoints2d, properties: Properties) -> None: + self.backend.draw_filled_polygon(points, self.converter(properties)) + + def draw_image(self, image_data: ImageData, properties: Properties) -> None: + self.backend.draw_image(image_data, self.converter(properties)) def _mask_image(image_data: ImageData, outer_bounds: list[BkPoints2d]) -> None: