# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * # * Copyright (c) 2020 Eliud Cabrera Castillo * # * Copyright (c) 2021 FreeCAD Developers * # * * # * This file is part of the FreeCAD CAx development system. * # * * # * 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. * # * * # * FreeCAD 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 FreeCAD; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** """Provides the viewprovider code for the Label object.""" ## @package view_label # \ingroup draftviewproviders # \brief Provides the viewprovider code for the Label object. ## \addtogroup draftviewproviders # @{ import math import sys import pivy.coin as coin from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App from draftutils import gui_utils from draftutils import params from draftutils import utils from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation if App.GuiUp: import FreeCADGui as Gui class ViewProviderLabel(ViewProviderDraftAnnotation): """Viewprovider for the Label annotation object.""" def set_text_properties(self, vobj, properties): """Set text properties only if they don't already exist.""" super().set_text_properties(vobj, properties) if "TextAlignment" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Vertical alignment") vobj.addProperty("App::PropertyEnumeration", "TextAlignment", "Text", _tip, locked=True) vobj.TextAlignment = ["Top", "Middle", "Bottom"] vobj.TextAlignment = "Bottom" if "MaxChars" not in properties: _tip = QT_TRANSLATE_NOOP( "App::Property", "Maximum number of characters " "on each line of the text box" ) vobj.addProperty("App::PropertyInteger", "MaxChars", "Text", _tip, locked=True) if "Justification" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Horizontal alignment") vobj.addProperty("App::PropertyEnumeration", "Justification", "Text", _tip, locked=True) vobj.Justification = ["Left", "Center", "Right"] if "LineSpacing" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Line spacing (relative to font size)") vobj.addProperty("App::PropertyFloat", "LineSpacing", "Text", _tip, locked=True) vobj.LineSpacing = params.get_param("LineSpacing") def set_graphics_properties(self, vobj, properties): """Set graphics properties only if they don't already exist.""" super().set_graphics_properties(vobj, properties) vobj.ArrowTypeStart = params.get_param("dimsymbolstart") if "Frame" not in properties: _tip = QT_TRANSLATE_NOOP( "App::Property", "The type of frame around the text " "of this object" ) vobj.addProperty("App::PropertyEnumeration", "Frame", "Graphics", _tip, locked=True) vobj.Frame = ["None", "Rectangle"] if "Line" not in properties: _tip = QT_TRANSLATE_NOOP("App::Property", "Display a leader line or not") vobj.addProperty("App::PropertyBool", "Line", "Graphics", _tip, locked=True) vobj.Line = True def getIcon(self): """Return the path to the icon used by the viewprovider.""" return ":/icons/Draft_Label.svg" def attach(self, vobj): """Set up the scene sub-graph of the viewprovider.""" self.Object = vobj.Object # Attributes of the Coin scenegraph self.arrow = coin.SoSeparator() self.arrowpos = coin.SoTransform() self.arrow.addChild(self.arrowpos) self.matline = coin.SoMaterial() self.drawstyle = coin.SoDrawStyle() self.drawstyle.style = coin.SoDrawStyle.LINES import PartGui # Required for "SoBrepEdgeSet" (because a label is not a Part::FeaturePython object). self.lcoords = coin.SoCoordinate3() self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() self.mattext = coin.SoMaterial() self.textpos = coin.SoTransform() self.font = coin.SoFont() self.text_wld = coin.SoAsciiText() # World orientation. Can be oriented in 3D space. self.text_scr = coin.SoText2() # Screen orientation. Always faces the camera. self.fcoords = coin.SoCoordinate3() self.frame = coin.SoType.fromName("SoBrepEdgeSet").createInstance() self.lineswitch = coin.SoSwitch() self.startSymbol = gui_utils.dim_symbol() textdrawstyle = coin.SoDrawStyle() textdrawstyle.style = coin.SoDrawStyle.FILLED # The text string needs to be initialized to something, # otherwise it crashes self.text_wld.string = self.text_scr.string = "Label" self.text_wld.justification = coin.SoAsciiText.RIGHT self.text_scr.justification = coin.SoText2.RIGHT self.font.name = params.get_param("textfont") switchnode = coin.SoSeparator() switchnode.addChild(self.line) self.lineswitch.addChild(switchnode) self.lineswitch.whichChild = 0 self.node_wld_txt = coin.SoGroup() self.node_wld_txt.addChild(self.font) self.node_wld_txt.addChild(self.text_wld) self.node_wld = coin.SoGroup() self.node_wld.addChild(self.matline) self.node_wld.addChild(self.arrow) self.node_wld.addChild(self.drawstyle) self.node_wld.addChild(self.lcoords) self.node_wld.addChild(self.lineswitch) self.node_wld.addChild(self.mattext) self.node_wld.addChild(textdrawstyle) self.node_wld.addChild(self.textpos) self.node_wld.addChild(self.node_wld_txt) self.node_wld.addChild(self.matline) self.node_wld.addChild(self.drawstyle) self.node_wld.addChild(self.fcoords) self.node_wld.addChild(self.frame) self.node_scr_txt = coin.SoGroup() self.node_scr_txt.addChild(self.font) self.node_scr_txt.addChild(self.text_scr) self.node_scr = coin.SoGroup() self.node_scr.addChild(self.matline) self.node_scr.addChild(self.arrow) self.node_scr.addChild(self.drawstyle) self.node_scr.addChild(self.lcoords) self.node_scr.addChild(self.lineswitch) self.node_scr.addChild(self.mattext) self.node_scr.addChild(textdrawstyle) self.node_scr.addChild(self.textpos) self.node_scr.addChild(self.node_scr_txt) self.node_scr.addChild(self.matline) self.node_scr.addChild(self.drawstyle) self.node_scr.addChild(self.fcoords) self.node_scr.addChild(self.frame) vobj.addDisplayMode(self.node_wld, "World") vobj.addDisplayMode(self.node_scr, "Screen") self.onChanged(vobj, "LineColor") self.onChanged(vobj, "TextColor") self.onChanged(vobj, "LineWidth") self.onChanged(vobj, "ArrowSizeStart") self.onChanged(vobj, "Line") def updateData(self, obj, prop): """Execute when a property from the Proxy class is changed.""" vobj = obj.ViewObject if prop == "Points": n_points = len(obj.Points) if n_points >= 2: self.line.coordIndex.deleteValues(0) self.lcoords.point.setValues(obj.Points) self.line.coordIndex.setValues(0, n_points, range(n_points)) self.onChanged(obj.ViewObject, "ArrowTypeStart") # When initially called (on doc open) properties of vobj are not yet available. # This function is however triggered again from update_label. if ( getattr(vobj, "Justification", "") == "Center" and obj.StraightDistance != 0.0 and obj.StraightDirection != "Custom" ): if obj.StraightDirection != "Vertical": obj.StraightDirection = "Vertical" if not hasattr(vobj, "TextAlignment"): pass elif obj.StraightDistance > 0.0 and vobj.TextAlignment != "Top": vobj.TextAlignment = "Top" elif obj.StraightDistance < 0.0 and vobj.TextAlignment != "Bottom": vobj.TextAlignment = "Bottom" elif obj.StraightDistance > 0.0 and obj.StraightDirection == "Horizontal": if vobj.Justification == "Left": vobj.Justification = "Right" elif obj.StraightDistance < 0.0 and obj.StraightDirection == "Horizontal": if vobj.Justification == "Right": vobj.Justification = "Left" self.onChanged( obj.ViewObject, "DisplayMode" ) # Property to trigger update_label and update_frame. # We could have used a different property. elif prop == "Text" and obj.Text: self.text_wld.string.setValue("") self.text_scr.string.setValue("") _list = [l for l in obj.Text if l] self.text_wld.string.setValues(_list) self.text_scr.string.setValues(_list) self.onChanged(obj.ViewObject, "DisplayMode") def onChanged(self, vobj, prop): """Execute when a view property is changed.""" super().onChanged(vobj, prop) obj = vobj.Object properties = vobj.PropertiesList can_update_label = ( "DisplayMode" in properties and "FontName" in properties and "FontSize" in properties and "Justification" in properties and "LineSpacing" in properties and "ScaleMultiplier" in properties and "TextAlignment" in properties ) # Top, Middle or Bottom. can_update_frame = can_update_label and "Frame" in properties if prop in [ "DisplayMode", "FontName", "FontSize", "Justification", "LineSpacing", "TextAlignment", "Frame", ]: if can_update_label: self.update_label(obj, vobj) if can_update_frame: self.update_frame(obj, vobj) elif prop == "ScaleMultiplier" and "ScaleMultiplier" in properties: if "ArrowSizeStart" in properties: s = vobj.ArrowSizeStart.Value * vobj.ScaleMultiplier if s: self.arrowpos.scaleFactor.setValue((s, s, s)) if can_update_label: self.update_label(obj, vobj) if can_update_frame: self.update_frame(obj, vobj) elif ( prop == "ArrowSizeStart" and "ArrowSizeStart" in properties and "ScaleMultiplier" in properties ): s = vobj.ArrowSizeStart.Value * vobj.ScaleMultiplier if s: self.arrowpos.scaleFactor.setValue((s, s, s)) elif prop == "ArrowTypeStart" and "ArrowTypeStart" in properties: if len(obj.Points) > 1: self.update_arrow(obj, vobj) elif prop == "Line" and "Line" in properties: if vobj.Line: self.lineswitch.whichChild = 0 else: self.lineswitch.whichChild = -1 elif prop == "LineWidth" and "LineWidth" in properties: self.drawstyle.lineWidth = vobj.LineWidth elif prop == "LineColor" and "LineColor" in properties: col = vobj.LineColor self.matline.diffuseColor.setValue([col[0], col[1], col[2]]) elif prop == "TextColor" and "TextColor" in properties: col = vobj.TextColor self.mattext.diffuseColor.setValue([col[0], col[1], col[2]]) def get_text_size(self, vobj): """Return the bounding box of the text element.""" if vobj.DisplayMode == "World": node = self.node_wld_txt else: node = self.node_scr_txt region = coin.SbViewportRegion() action = coin.SoGetBoundingBoxAction(region) node.getBoundingBox(action) return action.getBoundingBox().getSize().getValue() def update_label(self, obj, vobj): """Update the label including text size and multiplier.""" size = vobj.FontSize.Value * vobj.ScaleMultiplier line_spacing = max(1, vobj.LineSpacing) self.font.size = size self.text_wld.spacing = line_spacing self.text_scr.spacing = line_spacing self.font.name = vobj.FontName.encode("utf8") if vobj.Justification == "Left": self.text_wld.justification = coin.SoAsciiText.LEFT self.text_scr.justification = coin.SoText2.LEFT if obj.StraightDistance > 0.0 and obj.StraightDirection == "Horizontal": obj.StraightDistance = -obj.StraightDistance obj.recompute() obj.purgeTouched() elif vobj.Justification == "Right": self.text_wld.justification = coin.SoAsciiText.RIGHT self.text_scr.justification = coin.SoText2.RIGHT if obj.StraightDistance < 0.0 and obj.StraightDirection == "Horizontal": obj.StraightDistance = -obj.StraightDistance obj.recompute() obj.purgeTouched() else: self.text_wld.justification = coin.SoAsciiText.CENTER self.text_scr.justification = coin.SoText2.CENTER if obj.StraightDistance != 0.0 and obj.StraightDirection != "Custom": if obj.StraightDirection != "Vertical": obj.StraightDirection = "Vertical" if (obj.StraightDistance < 0.0 and vobj.TextAlignment == "Top") or ( obj.StraightDistance > 0.0 and vobj.TextAlignment == "Bottom" ): obj.StraightDistance = -obj.StraightDistance obj.recompute() obj.purgeTouched() if vobj.DisplayMode == "Screen": self.textpos.translation.setValue(obj.Placement.Base) return line_height = size * line_spacing if vobj.Frame == "None" and vobj.Justification != "Center": margin = line_height * 0.1 first_line_height = size # We need to calculate total_height without using get_text_size: # If StraightDirection = "Horizontal" and TextAlignment = "Bottom" # we want the horizontal line segment to be aligned with the # baseline of the bottom text even if there are descenders. total_height = first_line_height + (line_height * (len(obj.Text) - 1)) else: margin = line_height * 0.25 first_line_height = size + margin box = self.get_text_size(vobj) total_height = box[1] + (2 * margin) # Space between endpoint of line and text: if vobj.Justification == "Left": v = App.Vector(margin, 0, 0) elif vobj.Justification == "Right": v = App.Vector(-margin, 0, 0) else: v = App.Vector(0, 0, 0) if vobj.TextAlignment == "Top": v = v + App.Vector(0, -first_line_height, 0) elif vobj.TextAlignment == "Middle": v = v + App.Vector(0, -first_line_height + (total_height / 2), 0) elif vobj.TextAlignment == "Bottom": v = v + App.Vector(0, -first_line_height + total_height, 0) v = obj.Placement.Rotation.multVec(v) pos = v + obj.Placement.Base self.textpos.translation.setValue(pos) self.textpos.rotation.setValue(obj.Placement.Rotation.Q) def update_arrow(self, obj, vobj): """Update the arrow tip of the line.""" if hasattr(self, "startSymbol"): if self.arrow.findChild(self.startSymbol) != -1: self.arrow.removeChild(self.startSymbol) startS = utils.ARROW_TYPES.index(vobj.ArrowTypeStart) self.startSymbol = gui_utils.dim_symbol(startS) self.arrow.addChild(self.startSymbol) prec = 10 ** (-utils.precision()) x_axis = App.Vector(1, 0, 0) target_dir = None # search in Points to get first point != to TargetPoint and use it # to get the target line direction for pnt in obj.Points[-2::-1]: if not pnt.isEqual(obj.Points[-1], prec): target_dir = pnt.sub(obj.Points[-1]) break if target_dir is None: target_dir = x_axis target_dir_xy = obj.Placement.Rotation.inverted() * target_dir angle = target_dir_xy.getAngle(x_axis) * App.Units.Radian axis = x_axis.cross(target_dir_xy) rot = App.Rotation(axis, angle) self.arrowpos.rotation.setValue((obj.Placement.Rotation * rot).Q) self.arrowpos.translation.setValue(obj.Points[-1]) def update_frame(self, obj, vobj): """Update the frame around the text.""" self.frame.coordIndex.deleteValues(0) if vobj.Frame == "None": return if vobj.DisplayMode == "Screen": return size = vobj.FontSize.Value * vobj.ScaleMultiplier self.font.size = size line_height = size * max(1, vobj.LineSpacing) margin = line_height * 0.25 first_line_height = size + margin box = self.get_text_size(vobj) total_width = box[0] + (2 * margin) total_height = box[1] + (2 * margin) # Space between frame and text: if vobj.Justification == "Left": first_frame_point = App.Vector(-margin, first_line_height, 0) elif vobj.Justification == "Right": first_frame_point = App.Vector(margin - total_width, first_line_height, 0) else: first_frame_point = App.Vector(-total_width / 2, first_line_height, 0) # Shape of the rectangle # (p5)p1 --------- p2 # | | # b | # | | # p4 --------- p3 # pts = [] pts.append(first_frame_point) pts.append(first_frame_point + App.Vector(total_width, 0, 0)) pts.append(pts[-1] + App.Vector(0, -total_height, 0)) pts.append(pts[-1] + App.Vector(-total_width, 0, 0)) pts.append(first_frame_point) self.fcoords.point.setValues(pts) self.frame.coordIndex.setValues(0, len(pts), range(len(pts))) # Alias for compatibility with v0.18 and earlier ViewProviderDraftLabel = ViewProviderLabel ## @}