# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * # * Copyright (c) 2020 FreeCAD Developers * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** """Provides the object code for the ShapeString object.""" ## @package shapestring # \ingroup draftobjects # \brief Provides the object code for the ShapeString object. ## \addtogroup draftobjects # @{ import os import math from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import Part from draftgeoutils import faces from draftobjects.base import DraftObject from draftutils import gui_utils from draftutils.messages import _err, _log from draftutils.translate import translate class ShapeString(DraftObject): """The ShapeString object""" def __init__(self, obj): super().__init__(obj, "ShapeString") self.set_properties(obj) def set_properties(self, obj): """Add properties to the object and set them.""" properties = obj.PropertiesList if "String" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Text string") obj.addProperty("App::PropertyString", "String", "Draft", _tip, locked=True) if "FontFile" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Font file name") obj.addProperty("App::PropertyFile", "FontFile", "Draft", _tip, locked=True) if "Size" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Height of text") obj.addProperty("App::PropertyLength", "Size", "Draft", _tip, locked=True) if "Justification" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Horizontal and vertical alignment") obj.addProperty("App::PropertyEnumeration", "Justification", "Draft", _tip, locked=True) 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, locked=True ) 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, locked=True ).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, locked=True ).ScaleToSize = True if "Tracking" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Inter-character spacing") obj.addProperty("App::PropertyDistance", "Tracking", "Draft", _tip, locked=True) if "ObliqueAngle" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Oblique (slant) angle") obj.addProperty("App::PropertyAngle", "ObliqueAngle", "Draft", _tip, locked=True) if "MakeFace" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Fill letters with faces") obj.addProperty( "App::PropertyBool", "MakeFace", "Draft", _tip, locked=True ).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, locked=True).Fuse = False def onDocumentRestored(self, obj): super().onDocumentRestored(obj) gui_utils.restore_view_object( obj, vp_module="view_shapestring", vp_class="ViewProviderShapeString" ) if not hasattr(obj, "ObliqueAngle"): # several more properties were added self.update_properties_1v0(obj) def update_properties_1v0(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 _log( "v1.0, " + obj.Name + ", " + "added 'Fuse', 'Justification', 'JustificationReference', 'KeepLeftMargin', " + "'ObliqueAngle' and 'ScaleToSize' properties" ) _log("v1.0, " + obj.Name + ", changed 'Tracking' property type") def execute(self, obj): if self.props_changed_placement_only() or not obj.String or not obj.FontFile: obj.positionBySupport() self.props_changed_clear() return 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 # 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: # 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) else: App.Console.PrintWarning(translate("draft", "ShapeString: string has no wires") + "\n") obj.Placement = plm obj.positionBySupport() self.props_changed_clear() def onChanged(self, obj, prop): self.props_changed_store(prop) def justification_vector( self, ss_shape, cap_height, just, just_ref, keep_left_margin ): # ss_shape is a compound box = ss_shape.optimalBoundingBox() if keep_left_margin is True and "Left" in just: 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) return vec def make_faces(self, wireChar): wrn = translate("draft", "ShapeString: face creation failed for one character") + "\n" wirelist = [] for w in wireChar: compEdges = Part.Compound(w.Edges) compEdges = compEdges.connectEdgesToWires() if compEdges.Wires[0].isClosed(): wirelist.append(compEdges.Wires[0]) if not wirelist: App.Console.PrintWarning(wrn) return [] # Some test fonts: # https://raw.githubusercontent.com/FreeCAD/FPA/main/images/freecad_logo_official.svg # https://evolventa.github.io/ # not a problem font, but it is used by FreeCAD # https://forum.freecad.org/viewtopic.php?t=57774 # https://www.dafont.com/mutlu-ornamental.font # https://forum.freecad.org/viewtopic.php?t=65110&p=559810#p559886 # http://www.atelier-des-fougeres.fr/Cambam/Aide/Plugins/stickfonts.html # 1CamBam_Stick_0.ttf is actually not a stick font. # FaceMakerBullseye: # 1CamBam_Stick_0.ttf face validation problem with A, E, F, H, K, R, Y and y. # FaceMakerCheese: # 1CamBam_Stick_0.ttf face creation problem with: A, E, F, H, Q, R, e and y. # All fonts: face creation problem in case of double-nested wires f.e. with: ©. # FaceMakerSimple: # All fonts: overlapping faces in case of nested wires f.e. with: O. try: # print("try Bullseye") faces = Part.makeFace(wirelist, "Part::FaceMakerBullseye").Faces for face in faces: face.validate() except Part.OCCError: try: # print("try Cheese") faces = Part.makeFace(wirelist, "Part::FaceMakerCheese").Faces for face in faces: face.validate() except Part.OCCError: try: # print("try Simple") faces = Part.makeFace(wirelist, "Part::FaceMakerSimple").Faces for face in faces: face.validate() except Part.OCCError: App.Console.PrintWarning(wrn) return [] for face in faces: try: # some fonts fail here if face.normalAt(0, 0).z < 0: # Does not seem to occur for FaceMakerBullseye. face.reverse() except Exception: pass return faces # Alias for compatibility with v0.18 and earlier _ShapeString = ShapeString ## @}