1142 lines
36 KiB
Python
1142 lines
36 KiB
Python
# 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) 2019 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
|
|
# * Copyright (c) 2024 The FreeCAD Project Association *
|
|
# * *
|
|
# * 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 general utility functions used throughout the workbench.
|
|
|
|
This module contains auxiliary functions which can be used
|
|
in other modules of the workbench, and which don't require
|
|
the graphical user interface (GUI).
|
|
"""
|
|
## @package utils
|
|
# \ingroup draftutils
|
|
# \brief Provides general utility functions used throughout the workbench.
|
|
|
|
## \addtogroup draftutils
|
|
# @{
|
|
import os
|
|
import PySide.QtCore as QtCore
|
|
|
|
import FreeCAD as App
|
|
from draftutils import params
|
|
from draftutils.messages import _wrn, _err, _log
|
|
from draftutils.translate import translate
|
|
from builtins import open
|
|
|
|
# TODO: move the functions that require the graphical interface
|
|
# This module should not import any graphical commands; those should be
|
|
# in gui_utils
|
|
if App.GuiUp:
|
|
import FreeCADGui as Gui
|
|
import Draft_rc
|
|
|
|
# The module is used to prevent complaints from code checkers (flake8)
|
|
True if Draft_rc else False
|
|
|
|
|
|
ARROW_TYPES = ["Dot", "Circle", "Arrow", "Tick", "Tick-2", "None"]
|
|
DISPLAY_MODES = ["Flat Lines", "Shaded", "Wireframe", "Points"]
|
|
DRAW_STYLES = ["Solid", "Dashed", "Dotted", "Dashdot"]
|
|
arrowtypes = ARROW_TYPES
|
|
|
|
|
|
def get_default_annotation_style():
|
|
arrow_start_type_index = params.get_param("dimsymbolstart")
|
|
arrow_end_type_index = params.get_param("dimsymbolend")
|
|
return {
|
|
"ArrowSizeStart": ("float", params.get_param("arrowsizestart")),
|
|
"ArrowSizeEnd": ("float", params.get_param("arrowsizeend")),
|
|
"ArrowTypeStart": ("index", arrow_start_type_index, ARROW_TYPES[arrow_start_type_index]),
|
|
"ArrowTypeEnd": ("index", arrow_end_type_index, ARROW_TYPES[arrow_end_type_index]),
|
|
"Decimals": ("int", params.get_param("dimPrecision")),
|
|
"DimOvershoot": ("float", params.get_param("dimovershoot")),
|
|
"ExtLines": ("float", params.get_param("extlines")),
|
|
"ExtOvershoot": ("float", params.get_param("extovershoot")),
|
|
"FontName": ("font", params.get_param("textfont")),
|
|
"FontSize": ("float", params.get_param("textheight")),
|
|
"LineColor": ("color", params.get_param("DefaultAnnoLineColor") | 0x000000FF),
|
|
"LineSpacing": ("float", params.get_param("LineSpacing")),
|
|
"LineWidth": ("int", params.get_param("DefaultAnnoLineWidth")),
|
|
"ScaleMultiplier": ("float", params.get_param("DefaultAnnoScaleMultiplier")),
|
|
"ShowLine": ("bool", params.get_param("DimShowLine")),
|
|
"ShowUnit": ("bool", params.get_param("showUnit")),
|
|
"TextColor": ("color", params.get_param("DefaultTextColor") | 0x000000FF),
|
|
"TextSpacing": ("float", params.get_param("dimspacing")),
|
|
"UnitOverride": ("str", params.get_param("overrideUnit")),
|
|
}
|
|
|
|
|
|
def repair_annotation_style(style):
|
|
"""Repair a V0.19, V0.20 or < V1.1 style.
|
|
|
|
V0.19 and V0.20:
|
|
Some properties were missing or misspelled.
|
|
Some float values were wrongly stored as strings.
|
|
|
|
V1.0 -> V1.1:
|
|
ArrowSize has been replaced by ArrowSizeStart and ArrowSizeEnd.
|
|
ArrowType has been replaced by ArrowTypeStart and ArrowTypeEnd.
|
|
"""
|
|
for key in ("ArrowSize", "ArrowType"):
|
|
if (
|
|
style.get(key) is not None
|
|
and style.get(key + "Start") is None
|
|
and style.get(key + "End") is None
|
|
):
|
|
style[key + "Start"] = style[key]
|
|
style[key + "End"] = style[key]
|
|
default = get_default_annotation_style()
|
|
new = {}
|
|
for key, val in default.items():
|
|
if style.get(key) is None:
|
|
new[key] = val[1]
|
|
elif type(style[key]) == type(val[1]):
|
|
new[key] = style[key]
|
|
elif isinstance(style[key], str):
|
|
new[key] = float(style[key].replace(",", "."))
|
|
else:
|
|
new[key] = val[1]
|
|
return new
|
|
|
|
|
|
def get_default_shape_style():
|
|
# Uses the same format as get_default_annotation_style().
|
|
display_mode_index = params.get_param("DefaultDisplayMode")
|
|
draw_style_index = params.get_param("DefaultDrawStyle")
|
|
return {
|
|
"DisplayMode": ("index", display_mode_index, DISPLAY_MODES[display_mode_index]),
|
|
"DrawStyle": ("index", draw_style_index, DRAW_STYLES[draw_style_index]),
|
|
"LineColor": ("color", params.get_param_view("DefaultShapeLineColor") | 0x000000FF),
|
|
"LineWidth": ("int", params.get_param_view("DefaultShapeLineWidth")),
|
|
"PointColor": ("color", params.get_param_view("DefaultShapeVertexColor") | 0x000000FF),
|
|
"PointSize": ("int", params.get_param_view("DefaultShapePointSize")),
|
|
"ShapeAppearance": ("material", (get_view_material(),)),
|
|
}
|
|
|
|
|
|
def get_view_material():
|
|
"""Return a ShapeAppearance material with properties based on the preferences."""
|
|
material = App.Material()
|
|
material.AmbientColor = params.get_param_view("DefaultAmbientColor") | 0x000000FF
|
|
material.DiffuseColor = params.get_param_view("DefaultShapeColor") | 0x000000FF
|
|
material.EmissiveColor = params.get_param_view("DefaultEmissiveColor") | 0x000000FF
|
|
material.Shininess = params.get_param_view("DefaultShapeShininess") / 100
|
|
material.SpecularColor = params.get_param_view("DefaultSpecularColor") | 0x000000FF
|
|
material.Transparency = params.get_param_view("DefaultShapeTransparency") / 100
|
|
return material
|
|
|
|
|
|
def string_encode_coin(ustr):
|
|
"""Encode a unicode object to be used as a string in coin.
|
|
|
|
Parameters
|
|
----------
|
|
ustr : str
|
|
A string to be encoded
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
Encoded string. If the coin version is >= 4
|
|
it will encode the string to `'utf-8'`, otherwise
|
|
it will encode it to `'latin-1'`.
|
|
"""
|
|
try:
|
|
from pivy import coin
|
|
|
|
coin4 = coin.COIN_MAJOR_VERSION >= 4
|
|
except (ImportError, AttributeError):
|
|
coin4 = False
|
|
if coin4:
|
|
return ustr.encode("utf-8")
|
|
else:
|
|
return ustr.encode("latin1")
|
|
|
|
|
|
stringencodecoin = string_encode_coin
|
|
|
|
|
|
def type_check(args_and_types, name="?"):
|
|
"""Check that the arguments are instances of certain types.
|
|
|
|
Parameters
|
|
----------
|
|
args_and_types : list
|
|
A list of tuples. The first element of a tuple is tested as being
|
|
an instance of the second element.
|
|
::
|
|
args_and_types = [(a, Type), (b, Type2), ...]
|
|
|
|
Then
|
|
::
|
|
isinstance(a, Type)
|
|
isinstance(b, Type2)
|
|
|
|
A `Type` can also be a tuple of many types, in which case
|
|
the check is done for any of them.
|
|
::
|
|
args_and_types = [(a, (Type3, int, float)), ...]
|
|
|
|
isinstance(a, (Type3, int, float))
|
|
|
|
name : str, optional
|
|
Defaults to `'?'`. The name of the check.
|
|
|
|
Raises
|
|
------
|
|
TypeError
|
|
If the first element in the tuple is not an instance of the second
|
|
element, it raises `Draft.name`.
|
|
"""
|
|
for v, t in args_and_types:
|
|
if not isinstance(v, t):
|
|
w = "typecheck[{}]: '{}' is not {}".format(name, v, t)
|
|
_err(w)
|
|
raise TypeError("Draft." + str(name))
|
|
|
|
|
|
typecheck = type_check
|
|
|
|
|
|
def precision():
|
|
"""Return the precision value from the parameter database.
|
|
|
|
It is the number of decimal places that a float will have.
|
|
Example
|
|
::
|
|
precision=6, 0.123456
|
|
precision=5, 0.12345
|
|
precision=4, 0.1234
|
|
|
|
Due to floating point operations there may be rounding errors.
|
|
Therefore, this precision number is used to round up values
|
|
so that all operations are consistent.
|
|
By default the precision is 6 decimal places.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
params.get_param("precision")
|
|
"""
|
|
return params.get_param("precision")
|
|
|
|
|
|
def svg_precision():
|
|
"""Return the precision value for SVG import from the parameter database.
|
|
|
|
It is the number of decimal places that a float will have.
|
|
Example
|
|
::
|
|
precision=5, 0.12345
|
|
precision=4, 0.1234
|
|
precision=3, 0.123
|
|
|
|
Due to floating point operations there may be rounding errors.
|
|
Therefore, this precision number is used to round up values
|
|
so that all operations are consistent.
|
|
By default the precision is 3 decimal places.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
params.get_param("svgPrecision")
|
|
"""
|
|
return params.get_param("svgPrecision")
|
|
|
|
|
|
def tolerance():
|
|
"""Return a tolerance based on the precision() value
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
10 ** -precision()
|
|
"""
|
|
return 10 ** -precision()
|
|
|
|
|
|
def is_deleted(obj):
|
|
"""Return `True` if obj is deleted."""
|
|
try:
|
|
return not obj.isAttachedToDocument()
|
|
except:
|
|
return True
|
|
|
|
|
|
def get_real_name(name):
|
|
"""Strip the trailing numbers from a string to get only the letters.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
A string that may have a number at the end, `Line001`.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
A string without the numbers at the end, `Line`.
|
|
The returned string cannot be empty; it will have
|
|
at least one letter.
|
|
"""
|
|
for i in range(1, len(name) + 1):
|
|
if name[-i] not in "1234567890":
|
|
return name[: len(name) - (i - 1)]
|
|
return name
|
|
|
|
|
|
getRealName = get_real_name
|
|
|
|
|
|
def get_type(obj):
|
|
"""Return a string indicating the type of the given object.
|
|
|
|
Parameters
|
|
----------
|
|
obj : App::DocumentObject
|
|
Any type of scripted object created with Draft,
|
|
or any other workbench.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
If `obj` has a `Proxy`, it will return the value of `obj.Proxy.Type`.
|
|
|
|
* If `obj` is a `Part.Shape`, returns `'Shape'`
|
|
|
|
* If `obj` has a `TypeId`, returns `obj.TypeId`
|
|
|
|
In other cases, it will return `'Unknown'`,
|
|
or `None` if `obj` is `None`.
|
|
"""
|
|
import Part
|
|
|
|
if not obj:
|
|
return None
|
|
if isinstance(obj, Part.Shape):
|
|
return "Shape"
|
|
if hasattr(obj, "Class") and "Ifc" in str(obj.Class):
|
|
return obj.Class
|
|
if hasattr(obj, "Proxy") and hasattr(obj.Proxy, "Type"):
|
|
return obj.Proxy.Type
|
|
if hasattr(obj, "TypeId"):
|
|
return obj.TypeId
|
|
return "Unknown"
|
|
|
|
|
|
getType = get_type
|
|
|
|
|
|
def get_objects_of_type(objects, typ):
|
|
"""Return only the objects that match the type in the list of objects.
|
|
|
|
Parameters
|
|
----------
|
|
objects : list of App::DocumentObject
|
|
A list of objects which will be tested.
|
|
|
|
typ : str
|
|
A string that indicates a type. This should be one of the types
|
|
that can be returned by `get_type`.
|
|
|
|
Returns
|
|
-------
|
|
list of objects
|
|
Only the objects that match `typ` will be added to the output list.
|
|
"""
|
|
objs = []
|
|
for o in objects:
|
|
if getType(o) == typ:
|
|
objs.append(o)
|
|
return objs
|
|
|
|
|
|
getObjectsOfType = get_objects_of_type
|
|
|
|
|
|
def is_clone(obj, objtype=None, recursive=False):
|
|
"""Return True if the given object is a clone of a certain type.
|
|
|
|
A clone is of type `'Clone'`, and has a reference
|
|
to the original object inside its `Objects` attribute,
|
|
which is an `'App::PropertyLinkListGlobal'`.
|
|
|
|
The `Objects` attribute can point to another `'Clone'` object.
|
|
If `recursive` is `True`, the function will be called recursively
|
|
to further test this clone, until the type of the original object
|
|
can be compared to `objtype`.
|
|
|
|
Parameters
|
|
----------
|
|
obj : App::DocumentObject
|
|
The clone object that will be tested for a certain type.
|
|
|
|
objtype : str or list of str
|
|
A type string such as one obtained from `get_type`.
|
|
Or a list of such types.
|
|
|
|
recursive : bool, optional
|
|
It defaults to `False`.
|
|
If it is `True`, this same function will be called recursively
|
|
with `obj.Object[0]` as input.
|
|
|
|
This option only works if `obj.Object[0]` is of type `'Clone'`,
|
|
that is, if `obj` is a clone of a clone.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
Returns `True` if `obj` is of type `'Clone'`,
|
|
and `obj.Object[0]` is of type `objtype`.
|
|
|
|
If `objtype` is a list, then `obj.Objects[0]`
|
|
will be tested against each of the elements in the list,
|
|
and it will return `True` if at least one element matches the type.
|
|
|
|
If `obj` isn't of type `'Clone'` but has the `CloneOf` attribute,
|
|
it will also return `True`.
|
|
|
|
It returns `False` otherwise, for example,
|
|
if `obj` is not even a clone.
|
|
"""
|
|
if isinstance(objtype, list):
|
|
return any([is_clone(obj, t, recursive) for t in objtype])
|
|
if getType(obj) == "Clone":
|
|
if len(obj.Objects) == 1:
|
|
if objtype:
|
|
if getType(obj.Objects[0]) == objtype:
|
|
return True
|
|
elif recursive and (getType(obj.Objects[0]) == "Clone"):
|
|
return is_clone(obj.Objects[0], objtype, recursive)
|
|
elif hasattr(obj, "CloneOf"):
|
|
if obj.CloneOf:
|
|
if objtype:
|
|
if getType(obj.CloneOf) == objtype:
|
|
return True
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
|
|
isClone = is_clone
|
|
|
|
|
|
def get_clone_base(obj, strict=False, recursive=True):
|
|
"""Return the object cloned by this object, if any.
|
|
|
|
Parameters
|
|
----------
|
|
obj: App::DocumentObject
|
|
Any type of object.
|
|
|
|
strict: bool, optional
|
|
It defaults to `False`.
|
|
If it is `True`, and this object is not a clone,
|
|
this function will return `False`.
|
|
|
|
recursive: bool, optional
|
|
It defaults to `True`
|
|
If it is `True`, it call recursively to itself to
|
|
get base object and if it is `False` then it just
|
|
return base object, not call recursively to find
|
|
base object.
|
|
|
|
Returns
|
|
-------
|
|
App::DocumentObject
|
|
It `obj` is a `Draft Clone`, it will return the first object
|
|
that is in its `Objects` property.
|
|
|
|
If `obj` has a `CloneOf` property, it will search iteratively
|
|
inside the object pointed to by this property.
|
|
|
|
obj
|
|
If `obj` is not a `Draft Clone`, nor it has a `CloneOf` property,
|
|
it will return the same `obj`, as long as `strict` is `False`.
|
|
|
|
False
|
|
It will return `False` if `obj` is not a clone,
|
|
and `strict` is `True`.
|
|
"""
|
|
if hasattr(obj, "CloneOf") and obj.CloneOf:
|
|
if recursive:
|
|
return get_clone_base(obj.CloneOf)
|
|
return obj.CloneOf
|
|
if get_type(obj) == "Clone" and obj.Objects:
|
|
if recursive:
|
|
return get_clone_base(obj.Objects[0])
|
|
return obj.Objects[0]
|
|
if strict:
|
|
return False
|
|
return obj
|
|
|
|
|
|
getCloneBase = get_clone_base
|
|
|
|
|
|
def shapify(obj, delete=True):
|
|
"""Transform a parametric object into a static, non-parametric shape.
|
|
|
|
Parameters
|
|
----------
|
|
obj : App::DocumentObject
|
|
Any type of scripted object.
|
|
|
|
This object will be removed, and a non-parametric object
|
|
with the same topological shape (`Part::TopoShape`)
|
|
will be created.
|
|
|
|
delete: bool, optional
|
|
It defaults to `False`.
|
|
If it is `True`, the original object is deleted.
|
|
|
|
Returns
|
|
-------
|
|
Part::Feature
|
|
The new object that takes `obj.Shape` as its own.
|
|
|
|
Depending on the contents of the Shape, the resulting object
|
|
will be named `'Face'`, `'Solid'`, `'Compound'`,
|
|
`'Shell'`, `'Wire'`, `'Line'`, `'Circle'`,
|
|
or the name returned by `get_real_name(obj.Name)`.
|
|
|
|
If there is a problem with `obj.Shape`, it will return `None`,
|
|
and the original object will not be modified.
|
|
"""
|
|
try:
|
|
shape = obj.Shape
|
|
except Exception:
|
|
return None
|
|
|
|
if len(shape.Faces) == 1:
|
|
name = "Face"
|
|
elif len(shape.Solids) == 1:
|
|
name = "Solid"
|
|
elif len(shape.Solids) > 1:
|
|
name = "Compound"
|
|
elif len(shape.Faces) > 1:
|
|
name = "Shell"
|
|
elif len(shape.Wires) == 1:
|
|
name = "Wire"
|
|
elif len(shape.Edges) == 1:
|
|
import DraftGeomUtils
|
|
|
|
if DraftGeomUtils.geomType(shape.Edges[0]) == "Line":
|
|
name = "Line"
|
|
else:
|
|
name = "Circle"
|
|
else:
|
|
name = getRealName(obj.Name)
|
|
|
|
if delete:
|
|
App.ActiveDocument.removeObject(obj.Name)
|
|
newobj = App.ActiveDocument.addObject("Part::Feature", name)
|
|
newobj.Shape = shape
|
|
|
|
return newobj
|
|
|
|
|
|
def print_shape(shape):
|
|
"""Print detailed information of a topological shape.
|
|
|
|
Parameters
|
|
----------
|
|
shape : Part::TopoShape
|
|
Any topological shape in an object, usually obtained from `obj.Shape`.
|
|
"""
|
|
_msg(translate("draft", "Solids:") + " {}".format(len(shape.Solids)))
|
|
_msg(translate("draft", "Faces:") + " {}".format(len(shape.Faces)))
|
|
_msg(translate("draft", "Wires:") + " {}".format(len(shape.Wires)))
|
|
_msg(translate("draft", "Edges:") + " {}".format(len(shape.Edges)))
|
|
_msg(translate("draft", "Vertices:") + " {}".format(len(shape.Vertexes)))
|
|
|
|
if shape.Faces:
|
|
for f in range(len(shape.Faces)):
|
|
_msg(translate("draft", "Face") + " {}:".format(f))
|
|
for v in shape.Faces[f].Vertexes:
|
|
_msg(" {}".format(v.Point))
|
|
elif shape.Wires:
|
|
for w in range(len(shape.Wires)):
|
|
_msg(translate("draft", "Wire") + " {}:".format(w))
|
|
for v in shape.Wires[w].Vertexes:
|
|
_msg(" {}".format(v.Point))
|
|
else:
|
|
for v in shape.Vertexes:
|
|
_msg(" {}".format(v.Point))
|
|
|
|
|
|
printShape = print_shape
|
|
|
|
|
|
def compare_objects(obj1, obj2):
|
|
"""Print the differences between 2 objects.
|
|
|
|
The two objects are compared through their `TypeId` attribute,
|
|
or by using the `get_type` function.
|
|
|
|
If they are the same type their properties are compared
|
|
looking for differences.
|
|
|
|
Neither `Shape` nor `Label` attributes are compared.
|
|
|
|
Parameters
|
|
----------
|
|
obj1 : App::DocumentObject
|
|
Any type of scripted object.
|
|
obj2 : App::DocumentObject
|
|
Any type of scripted object.
|
|
"""
|
|
if obj1.TypeId != obj2.TypeId:
|
|
_msg(
|
|
"'{0}' ({1}), '{2}' ({3}): ".format(obj1.Name, obj1.TypeId, obj2.Name, obj2.TypeId)
|
|
+ translate("draft", "different types")
|
|
+ " (TypeId)"
|
|
)
|
|
elif getType(obj1) != getType(obj2):
|
|
_msg(
|
|
"'{0}' ({1}), '{2}' ({3}): ".format(
|
|
obj1.Name, get_type(obj1), obj2.Name, get_type(obj2)
|
|
)
|
|
+ translate("draft", "different types")
|
|
+ " (Proxy.Type)"
|
|
)
|
|
else:
|
|
for p in obj1.PropertiesList:
|
|
if p in obj2.PropertiesList:
|
|
if p in ("Shape", "Label"):
|
|
pass
|
|
elif p == "Placement":
|
|
delta = obj1.Placement.Base.sub(obj2.Placement.Base)
|
|
text = translate(
|
|
"draft",
|
|
"Objects have different placements. "
|
|
"Distance between the two base points:",
|
|
)
|
|
_msg(text + " " + str(delta.Length))
|
|
else:
|
|
if getattr(obj1, p) != getattr(obj2, p):
|
|
_msg("'{}' ".format(p) + translate("draft", "has a different value"))
|
|
else:
|
|
_msg("{} ".format(p) + translate("draft", "doesn't exist in one of the objects"))
|
|
|
|
|
|
compareObjects = compare_objects
|
|
|
|
|
|
def load_svg_patterns():
|
|
"""Load the default Draft SVG patterns and user defined patterns.
|
|
|
|
The SVG patterns are added as a dictionary to the `App.svgpatterns`
|
|
attribute.
|
|
"""
|
|
import importSVG
|
|
|
|
App.svgpatterns = {}
|
|
|
|
# Get default patterns in the resource file
|
|
patfiles = QtCore.QDir(":/patterns").entryList()
|
|
for fn in patfiles:
|
|
file = ":/patterns/" + str(fn)
|
|
f = QtCore.QFile(file)
|
|
f.open(QtCore.QIODevice.ReadOnly)
|
|
p = importSVG.getContents(str(f.readAll()), "pattern", True)
|
|
if p:
|
|
for k in p:
|
|
p[k] = [p[k], file]
|
|
App.svgpatterns.update(p)
|
|
|
|
# Get patterns in a user defined file
|
|
altpat = params.get_param("patternFile")
|
|
if os.path.isdir(altpat):
|
|
for f in os.listdir(altpat):
|
|
if f[-4:].upper() == ".SVG":
|
|
file = os.path.join(altpat, f)
|
|
p = importSVG.getContents(file, "pattern")
|
|
if p:
|
|
for k in p:
|
|
p[k] = [p[k], file]
|
|
App.svgpatterns.update(p)
|
|
|
|
# Get TechDraw patterns
|
|
altpat = os.path.join(App.getResourceDir(), "Mod", "TechDraw", "Patterns")
|
|
if os.path.isdir(altpat):
|
|
for f in os.listdir(altpat):
|
|
if f[-4:].upper() == ".SVG":
|
|
file = os.path.join(altpat, f)
|
|
p = importSVG.getContents(file, "pattern")
|
|
if p:
|
|
for k in p:
|
|
p[k] = [p[k], file]
|
|
else:
|
|
# some TD pattern files have no <pattern> definition but can still be used by Draft
|
|
p = {f[:-4]: ["<pattern></pattern>", file]}
|
|
App.svgpatterns.update(p)
|
|
|
|
|
|
loadSvgPatterns = load_svg_patterns
|
|
|
|
|
|
def svg_patterns():
|
|
"""Return a dictionary with installed SVG patterns.
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Returns `App.svgpatterns` if it exists.
|
|
Otherwise it calls `load_svg_patterns` to create it
|
|
before returning it.
|
|
"""
|
|
if hasattr(App, "svgpatterns"):
|
|
return App.svgpatterns
|
|
else:
|
|
loadSvgPatterns()
|
|
if hasattr(App, "svgpatterns"):
|
|
return App.svgpatterns
|
|
return {}
|
|
|
|
|
|
svgpatterns = svg_patterns
|
|
|
|
|
|
def get_rgb(color, testbw=True):
|
|
"""Return an RRGGBB value #000000 from a FreeCAD color.
|
|
|
|
Parameters
|
|
----------
|
|
color : list or tuple with RGB values
|
|
The values must be in the 0.0-1.0 range.
|
|
testwb : bool (default = True)
|
|
Pure white will be converted into pure black.
|
|
"""
|
|
r = str(hex(int(color[0] * 255)))[2:].zfill(2)
|
|
g = str(hex(int(color[1] * 255)))[2:].zfill(2)
|
|
b = str(hex(int(color[2] * 255)))[2:].zfill(2)
|
|
col = "#" + r + g + b
|
|
if testbw:
|
|
if col == "#ffffff":
|
|
# print(params.get_param("SvgLinesBlack"))
|
|
if params.get_param("SvgLinesBlack"):
|
|
col = "#000000"
|
|
return col
|
|
|
|
|
|
getrgb = get_rgb
|
|
|
|
|
|
def argb_to_rgba(color):
|
|
"""Change byte order of a 4 byte color int from ARGB (Qt) to RGBA (FreeCAD).
|
|
|
|
Alpha in both integers should always be 255.
|
|
|
|
Alpha in color properties is not used in the 3D view, but is shown in the
|
|
color swatches in the Property editor. It therefore better to ensure alpha
|
|
is 255 (version 1.1 dev cycle).
|
|
|
|
Usage:
|
|
|
|
qt_int = self.form.ShapeColor.property("color").rgba() # Note: returns ARGB int
|
|
qt_int = self.form.ShapeColor.property("color").rgb() # Note: returns ARGB int
|
|
fc_int = argb_to_rgba(qt_int)
|
|
|
|
FreeCAD.ParamGet("User parameter:BaseApp/Preferences/View")\
|
|
.SetUnsigned("DefaultShapeColor", fc_int)
|
|
|
|
obj.ViewObject.ShapeColor = fc_int | 0x000000FF
|
|
|
|
Related:
|
|
|
|
getRgbF() returns an RGBA tuple. 4 floats in the range 0.0 - 1.0. Alpha is always 1.0.
|
|
"""
|
|
return ((color & 0xFFFFFF) << 8) + ((color & 0xFF000000) >> 24)
|
|
|
|
|
|
def rgba_to_argb(color):
|
|
"""Change byte order of a 4 byte color int from RGBA (FreeCAD) to ARGB (Qt)."""
|
|
return ((color & 0xFFFFFF00) >> 8) + ((color & 0xFF) << 24)
|
|
|
|
|
|
def get_rgba_tuple(color, typ=1.0):
|
|
"""Return an RGBA tuple.
|
|
|
|
Parameters
|
|
----------
|
|
color: int
|
|
RGBA integer.
|
|
typ: any float (default = 1.0) or int (use 255)
|
|
If float the values in the returned tuple are in the 0.0-1.0 range.
|
|
Else the values are in the 0-255 range.
|
|
"""
|
|
color = ((color >> 24) & 0xFF, (color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF)
|
|
if type(typ) == float:
|
|
return tuple([x / 255.0 for x in color])
|
|
else:
|
|
return color
|
|
|
|
|
|
def _modifiers_process_subselection(sels, copy):
|
|
data_list = []
|
|
sel_info = []
|
|
for sel in sels:
|
|
for sub in sel.SubElementNames if sel.SubElementNames else [""]:
|
|
if not ("Vertex" in sub or "Edge" in sub):
|
|
continue
|
|
if copy and "Vertex" in sub:
|
|
continue
|
|
obj = sel.Object.getSubObject(sub, 1)
|
|
pla = sel.Object.getSubObject(sub, 3)
|
|
if "Vertex" in sub:
|
|
vert_idx = int(sub.rpartition("Vertex")[2]) - 1
|
|
edge_idx = -1
|
|
else:
|
|
vert_idx = -1
|
|
edge_idx = int(sub.rpartition("Edge")[2]) - 1
|
|
data_list.append((obj, vert_idx, edge_idx, pla))
|
|
sel_info.append(("", sel.Object.Name, sub))
|
|
return data_list, sel_info
|
|
|
|
|
|
def _modifiers_process_selection(sels, copy, scale=False, add_movable_children=False):
|
|
# Only when creating ghosts and if copy is False, should add_movable_children be True.
|
|
objects = []
|
|
places = []
|
|
sel_info = []
|
|
for sel in sels:
|
|
for sub in sel.SubElementNames if sel.SubElementNames else [""]:
|
|
obj = sel.Object.getSubObject(sub, 1)
|
|
# Get the global placement of the parent:
|
|
if obj == sel.Object:
|
|
pla = App.Placement()
|
|
else:
|
|
pla = sel.Object.getSubObject(sub.rpartition(obj.Name)[0], 3)
|
|
objs = _modifiers_get_group_contents(obj)
|
|
if add_movable_children:
|
|
children = []
|
|
for obj in objs:
|
|
children.extend(_modifiers_get_movable_children(obj))
|
|
objs.extend(children)
|
|
objs = _modifiers_filter_objects(objs, copy, scale)
|
|
objects.extend(objs)
|
|
places.extend(len(objs) * [pla])
|
|
if "." in sub:
|
|
sub = sub.rpartition(".")[0] + "."
|
|
elif "Face" in sub or "Edge" in sub or "Vertex" in sub:
|
|
sub = ""
|
|
sel_info.append(("", sel.Object.Name, sub))
|
|
return objects, places, sel_info
|
|
|
|
|
|
def _modifiers_get_group_contents(obj):
|
|
from draftutils import groups
|
|
|
|
return groups.get_group_contents(obj, addgroups=True, spaces=True, noarchchild=True)
|
|
|
|
|
|
def _modifiers_get_movable_children(obj):
|
|
result = []
|
|
if hasattr(obj, "Proxy") and hasattr(obj.Proxy, "getMovableChildren"):
|
|
children = obj.Proxy.getMovableChildren(obj)
|
|
result.extend(children)
|
|
for child in children:
|
|
result.extend(_modifiers_get_movable_children(child))
|
|
return result
|
|
|
|
|
|
def _modifiers_filter_objects(objs, copy, scale=False):
|
|
|
|
def is_scalable(obj):
|
|
if hasattr(obj, "Placement") and hasattr(obj, "Shape"):
|
|
return True
|
|
if obj.isDerivedFrom("App::DocumentObjectGroup"):
|
|
return True
|
|
if obj.isDerivedFrom("App::Annotation"):
|
|
return True
|
|
if obj.isDerivedFrom("Image::ImagePlane"):
|
|
return True
|
|
return False
|
|
|
|
result = []
|
|
for obj in objs:
|
|
if not copy and hasattr(obj, "MoveBase") and obj.MoveBase and obj.Base:
|
|
parents = []
|
|
for parent in obj.Base.InList:
|
|
if parent.isDerivedFrom("Part::Feature"):
|
|
parents.append(parent.Name)
|
|
if len(parents) > 1:
|
|
message = translate(
|
|
"draft",
|
|
"%s shares a base with %d other objects. Please check if you want to modify this.",
|
|
) % (obj.Name, len(parents) - 1)
|
|
_err(message)
|
|
if not scale or utils.get_type(obj.Base) == "Wire":
|
|
result.append(obj.Base)
|
|
elif (
|
|
not copy and hasattr(obj, "Placement") and "ReadOnly" in obj.getEditorMode("Placement")
|
|
):
|
|
_err(
|
|
translate("draft", "%s cannot be modified because its placement is readonly")
|
|
% obj.Name
|
|
)
|
|
elif not scale or is_scalable(obj):
|
|
result.append(obj)
|
|
return result
|
|
|
|
|
|
def is_closed_edge(edge_index, object):
|
|
return edge_index + 1 >= len(object.Points)
|
|
|
|
|
|
def utf8_decode(text):
|
|
r"""Decode the input string and return a unicode string.
|
|
|
|
Python 2:
|
|
::
|
|
str -> unicode
|
|
unicode -> unicode
|
|
|
|
Python 3:
|
|
::
|
|
str -> str
|
|
bytes -> str
|
|
|
|
It runs
|
|
::
|
|
try:
|
|
return text.decode("utf-8")
|
|
except AttributeError:
|
|
return text
|
|
|
|
Parameters
|
|
----------
|
|
text : str, unicode or bytes
|
|
A str, unicode, or bytes object that may have unicode characters
|
|
like accented characters.
|
|
|
|
In Python 2, a `bytes` object can include accented characters,
|
|
but in Python 3 it must only contain ASCII literal characters.
|
|
|
|
Returns
|
|
-------
|
|
unicode or str
|
|
In Python 2 it will try decoding the `bytes` string
|
|
and return a `'utf-8'` decoded string.
|
|
|
|
>>> "Aá".decode("utf-8")
|
|
>>> b"Aá".decode("utf-8")
|
|
u'A\xe1'
|
|
|
|
In Python 2 the unicode string is prefixed with `u`,
|
|
and unicode characters are replaced by their two-digit hexadecimal
|
|
representation, or four digit unicode escape.
|
|
|
|
>>> "AáBẃCñ".decode("utf-8")
|
|
u'A\xe1B\u1e83C\xf1'
|
|
|
|
In Python 2 it will always return a `unicode` object.
|
|
|
|
In Python 3 a regular string is already unicode encoded,
|
|
so strings have no `decode` method. In this case, `text`
|
|
will be returned as is.
|
|
|
|
In Python 3, if `text` is a `bytes` object, then it will be converted
|
|
to `str`; in this case, the `bytes` object cannot have accents,
|
|
it must only contain ASCII literal characters.
|
|
|
|
>>> b"ABC".decode("utf-8")
|
|
'ABC'
|
|
|
|
In Python 3 it will always return a `str` object, with no prefix.
|
|
"""
|
|
try:
|
|
return text.decode("utf-8")
|
|
except AttributeError:
|
|
return text
|
|
|
|
|
|
def print_header(name, description, debug=True):
|
|
"""Print a line to the console when something is called, and log it.
|
|
|
|
Parameters
|
|
----------
|
|
name: str
|
|
The name of the function or class that is being called.
|
|
This `name` will be logged in the log file, so if there are problems
|
|
the log file can be investigated for clues.
|
|
|
|
description: str
|
|
Arbitrary text that will be printed to the console
|
|
when the function or class is called.
|
|
|
|
debug: bool, optional
|
|
It defaults to `True`.
|
|
If it is `False` the `description` will not be printed
|
|
to the console.
|
|
On the other hand the `name` will always be logged.
|
|
"""
|
|
_log(name)
|
|
if debug:
|
|
_msg(16 * "-")
|
|
_msg(description)
|
|
|
|
|
|
def find_doc(doc=None):
|
|
"""Return the active document or find a document by name.
|
|
|
|
Parameters
|
|
----------
|
|
doc: App::Document or str, optional
|
|
The document that will be searched in the session.
|
|
It defaults to `None`, in which case it tries to find
|
|
the active document.
|
|
If `doc` is a string, it will try to get the document by `Name`.
|
|
|
|
Returns
|
|
-------
|
|
bool, App::Document
|
|
A tuple containing the information on whether the search
|
|
was successful. In this case, the boolean is `True`,
|
|
and the second value is the document instance.
|
|
|
|
False, None
|
|
If there is no active document, or the string in `doc`
|
|
doesn't correspond to an open document in the session.
|
|
"""
|
|
FOUND = True
|
|
|
|
if not doc:
|
|
doc = App.activeDocument()
|
|
if not doc:
|
|
return not FOUND, None
|
|
|
|
if isinstance(doc, str):
|
|
try:
|
|
doc = App.getDocument(doc)
|
|
except NameError:
|
|
_err(translate("draft", "Wrong input: unknown document {}").format(doc))
|
|
return not FOUND, None
|
|
|
|
return FOUND, doc
|
|
|
|
|
|
def find_object(obj, doc=None):
|
|
"""Find object in the document, inclusive by Label.
|
|
|
|
Parameters
|
|
----------
|
|
obj: App::DocumentObject or str
|
|
The object to search in `doc`.
|
|
Or if the `obj` is a string, it will search the object by `Label`.
|
|
Since Labels are not guaranteed to be unique, it will get the first
|
|
object with that label in the document.
|
|
|
|
doc: App::Document or str, optional
|
|
The document in which the object will be searched.
|
|
It defaults to `None`, in which case it tries to search in the
|
|
active document.
|
|
If `doc` is a string, it will search the document by `Name`.
|
|
|
|
Returns
|
|
-------
|
|
bool, App::DocumentObject
|
|
A tuple containing the information on whether the search
|
|
was successful. In this case, the boolean is `True`,
|
|
and the second value is the object found.
|
|
|
|
False, None
|
|
If the object doesn't exist in the document.
|
|
"""
|
|
FOUND = True
|
|
|
|
found, doc = find_doc(doc)
|
|
if not found:
|
|
_err(translate("draft", "No active document. Aborting."))
|
|
return not FOUND, None
|
|
|
|
if isinstance(obj, str):
|
|
try:
|
|
obj = doc.getObjectsByLabel(obj)[0]
|
|
except IndexError:
|
|
return not FOUND, None
|
|
|
|
if obj not in doc.Objects:
|
|
return not FOUND, None
|
|
|
|
return FOUND, obj
|
|
|
|
|
|
def use_instead(function, version=""):
|
|
"""Print a deprecation message and suggest another function.
|
|
|
|
This function must be used inside the definition of a function
|
|
that has been considered for deprecation, so we must provide
|
|
an alternative.
|
|
::
|
|
def old_function():
|
|
use_instead('new_function', 1.0)
|
|
|
|
def someFunction():
|
|
use_instead('some_function')
|
|
|
|
Parameters
|
|
----------
|
|
function: str
|
|
The name of the function to use instead of the current one.
|
|
|
|
version: float or str, optional
|
|
It defaults to the empty string `''`.
|
|
The version where this command is to be deprecated, if it is known.
|
|
If we don't know when this command will be deprecated
|
|
then we should not give a version.
|
|
"""
|
|
if version:
|
|
_wrn(
|
|
translate("draft", "This function will be deprecated in {}. Please use '{}'.").format(
|
|
version, function
|
|
)
|
|
)
|
|
else:
|
|
_wrn(
|
|
translate("draft", "This function will be deprecated. Please use '{}'.").format(
|
|
function
|
|
)
|
|
)
|
|
|
|
|
|
def pyopen(
|
|
file,
|
|
mode="r",
|
|
buffering=-1,
|
|
encoding=None,
|
|
errors=None,
|
|
newline=None,
|
|
closefd=True,
|
|
opener=None,
|
|
):
|
|
if encoding is None:
|
|
encoding = "utf-8"
|
|
return open(file, mode, buffering, encoding, errors, newline, closefd, opener)
|
|
|
|
|
|
## @}
|