From 31cdf5bf5ec31c39e13d8d0682ff3e9a139ebd0a Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:03:30 +0200 Subject: [PATCH] Draft: Improve ShapeString execute function --- src/Mod/Draft/draftobjects/shapestring.py | 170 +++++++++++----------- 1 file changed, 86 insertions(+), 84 deletions(-) diff --git a/src/Mod/Draft/draftobjects/shapestring.py b/src/Mod/Draft/draftobjects/shapestring.py index 92af708608..402c7cc636 100644 --- a/src/Mod/Draft/draftobjects/shapestring.py +++ b/src/Mod/Draft/draftobjects/shapestring.py @@ -123,100 +123,102 @@ class ShapeString(DraftObject): + translate("draft", "changed 'Tracking' property type")) def execute(self, obj): - if self.props_changed_placement_only(): + if self.props_changed_placement_only() \ + or not obj.String \ + or not obj.FontFile: obj.positionBySupport() self.props_changed_clear() return - if obj.String and obj.FontFile: - plm = obj.Placement + if obj.FontFile[0] == ".": + # FontFile path relative to the FreeCAD file directory. + font_file = os.path.join(os.path.dirname(obj.Document.FileName), obj.FontFile) + # We need the absolute path to do some file checks. + font_file = os.path.abspath(font_file) + else: + font_file = obj.FontFile - if obj.FontFile[0] == ".": - # FontFile path relative to the FreeCAD file directory. - font_file = os.path.join(os.path.dirname(obj.Document.FileName), obj.FontFile) - # We need the absolute path to do some file checks. - font_file = os.path.abspath(font_file) + # File checks: + if not os.path.exists(font_file): + _err(obj.Label + ": " + translate("draft", "Font file not found")) + self.props_changed_clear() + return + if not os.path.isfile(font_file): + _err(obj.Label + ": " + translate("draft", "Specified font file is not a file")) + self.props_changed_clear() + return + if not os.path.splitext(font_file)[1].lower() in (".ttc", ".ttf", ".otf", ".pfb"): + _err(obj.Label + ": " + translate("draft", "Specified font type is not supported")) + self.props_changed_clear() + return + + plm = obj.Placement + fill = obj.MakeFace + if fill is True: + # Test a simple letter to know if we have a sticky font or not. + # If the font is sticky change fill to `False`. + # The 0.03 total area minimum is based on tests with: + # 1CamBam_Stick_0.ttf and 1CamBam_Stick_0C.ttf. + # See the make_faces function for more information. + char = Part.makeWireString("L", font_file, 1, 0)[0] + shapes = self.make_faces(char) # char is list of wires + if not shapes: + fill = False else: - font_file = obj.FontFile + # Depending on the font the size of char can be very small. + # For the area check to make sense we need to use a scale factor. + # https://github.com/FreeCAD/FreeCAD/issues/21501 + char_comp = Part.Compound(char) + factor = 1 / char_comp.BoundBox.YLength + fill = sum([shape.Area for shape in shapes]) > (0.03 / factor ** 2) \ + and math.isclose(char_comp.BoundBox.DiagonalLength, + Part.Compound(shapes).BoundBox.DiagonalLength, + rel_tol=1e-7) - # File checks: - if not os.path.exists(font_file): - _err(obj.Label + ": " + translate("draft", "Font file not found")) - return - if not os.path.isfile(font_file): - _err(obj.Label + ": " + translate("draft", "Specified font file is not a file")) - return - if not os.path.splitext(font_file)[1].lower() in (".ttc", ".ttf", ".otf", ".pfb"): - _err(obj.Label + ": " + translate("draft", "Specified font type is not supported")) - return + chars = Part.makeWireString(obj.String, font_file, obj.Size, obj.Tracking) + shapes = [] - fill = obj.MakeFace - if fill is True: - # Test a simple letter to know if we have a sticky font or not. - # If the font is sticky change fill to `False`. - # The 0.03 total area minimum is based on tests with: - # 1CamBam_Stick_0.ttf and 1CamBam_Stick_0C.ttf. - # See the make_faces function for more information. - char = Part.makeWireString("L", font_file, 1, 0)[0] - shapes = self.make_faces(char) # char is list of wires - if not shapes: - fill = False - else: - # Depending on the font the size of char can be very small. - # For the area check to make sense we need to use a scale factor. - # https://github.com/FreeCAD/FreeCAD/issues/21501 - char_comp = Part.Compound(char) - factor = 1 / char_comp.BoundBox.YLength - fill = sum([shape.Area for shape in shapes]) > (0.03 / factor ** 2) \ - and math.isclose(char_comp.BoundBox.DiagonalLength, - Part.Compound(shapes).BoundBox.DiagonalLength, - rel_tol=1e-7) - - chars = Part.makeWireString(obj.String, font_file, obj.Size, obj.Tracking) - shapes = [] - - for char in chars: - if fill is False: - shapes.extend(char) - elif char: - shapes.extend(self.make_faces(char)) - if shapes: - if fill and obj.Fuse: - ss_shape = shapes[0].fuse(shapes[1:]) - ss_shape = faces.concatenate(ss_shape) - # Concatenate returns a Face or a Compound. We always - # need a Compound as we use ss_shape.SubShapes later. - if ss_shape.ShapeType == "Face": - ss_shape = Part.Compound([ss_shape]) - else: - ss_shape = Part.Compound(shapes) - cap_char = Part.makeWireString("M", font_file, obj.Size, obj.Tracking)[0] - cap_height = Part.Compound(cap_char).BoundBox.YMax - if obj.ScaleToSize: - ss_shape.scale(obj.Size / cap_height) - cap_height = obj.Size - if obj.ObliqueAngle: - if -80 <= obj.ObliqueAngle <= 80: - mtx = App.Matrix() - mtx.A12 = math.tan(math.radians(obj.ObliqueAngle)) - ss_shape = ss_shape.transformGeometry(mtx) - else: - wrn = translate("draft", "ShapeString: oblique angle must be in the -80 to +80 degree range") + "\n" - App.Console.PrintWarning(wrn) - just_vec = self.justification_vector(ss_shape, - cap_height, - obj.Justification, - obj.JustificationReference, - obj.KeepLeftMargin) - shapes = ss_shape.SubShapes - for shape in shapes: - shape.translate(just_vec) - obj.Shape = Part.Compound(shapes) + for char in chars: + if fill is False: + shapes.extend(char) + elif char: + shapes.extend(self.make_faces(char)) + if shapes: + if fill and obj.Fuse: + ss_shape = shapes[0].fuse(shapes[1:]) + ss_shape = faces.concatenate(ss_shape) + # Concatenate returns a Face or a Compound. We always + # need a Compound as we use ss_shape.SubShapes later. + if ss_shape.ShapeType == "Face": + ss_shape = Part.Compound([ss_shape]) else: - App.Console.PrintWarning(translate("draft", "ShapeString: string has no wires") + "\n") - - obj.Placement = plm + ss_shape = Part.Compound(shapes) + cap_char = Part.makeWireString("M", font_file, obj.Size, obj.Tracking)[0] + cap_height = Part.Compound(cap_char).BoundBox.YMax + if obj.ScaleToSize: + ss_shape.scale(obj.Size / cap_height) + cap_height = obj.Size + if obj.ObliqueAngle: + if -80 <= obj.ObliqueAngle <= 80: + mtx = App.Matrix() + mtx.A12 = math.tan(math.radians(obj.ObliqueAngle)) + ss_shape = ss_shape.transformGeometry(mtx) + else: + wrn = translate("draft", "ShapeString: oblique angle must be in the -80 to +80 degree range") + "\n" + App.Console.PrintWarning(wrn) + just_vec = self.justification_vector(ss_shape, + cap_height, + obj.Justification, + obj.JustificationReference, + obj.KeepLeftMargin) + shapes = ss_shape.SubShapes + for shape in shapes: + shape.translate(just_vec) + obj.Shape = Part.Compound(shapes) + else: + App.Console.PrintWarning(translate("draft", "ShapeString: string has no wires") + "\n") + obj.Placement = plm obj.positionBySupport() self.props_changed_clear()