From fff0ca7217e2356f8a15086c6ca67f6b7387b4d5 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:08:49 +0200 Subject: [PATCH] Draft: Add Justification option to ShapeString (#10233) --- src/Mod/Draft/draftobjects/shapestring.py | 212 ++++++++++++++-------- 1 file changed, 132 insertions(+), 80 deletions(-) diff --git a/src/Mod/Draft/draftobjects/shapestring.py b/src/Mod/Draft/draftobjects/shapestring.py index be71191c71..1370a31f08 100644 --- a/src/Mod/Draft/draftobjects/shapestring.py +++ b/src/Mod/Draft/draftobjects/shapestring.py @@ -30,9 +30,12 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App -import draftutils.utils as utils +import Part +from draftgeoutils import faces +from draftutils.messages import _wrn from draftutils.translate import translate + from draftobjects.base import DraftObject @@ -40,22 +43,77 @@ class ShapeString(DraftObject): """The ShapeString object""" def __init__(self, obj): - super(ShapeString, self).__init__(obj, "ShapeString") + super().__init__(obj, "ShapeString") + self.set_properties(obj) - _tip = QT_TRANSLATE_NOOP("App::Property", "Text string") - obj.addProperty("App::PropertyString", "String", "Draft", _tip) + def set_properties(self, obj): + """Add properties to the object and set them.""" + properties = obj.PropertiesList - _tip = QT_TRANSLATE_NOOP("App::Property", "Font file name") - obj.addProperty("App::PropertyFile", "FontFile", "Draft", _tip) + if "String" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Text string") + obj.addProperty("App::PropertyString", "String", "Draft", _tip) - _tip = QT_TRANSLATE_NOOP("App::Property", "Height of text") - obj.addProperty("App::PropertyLength", "Size", "Draft", _tip) + if "FontFile" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Font file name") + obj.addProperty("App::PropertyFile", "FontFile", "Draft", _tip) - _tip = QT_TRANSLATE_NOOP("App::Property", "Inter-character spacing") - obj.addProperty("App::PropertyLength", "Tracking", "Draft", _tip) + if "Size" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Height of text") + obj.addProperty("App::PropertyLength", "Size", "Draft", _tip) - _tip = QT_TRANSLATE_NOOP("App::Property", "Fill letters with faces") - obj.addProperty("App::PropertyBool", "MakeFace", "Draft", _tip).MakeFace = True + if "Justification" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Horizontal and vertical alignment") + obj.addProperty("App::PropertyEnumeration", "Justification", "Draft", _tip) + obj.Justification = ["Top-Left", "Top-Center", "Top-Right", + "Middle-Left", "Middle-Center", "Middle-Right", + "Bottom-Left", "Bottom-Center", "Bottom-Right"] + obj.Justification = "Bottom-Left" + + if "JustificationReference" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Height reference used for justification") + obj.addProperty("App::PropertyEnumeration", "JustificationReference", "Draft", _tip) + obj.JustificationReference = ["Cap Height", "Shape Height"] + obj.JustificationReference = "Cap Height" + + if "KeepLeftMargin" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Keep left margin and leading white space when justification is left") + obj.addProperty("App::PropertyBool", "KeepLeftMargin", "Draft", _tip).KeepLeftMargin = False + + if "ScaleToSize" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Scale to ensure cap height is equal to size") + obj.addProperty("App::PropertyBool", "ScaleToSize", "Draft", _tip).ScaleToSize = True + + if "Tracking" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Inter-character spacing") + obj.addProperty("App::PropertyDistance", "Tracking", "Draft", _tip) + + if "MakeFace" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Fill letters with faces") + obj.addProperty("App::PropertyBool", "MakeFace", "Draft", _tip).MakeFace = True + + if "Fuse" not in properties: + _tip = QT_TRANSLATE_NOOP("App::Property", "Fuse faces if faces overlap, usually not required (can be very slow)") + obj.addProperty("App::PropertyBool", "Fuse", "Draft", _tip).Fuse = False + + def onDocumentRestored(self, obj): + super().onDocumentRestored(obj) + if hasattr(obj, "Justification"): # several more properties were added + return + self.update_properties_0v22(obj) + + def update_properties_0v22(self, obj): + """Update view properties.""" + old_tracking = obj.Tracking # no need for obj.getTypeIdOfProperty("Tracking") + obj.removeProperty("Tracking") + self.set_properties(obj) + obj.KeepLeftMargin = True + obj.ScaleToSize = False + obj.Tracking = old_tracking + _wrn("v0.22, " + obj.Label + ", " + + translate("draft", "added 'Justification', 'JustificationReference', 'KeepLeftMargin', 'ScaleToSize' and 'Fuse' properties")) + _wrn("v0.22, " + obj.Label + ", " + + translate("draft", "changed 'Tracking' property type")) def execute(self, obj): if self.props_changed_placement_only(): @@ -64,42 +122,50 @@ class ShapeString(DraftObject): return if obj.String and obj.FontFile: - import Part - if obj.Placement: plm = obj.Placement - # test a simple letter to know if we have a sticky font or not - sticky = False - testWire = Part.makeWireString("L", obj.FontFile, obj.Size, obj.Tracking)[0][0] - if testWire.isClosed: - try: - testFace = Part.Face(testWire) - except Part.OCCError: - sticky = True + fill = obj.MakeFace + if fill is True: + # test a simple letter to know if we have a sticky font or not + # if font is sticky change fill to `False` + test_wire = Part.makeWireString("L", obj.FontFile, obj.Size, obj.Tracking)[0][0] + if test_wire.isClosed: + try: + test_face = Part.Face(test_wire) + except Part.OCCError: + fill = False + else: + fill = test_face.isValid() and test_face.Area > 1e-7 else: - if not testFace.isValid(): - sticky = True - else: - sticky = True + fill = False - fill = True - if hasattr(obj, "MakeFace"): - fill = obj.MakeFace + chars = Part.makeWireString(obj.String, obj.FontFile, obj.Size, obj.Tracking) + shapes = [] - CharList = Part.makeWireString(obj.String, obj.FontFile, obj.Size, obj.Tracking) - SSChars = [] - - for char in CharList: - if sticky or (not fill): - SSChars.extend(char) + for char in chars: + if fill is False: + shapes.extend(char) elif char: - SSChars.extend(self.makeFaces(char)) - if SSChars: - shape = Part.Compound(SSChars) - obj.Shape = shape + shapes.extend(self.make_faces(char)) + if shapes: + if obj.MakeFace and obj.Fuse: + ss_shape = shapes[0].fuse(shapes[1:]) + ss_shape = faces.concatenate(ss_shape) + else: + ss_shape = Part.Compound(shapes) + cap_char = Part.makeWireString("M", obj.FontFile, 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 + obj.Shape = self.justification(ss_shape, + cap_height, + obj.Justification, + obj.JustificationReference, + obj.KeepLeftMargin) else: - App.Console.PrintWarning(translate("draft", "ShapeString: string has no wires")+"\n") + App.Console.PrintWarning(translate("draft", "ShapeString: string has no wires") + "\n") if plm: obj.Placement = plm @@ -110,9 +176,32 @@ class ShapeString(DraftObject): def onChanged(self, obj, prop): self.props_changed_store(prop) - def makeFaces(self, wireChar): - import Part + def justification(self, ss_shape, cap_height, just, just_ref, keep_left_margin): # ss_shape is a compound + shapes = ss_shape.SubShapes + box = ss_shape.BoundBox + if keep_left_margin is True: + vec = App.Vector(0, 0, 0) + else: + vec = App.Vector(-box.XMin, 0, 0) # remove left margin caused by kerning and white space characters + width = box.XLength + if "Shape" in just_ref: + vec = vec + App.Vector(0, -box.YMin, 0) + height = box.YLength + else: + height = cap_height + if "Top" in just: + vec = vec + App.Vector(0, -height, 0) + elif "Middle" in just: + vec = vec + App.Vector(0, -height/2, 0) + if "Right" in just: + vec = vec + App.Vector(-width, 0, 0) + elif "Center" in just: + vec = vec + App.Vector(-width/2, 0, 0) + for shape in shapes: + shape.translate(vec) + return Part.Compound(shapes) + def make_faces(self, wireChar): wrn = translate("draft", "ShapeString: face creation failed for one character") + "\n" wirelist = [] @@ -168,50 +257,13 @@ class ShapeString(DraftObject): for face in faces: try: # some fonts fail here - if face.Surface.Axis.z < 0.0: # Does not seem to occur for FaceMakerBullseye. + if face.normalAt(0, 0).z < 0: # Does not seem to occur for FaceMakerBullseye. face.reverse() except Exception: pass return faces - def makeGlyph(self, facelist): - ''' turn list of simple contour faces into a compound shape representing a glyph ''' - ''' remove cuts, fuse overlapping contours, retain islands ''' - import Part - if len(facelist) == 1: - return facelist[0] - - sortedfaces = sorted(facelist,key=(lambda shape: shape.Area),reverse=True) - - biggest = sortedfaces[0] - result = biggest - islands =[] - for face in sortedfaces[1:]: - bcfA = biggest.common(face).Area - fA = face.Area - difA = abs(bcfA - fA) - eps = utils.epsilon() - # if biggest.common(face).Area == face.Area: - if difA <= eps: # close enough to zero - # biggest completely overlaps current face ==> cut - result = result.cut(face) - # elif biggest.common(face).Area == 0: - elif bcfA <= eps: - # island - islands.append(face) - else: - # partial overlap - (font designer error?) - result = result.fuse(face) - #glyphfaces = [result] - wl = result.Wires - for w in wl: - w.fixWire() - glyphfaces = [Part.Face(wl)] - glyphfaces.extend(islands) - ret = Part.Compound(glyphfaces) # should we fuse these instead of making compound? - return ret - # Alias for compatibility with v0.18 and earlier _ShapeString = ShapeString