def _get_extra_transform(text: AnyText) -> Matrix44: extra_transform = Matrix44() if isinstance(text, Text): # ALIGNED: scaled to fit in the text box (aspect ratio preserved). Does not have to be handled specially. # FIT: scaled to fit in the text box (aspect ratio *not* preserved). Handled by dxf.width scale_x = text.dxf.width # 'width' is the width *scale factor* so 1.0 by default scale_y = 1 if text.dxf.text_generation_flag & DXFConstants.MIRROR_X: scale_x *= -1 if text.dxf.text_generation_flag & DXFConstants.MIRROR_Y: scale_y *= -1 # magnitude of extrusion does not have any effect. An extrusion of (0, 0, 0) acts like (0, 0, 1) scale_x *= sign(text.dxf.extrusion.z) if scale_x != 1 or scale_y != 1: extra_transform = Matrix44.scale(scale_x, scale_y) elif isinstance(text, MText): # not sure about the rationale behind this but it does match AutoCAD behavior... scale_y = sign(text.dxf.extrusion.z) if scale_y != 1: extra_transform = Matrix44.scale(1, scale_y) return extra_transform
def text_transformation_matrix(entity: Text) -> Matrix44: """ Apply rotation, width factor, translation to the insertion point and if necessary transformation from OCS to WCS. """ angle = math.radians(entity.dxf.rotation) width_factor = entity.dxf.width align, p1, p2 = entity.get_pos() mirror_x = -1 if entity.is_backward else 1 mirror_y = -1 if entity.is_upside_down else 1 oblique = math.radians(entity.dxf.oblique) location = p1 if align in ('ALIGNED', 'FIT'): width_factor = 1.0 # text goes from p1 to p2, no stretching applied location = p1.lerp(p2, factor=0.5) angle = (p2 - p1).angle # override stored angle m = Matrix44() if oblique: m *= Matrix44.shear_xy(angle_x=oblique) sx = width_factor * mirror_x sy = mirror_y if sx != 1 or sy != 1: m *= Matrix44.scale(sx, sy, 1) if angle: m *= Matrix44.z_rotate(angle) if location: m *= Matrix44.translate(location.x, location.y, location.z) ocs = entity.ocs() if ocs.transform: # to WCS m *= ocs.matrix return m
def test_apply_transformation_multiple_times(sx, sy, sz, doc1: 'Drawing'): def insert(): return Insert.new(dxfattribs={ 'name': 'AXIS', 'insert': (0, 0, 0), 'xscale': 1, 'yscale': 1, 'zscale': 1, 'rotation': 0, }, doc=doc1), [(0, 0, 0), X_AXIS, Y_AXIS, Z_AXIS] entity, vertices = insert() m = Matrix44.chain( Matrix44.scale(sx, sy, sz), Matrix44.z_rotate(math.radians(10)), Matrix44.translate(1, 1, 1), ) for i in range(5): entity, vertices = synced_transformation(entity, vertices, m) points = list(vertices) for num, line in enumerate(entity.virtual_entities()): assert points[0].isclose(line.dxf.start, abs_tol=1e-9) assert points[num + 1].isclose(line.dxf.end, abs_tol=1e-9)
def draw_text( self, text: str, transform: Matrix44, properties: Properties, cap_height: float, ): if not text.strip(): return # no point rendering empty strings font_properties = self.get_font_properties(properties.font) assert self.current_entity is not None text = prepare_string_for_rendering(text, self.current_entity.dxftype()) transformed_path = _transform_path( self._text_renderer.get_text_path(text, font_properties), Matrix44.scale( self._text_renderer.get_scale(cap_height, font_properties)) @ transform, ) self.ax.add_patch( PathPatch( transformed_path, facecolor=properties.color, linewidth=0, zorder=self._get_z(), ))
def test_wcs_mirror_transformations_of_clockwise_oriented_curves(sx, sy, kind): hatch = Hatch() edge_path = hatch.paths.add_edge_path() # A closed loop is required to get a path! edge_path.add_line((15, 5), (5, 5)) if kind == "arc": edge_path.add_arc((10, 5), 5, start_angle=0, end_angle=180, ccw=False) elif kind == "ellipse": edge_path.add_ellipse( (10, 5), (5, 0), ratio=0.7, start_angle=0, end_angle=180, ccw=False ) else: pytest.fail(f"unknown kind: {kind}") src_path = make_path(hatch) assert len(src_path) > 1, "expected non empty path" m = Matrix44.scale(sx, sy, 1) transformed_hatch = transformed_copy(hatch, m) expected_path = src_path.transform(m) path_of_transformed_hatch = make_path(transformed_hatch) assert ( have_close_control_vertices(path_of_transformed_hatch, expected_path) is True )
def test_apply_transformation_multiple_times(sx, sy, sz, doc1: "Drawing"): def insert(): return ( Insert.new( dxfattribs={ "name": "AXIS", "insert": (0, 0, 0), "xscale": 1, "yscale": 1, "zscale": 1, "rotation": 0, }, doc=doc1, ), [(0, 0, 0), X_AXIS, Y_AXIS, Z_AXIS], ) entity, vertices = insert() m = Matrix44.chain( Matrix44.scale(sx, sy, sz), Matrix44.z_rotate(math.radians(10)), Matrix44.translate(1, 1, 1), ) for i in range(5): entity, vertices = synced_transformation(entity, vertices, m) points = list(vertices) for num, line in enumerate(entity.virtual_entities()): assert points[0].isclose(line.dxf.start, abs_tol=1e-6) assert points[num + 1].isclose(line.dxf.end, abs_tol=1e-6)
def alignment_transformation( fm: fonts.FontMeasurements, bbox: BoundingBox, align: TextEntityAlignment, length: float, ) -> Matrix44: """Returns the alignment transformation matrix to transform a basic text path at location (0, 0) and alignment :attr:`LEFT` into the final text path of the given alignment. For the alignments :attr:`FIT` and :attr:`ALIGNED` defines the argument `length` the total length of the final text path. The given bounding box defines the rendering borders of the basic text path. """ halign, valign = MAP_TEXT_ENUM_TO_ALIGN_FLAGS[align] matrix = basic_alignment_transformation(fm, bbox, halign, valign) stretch_x = 1.0 stretch_y = 1.0 if align == TextEntityAlignment.ALIGNED: stretch_x = length / bbox.size.x stretch_y = stretch_x elif align == TextEntityAlignment.FIT: stretch_x = length / bbox.size.x if stretch_x != 1.0: matrix *= Matrix44.scale(stretch_x, stretch_y, 1.0) return matrix
def __init__(self, backend: Backend, factor: float): self._backend = backend self._factor = float(factor) if self._factor < 1e-9: raise ValueError("scaling factor too small or negative") self._scaling_matrix = Matrix44.scale( self._factor, self._factor, self._factor )
def get_text_line_width(self, text: str, cap_height: float) -> float: if not text: return 0 assert '\n' not in text, 'not a single line of text' path = _text_path(text, self.font) scale = cap_height / self._font_measurements.cap_height transformed_xs = _transform_path(path, Matrix44.scale(scale)).vertices[:, 0].tolist() return max(transformed_xs)
def draw_text(self, text: str, transform: Matrix44, properties: Properties, cap_height: float): if not text: return # no point rendering empty strings assert '\n' not in text, 'not a single line of text' scale = cap_height / self._font_measurements.cap_height path = _text_path(text, self.font) transformed_path = _transform_path(path, Matrix44.scale(scale) @ transform) self.ax.add_patch(PathPatch(transformed_path, facecolor=properties.color, linewidth=0, zorder=self._get_z()))
def scale(self, sx: float, sy: float, sz: float) -> 'DXFGraphic': """ Scale entity inplace about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ return self.transform(Matrix44.scale(sx, sy, sz))
def scale_uniform(self, s: float) -> 'DXFGraphic': """ Scale entity inplace uniform about `s` in x-axis, y-axis and z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ return self.transform(Matrix44.scale(s))
def test_insert_transformation_error(): insert = Insert.new(dxfattribs={ 'name': 'AXIS', 'insert': (0, 0, 0), 'rotation': 45, }) m = Matrix44.scale(0.5, 1, 1) with pytest.raises(InsertTransformationError): insert.transform(m)
def test_transform_mtext_extrusion(self): """The extrusion vector is always created by the right-hand-rule from the transformed x- and y-axis: Z = X "cross" Y. """ mtext = MTextData() m = Matrix44.scale(-1, 1, 1) mtext.transform(WCSTransform(m)) assert mtext.text_direction.isclose(m.transform(X_AXIS)) assert mtext.extrusion.isclose( -m.transform(Z_AXIS)), "expected reversed z-axis"
def test_draw_scaled_path(self, backend): p1 = Path((1, 2)) p1.line_to((2, 3)) backend.draw_path(p1, Properties()) f = backend.factor expected_p2 = p1.transform(Matrix44.scale(f, f, f)) p2 = backend.collector[0][1] for v1, v2 in zip(p2.control_vertices(), expected_p2.control_vertices()): assert v1.isclose(v2)
def test_circle_default_ocs(): circle = Circle.new(dxfattribs={'center': (2, 3, 4), 'thickness': 2}) # 1. rotation - 2. scaling - 3. translation m = Matrix44.chain(Matrix44.scale(2, 2, 3), Matrix44.translate(1, 1, 1)) # default extrusion is (0, 0, 1), therefore scale(2, 2, ..) is a uniform scaling in the xy-play of the OCS circle.transform(m) assert circle.dxf.center == (5, 7, 13) assert circle.dxf.extrusion == (0, 0, 1) assert circle.dxf.thickness == 6
def test_xline_transform(): # same implementation for Ray() xline = XLine.new(dxfattribs={ "start": (2, 3, 4), "unit_vector": (1, 0, 0) }) # 1. scaling - 2. rotation - 3. translation m = Matrix44.chain(Matrix44.scale(2, 2, 3), Matrix44.translate(1, 1, 1)) xline.transform(m) assert xline.dxf.start == (5, 7, 13) assert xline.dxf.unit_vector == (1, 0, 0)
def main_multi_ellipse(layout): m = Matrix44.chain( Matrix44.scale(1.1, 1.3, 1), Matrix44.z_rotate(math.radians(10)), Matrix44.translate(1, 1, 0), ) entity, vertices, axis_vertices = ellipse(start=math.pi / 2, end=-math.pi / 2) for index in range(5): entity, vertices = synced_transformation(entity, vertices, m) add(layout, entity, vertices)
def test_circle_non_uniform_scaling(): circle = Circle.new(dxfattribs={'center': (2, 3, 4), 'extrusion': (0, 1, 0), 'thickness': 2}) # extrusion in WCS y-axis, therefore scale(2, ..., 3) is a non uniform # scaling in the xy-play of the OCS which is the xz-plane of the WCS with pytest.raises(NonUniformScalingError): circle.transform(Matrix44.scale(2, 2, 3)) # source values unchanged after exception assert circle.dxf.center == (2, 3, 4) assert circle.dxf.extrusion == (0, 1, 0) assert circle.dxf.thickness == 2
def test_scale_and_mirror_y(self, tags): data = Tags(transform_xdata_tags(tags, Matrix44.scale(2, -2, 2))) # 1011 - move, scale, rotate and mirror assert Vec3((22, -42, 62)).isclose(data[2].value) # 1012 - scale, rotate and mirror assert Vec3((24, -44, 64)).isclose(data[3].value) # 1013 - rotate and mirror assert Vec3((13, -23, 33)).isclose(data[4].value) # 1041 - scale distance assert math.isclose(4, data[5].value) # 1042 - scale factor assert math.isclose(4, data[6].value)
def _get_extra_transform(text: AnyText, line_width: float) -> Matrix44: extra_transform = Matrix44() if isinstance(text, Text): # Attrib and AttDef are sub-classes of Text # 'width' is the width *scale factor* so 1.0 by default: scale_x = text.dxf.width scale_y = 1.0 # Calculate text stretching for FIT and ALIGNED: alignment = text.get_align_enum() line_width = abs(line_width) if ( alignment in (TextEntityAlignment.FIT, TextEntityAlignment.ALIGNED) and line_width > 1e-9 ): defined_length = (text.dxf.align_point - text.dxf.insert).magnitude stretch_factor = defined_length / line_width scale_x = stretch_factor if alignment == TextEntityAlignment.ALIGNED: scale_y = stretch_factor if text.dxf.text_generation_flag & DXFConstants.MIRROR_X: scale_x *= -1.0 if text.dxf.text_generation_flag & DXFConstants.MIRROR_Y: scale_y *= -1.0 # Magnitude of extrusion does not have any effect. # An extrusion of (0, 0, 0) acts like (0, 0, 1) scale_x *= sign(text.dxf.extrusion.z) if scale_x != 1.0 or scale_y != 1.0: extra_transform = Matrix44.scale(scale_x, scale_y) elif isinstance(text, MText): # Not sure about the rationale behind this but it does match AutoCAD # behavior... scale_y = sign(text.dxf.extrusion.z) if scale_y != 1.0: extra_transform = Matrix44.scale(1.0, scale_y) return extra_transform
def draw_text(self, text: str, transform: Matrix44, properties: Properties, cap_height: float) -> None: if not text: return # no point rendering empty strings text = prepare_string_for_rendering(text, self.current_entity.dxftype()) scale = cap_height / self._font_measurements.cap_height transform = Matrix44.scale(scale, -scale, 0) @ transform path = qg.QPainterPath() path.addText(0, 0, self._font, text) path = _matrix_to_qtransform(transform).map(path) item = self.scene.addPath(path, self._no_line, self._get_color(properties.color)) self._set_item_data(item)
def draw_text(self, text: str, transform: Matrix44, properties: Properties, cap_height: float) -> None: if not text: return # no point rendering empty strings assert '\n' not in text, 'not a single line of text' scale = cap_height / self._font_measurements.cap_height transform = Matrix44.scale(scale, -scale, 0) @ transform path = qg.QPainterPath() path.addText(0, 0, self._font, text) path = _matrix_to_qtransform(transform).map(path) item = self.scene.addPath(path, self._no_line, self._get_color(properties.color)) self._set_item_data(item)
def test_mirror_x(self, tags): data = Tags(transform_xdata_tags(tags, Matrix44.scale(-1, 1, 1))) # 1010 - fixed 3D point -> no transformation assert Vec3((10, 20, 30)).isclose(data[1].value) # 1011 - move, scale, rotate and mirror assert Vec3((-11, 21, 31)).isclose(data[2].value) # 1012 - scale, rotate and mirror assert Vec3((-12, 22, 32)).isclose(data[3].value) # 1013 - rotate and mirror assert Vec3((-13, 23, 33)).isclose(data[4].value) # 1041 - scale distance - BricsCAD transforms to -2 ??? assert math.isclose(2, data[5].value) # 1042 - scale factor - BricsCAD transforms to -2 ??? assert math.isclose(2, data[6].value)
def test_scale(self, tags): data = Tags(transform_xdata_tags(tags, Matrix44.scale(2, 2, 2))) # 1010 - fixed 3D point -> no transformation assert Vec3((10, 20, 30)).isclose(data[1].value) # 1011 - move, scale, rotate and mirror assert Vec3((22, 42, 62)).isclose(data[2].value) # 1012 - scale, rotate and mirror assert Vec3((24, 44, 64)).isclose(data[3].value) # 1013 - rotate and mirror assert Vec3((13, 23, 33)).isclose(data[4].value) # 1041 - scale distance assert math.isclose(4, data[5].value) # 1042 - scale factor assert math.isclose(4, data[6].value)
def test_scale_and_reflexion(rx, ry, text2): insert = Vec3(0, 0, 0) m = Matrix44.chain( Matrix44.scale(2 * rx, 3 * ry, 1), Matrix44.z_rotate(math.radians(45)), Matrix44.translate(3 * rx, 3 * ry, 0), ) text2.transform(m) check_point = m.transform(insert) ocs = text2.ocs() assert ocs.to_wcs(text2.dxf.insert).isclose(check_point) assert math.isclose(text2.dxf.height, 3.0) assert math.isclose(text2.dxf.width, 2.0 / 3.0)
def synced_scaling(entity, chk, axis_vertices=None, sx: float = 1, sy: float = 1, sz: float = 1): entity = entity.copy() entity.scale(sx, sy, sz) m = Matrix44.scale(sx, sy, sz) chk = list(m.transform_vertices(chk)) if axis_vertices: axis_vertices = list(m.transform_vertices(axis_vertices)) return entity, chk, axis_vertices return entity, chk
def test_scaling(): line = Line.new( dxfattribs={ "start": (0, 0, 0), "end": (1, 0, 0), "extrusion": (0, 1, 0), "thickness": 2, }) m = Matrix44.scale(2, 2, 0) line.transform(m) assert line.dxf.start == (0, 0, 0) assert line.dxf.end == (2, 0, 0) assert line.dxf.extrusion == (0, 1, 0) assert line.dxf.thickness == 4
def test_scaling(): line = Line.new( dxfattribs={ 'start': (0, 0, 0), 'end': (1, 0, 0), 'extrusion': (0, 1, 0), 'thickness': 2 }) m = Matrix44.scale(2, 2, 0) line.transform(m) assert line.dxf.start == (0, 0, 0) assert line.dxf.end == (2, 0, 0) assert line.dxf.extrusion == (0, 1, 0) assert line.dxf.thickness == 4
def draw_text(self, text: str, transform: Matrix44, properties: Properties, cap_height: float) -> None: if not text.strip(): return # no point rendering empty strings text = prepare_string_for_rendering(text, self.current_entity.dxftype()) qfont = self.get_qfont(properties.font) scale = self._text_renderer.get_scale(cap_height, qfont) transform = Matrix44.scale(scale, -scale, 0) @ transform path = self._text_renderer.get_text_path(text, qfont) path = _matrix_to_qtransform(transform).map(path) item = self._scene.addPath(path, self._no_line, self._get_color(properties.color)) self._set_item_data(item)