Download src/Mod/Draft/draftobjects/shapestring.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Draft/draftobjects/shapestring.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/Draft/draftobjects/shapestring.py
-
curl -L -o shapestring.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Draft/draftobjects/shapestring.py
15.2 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net> * | |
| # * Copyright (c) 2009, 2010 Ken Cline <cline@frii.com> * | |
| # * 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 | |
| ## @} | |