1840 lines
63 KiB
Python
1840 lines
63 KiB
Python
# ***************************************************************************
|
|
# * Copyright (c) 2009, 2010 Ken Cline <cline@frii.com> *
|
|
# * Copyright (c) 2023 FreeCAD Project Association *
|
|
# * *
|
|
# * 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 *
|
|
# * *
|
|
# ***************************************************************************
|
|
"""Provide the working plane code and utilities for the Draft Workbench.
|
|
|
|
This module provides the plane class which provides a virtual working plane
|
|
in FreeCAD and a couple of utility functions.
|
|
The working plane is mostly intended to be used in the Draft Workbench
|
|
to draw 2D objects in various orientations, not only in the standard XY,
|
|
YZ, and XZ planes.
|
|
"""
|
|
## @package WorkingPlane
|
|
# \ingroup DRAFT
|
|
# \brief This module handles the Working Plane and grid of the Draft module.
|
|
#
|
|
# This module provides the plane class which provides a virtual working plane
|
|
# in FreeCAD and a couple of utility functions.
|
|
|
|
import math
|
|
import lazy_loader.lazy_loader as lz
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
|
|
import FreeCAD
|
|
import DraftVecUtils
|
|
from FreeCAD import Vector
|
|
from draftutils import gui_utils
|
|
from draftutils import params
|
|
from draftutils import utils
|
|
from draftutils.messages import _wrn
|
|
from draftutils.translate import translate
|
|
|
|
DraftGeomUtils = lz.LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
|
|
Part = lz.LazyLoader("Part", globals(), "Part")
|
|
FreeCADGui = lz.LazyLoader("FreeCADGui", globals(), "FreeCADGui")
|
|
|
|
__title__ = "FreeCAD Working Plane utility"
|
|
__author__ = "Ken Cline"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
|
|
class PlaneBase:
|
|
"""PlaneBase is the base class for the Plane class and the PlaneGui class.
|
|
|
|
Parameters
|
|
----------
|
|
u: Base.Vector or WorkingPlane.PlaneBase, optional
|
|
Defaults to Vector(1, 0, 0).
|
|
If a WP is provided:
|
|
A copy of the WP is created, all other parameters are then ignored.
|
|
If a vector is provided:
|
|
Unit vector for the `u` attribute (+X axis).
|
|
|
|
v: Base.Vector, optional
|
|
Defaults to Vector(0, 1, 0).
|
|
Unit vector for the `v` attribute (+Y axis).
|
|
|
|
w: Base.Vector, optional
|
|
Defaults to Vector(0, 0, 1).
|
|
Unit vector for the `axis` attribute (+Z axis).
|
|
|
|
pos: Base.Vector, optional
|
|
Defaults to Vector(0, 0, 0).
|
|
Vector for the `position` attribute (origin).
|
|
|
|
Note that the u, v and w vectors are not checked for validity.
|
|
"""
|
|
|
|
def __init__(self,
|
|
u=Vector(1, 0, 0), v=Vector(0, 1, 0), w=Vector(0, 0, 1),
|
|
pos=Vector(0, 0, 0)):
|
|
|
|
if isinstance(u, PlaneBase):
|
|
self.match(u)
|
|
return
|
|
self.u = Vector(u)
|
|
self.v = Vector(v)
|
|
self.axis = Vector(w)
|
|
self.position = Vector(pos)
|
|
|
|
def __repr__(self):
|
|
text = "Workplane"
|
|
text += " x=" + str(DraftVecUtils.rounded(self.u))
|
|
text += " y=" + str(DraftVecUtils.rounded(self.v))
|
|
text += " z=" + str(DraftVecUtils.rounded(self.axis))
|
|
text += " pos=" + str(DraftVecUtils.rounded(self.position))
|
|
return text
|
|
|
|
def copy(self):
|
|
"""Return a new WP that is a copy of the present object."""
|
|
wp = PlaneBase()
|
|
self.match(source=self, target=wp)
|
|
return wp
|
|
|
|
def _copy_value(self, val):
|
|
"""Return a copy of a value, primarily required for vectors."""
|
|
return val.__class__(val)
|
|
|
|
def match(self, source, target=None):
|
|
"""Match the main properties of two working planes.
|
|
|
|
Parameters
|
|
----------
|
|
source: WP object
|
|
WP to copy properties from.
|
|
target: WP object, optional
|
|
Defaults to `None`.
|
|
WP to copy properties to. If `None` the present object is used.
|
|
"""
|
|
if target is None:
|
|
target = self
|
|
for prop in self._get_prop_list():
|
|
setattr(target, prop, self._copy_value(getattr(source, prop)))
|
|
|
|
def get_parameters(self):
|
|
"""Return a data dictionary with the main properties of the WP."""
|
|
data = {}
|
|
for prop in self._get_prop_list():
|
|
data[prop] = self._copy_value(getattr(self, prop))
|
|
return data
|
|
|
|
def set_parameters(self, data):
|
|
"""Set the main properties of the WP according to a data dictionary."""
|
|
for prop in self._get_prop_list():
|
|
setattr(self, prop, self._copy_value(data[prop]))
|
|
|
|
def align_to_3_points(self, p1, p2, p3, offset=0):
|
|
"""Align the WP to 3 points with an optional offset.
|
|
|
|
The points must define a plane.
|
|
|
|
Parameters
|
|
----------
|
|
p1: Base.Vector
|
|
New WP `position`.
|
|
p2: Base.Vector
|
|
Point on the +X axis. (p2 - p1) defines the WP `u` axis.
|
|
p3: Base.Vector
|
|
Defines the plane.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
return self.align_to_edge_or_wire(Part.makePolygon([p1, p2, p3]), offset)
|
|
|
|
def align_to_edges_vertexes(self, shapes, offset=0):
|
|
"""Align the WP to the endpoints of edges and/or the points of vertexes
|
|
with an optional offset.
|
|
|
|
The points must define a plane.
|
|
|
|
The first 2 points define the WP `position` and `u` axis.
|
|
|
|
Parameters
|
|
----------
|
|
shapes: iterable
|
|
One or more edges and/or vertexes.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
points = [vert.Point for shape in shapes for vert in shape.Vertexes]
|
|
if len(points) < 2:
|
|
return False
|
|
return self.align_to_edge_or_wire(Part.makePolygon(points), offset)
|
|
|
|
def align_to_edge_or_wire(self, shape, offset=0):
|
|
"""Align the WP to an edge or wire with an optional offset.
|
|
|
|
The shape must define a plane.
|
|
|
|
If the shape is an edge with a `Center` then that defines the WP
|
|
`position`. The vector between the center and its start point then
|
|
defines the WP `u` axis.
|
|
|
|
In other cases the start point of the first edge defines the WP
|
|
`position` and the 1st derivative at that point the WP `u` axis.
|
|
|
|
Parameters
|
|
----------
|
|
shape: Part.Edge or Part.Wire
|
|
Edge or wire.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
tol = 1e-7
|
|
plane = shape.findPlane()
|
|
if plane is None:
|
|
return False
|
|
self.axis = plane.Axis
|
|
if shape.ShapeType == "Edge" and hasattr(shape.Curve, "Center"):
|
|
pos = shape.Curve.Center
|
|
vec = shape.Vertexes[0].Point - pos
|
|
if vec.Length > tol:
|
|
self.u = vec
|
|
self.u.normalize()
|
|
self.v = self.axis.cross(self.u)
|
|
else:
|
|
self.u, self.v, _ = self._axes_from_rotation(plane.Rotation)
|
|
elif shape.Edges[0].Length > tol:
|
|
pos = shape.Vertexes[0].Point
|
|
self.u = shape.Edges[0].derivative1At(0)
|
|
self.u.normalize()
|
|
self.v = self.axis.cross(self.u)
|
|
else:
|
|
pos = shape.Vertexes[0].Point
|
|
self.u, self.v, _ = self._axes_from_rotation(plane.Rotation)
|
|
self.position = pos + (self.axis * offset)
|
|
return True
|
|
|
|
def align_to_face(self, shape, offset=0):
|
|
"""Align the WP to a face with an optional offset.
|
|
|
|
The face must be planar.
|
|
|
|
The center of gravity of the face defines the WP `position` and the
|
|
normal of the face the WP `axis`. The WP `u` and `v` vectors are
|
|
determined by the DraftGeomUtils.uv_vectors_from_face function.
|
|
See there.
|
|
|
|
Parameters
|
|
----------
|
|
shape: Part.Face
|
|
Face.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
if shape.Surface.isPlanar() is False:
|
|
return False
|
|
place = DraftGeomUtils.placement_from_face(shape)
|
|
self.u, self.v, self.axis = self._axes_from_rotation(place.Rotation)
|
|
self.position = place.Base + (self.axis * offset)
|
|
return True
|
|
|
|
def align_to_placement(self, place, offset=0):
|
|
"""Align the WP to a placement with an optional offset.
|
|
|
|
Parameters
|
|
----------
|
|
place: Base.Placement
|
|
Placement.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`True`
|
|
"""
|
|
self.u, self.v, self.axis = self._axes_from_rotation(place.Rotation)
|
|
self.position = place.Base + (self.axis * offset)
|
|
return True
|
|
|
|
def align_to_point_and_axis(self, point, axis, offset=0, upvec=Vector(1, 0, 0)):
|
|
"""Align the WP to a point and an axis with an optional offset and an
|
|
optional up-vector.
|
|
|
|
If the axis and up-vector are parallel the FreeCAD.Rotation algorithm
|
|
will replace the up-vector: Vector(0, 0, 1) is tried first, then
|
|
Vector(0, 1, 0), and finally Vector(1, 0, 0).
|
|
|
|
Parameters
|
|
----------
|
|
point: Base.Vector
|
|
New WP `position`.
|
|
axis: Base.Vector
|
|
New WP `axis`.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
upvec: Base.Vector, optional
|
|
Defaults to Vector(1, 0, 0).
|
|
Up-vector.
|
|
|
|
Returns
|
|
-------
|
|
`True`
|
|
"""
|
|
tol = 1e-7
|
|
if axis.Length < tol:
|
|
return False
|
|
if upvec.Length < tol:
|
|
return False
|
|
axis = Vector(axis).normalize()
|
|
upvec = Vector(upvec).normalize()
|
|
if axis.isEqual(upvec, tol) or axis.isEqual(upvec.negative(), tol):
|
|
upvec = axis
|
|
rot = FreeCAD.Rotation(Vector(), upvec, axis, "ZYX")
|
|
self.u, self.v, _ = self._axes_from_rotation(rot)
|
|
self.axis = axis
|
|
self.position = point + (self.axis * offset)
|
|
return True
|
|
|
|
def align_to_point_and_axis_svg(self, point, axis, offset=0):
|
|
"""Align the WP to a point and an axis with an optional offset.
|
|
|
|
It aligns `u` and `v` based on the magnitude of the components
|
|
of `axis`.
|
|
|
|
Parameters
|
|
----------
|
|
point: Base.Vector
|
|
The new `position` of the plane, adjusted by
|
|
the `offset`.
|
|
axis: Base.Vector
|
|
A vector whose unit vector will be used as the new `axis`
|
|
of the plane.
|
|
The magnitudes of the `x`, `y`, `z` components of the axis
|
|
determine the orientation of `u` and `v` of the plane.
|
|
offset: float, optional
|
|
Defaults to zero. A value which will be used to offset
|
|
the plane in the direction of its `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`True`
|
|
|
|
Cases
|
|
-----
|
|
The `u` and `v` are always calculated the same
|
|
|
|
* `u` is the cross product of the positive or negative of `axis`
|
|
with a `reference vector`.
|
|
::
|
|
u = [+1|-1] axis.cross(ref_vec)
|
|
* `v` is `u` rotated 90 degrees around `axis`.
|
|
|
|
Whether the `axis` is positive or negative, and which reference
|
|
vector is used, depends on the absolute values of the `x`, `y`, `z`
|
|
components of the `axis` unit vector.
|
|
|
|
#. If `x > y`, and `y > z`
|
|
The reference vector is +Z
|
|
::
|
|
u = -1 axis.cross(+Z)
|
|
#. If `y > z`, and `z >= x`
|
|
The reference vector is +X.
|
|
::
|
|
u = -1 axis.cross(+X)
|
|
#. If `y >= x`, and `x > z`
|
|
The reference vector is +Z.
|
|
::
|
|
u = +1 axis.cross(+Z)
|
|
#. If `x > z`, and `z >= y`
|
|
The reference vector is +Y.
|
|
::
|
|
u = +1 axis.cross(+Y)
|
|
#. If `z >= y`, and `y > x`
|
|
The reference vector is +X.
|
|
::
|
|
u = +1 axis.cross(+X)
|
|
#. otherwise
|
|
The reference vector is +Y.
|
|
::
|
|
u = -1 axis.cross(+Y)
|
|
"""
|
|
self.axis = Vector(axis).normalize()
|
|
ref_vec = Vector(0.0, 1.0, 0.0)
|
|
|
|
if ((abs(axis.x) > abs(axis.y)) and (abs(axis.y) > abs(axis.z))):
|
|
ref_vec = Vector(0.0, 0., 1.0)
|
|
self.u = axis.negative().cross(ref_vec)
|
|
self.u.normalize()
|
|
self.v = DraftVecUtils.rotate(self.u, math.pi/2, self.axis)
|
|
# projcase = "Case new"
|
|
|
|
elif ((abs(axis.y) > abs(axis.z)) and (abs(axis.z) >= abs(axis.x))):
|
|
ref_vec = Vector(1.0, 0.0, 0.0)
|
|
self.u = axis.negative().cross(ref_vec)
|
|
self.u.normalize()
|
|
self.v = DraftVecUtils.rotate(self.u, math.pi/2, self.axis)
|
|
# projcase = "Y>Z, View Y"
|
|
|
|
elif ((abs(axis.y) >= abs(axis.x)) and (abs(axis.x) > abs(axis.z))):
|
|
ref_vec = Vector(0.0, 0., 1.0)
|
|
self.u = axis.cross(ref_vec)
|
|
self.u.normalize()
|
|
self.v = DraftVecUtils.rotate(self.u, math.pi/2, self.axis)
|
|
# projcase = "ehem. XY, Case XY"
|
|
|
|
elif ((abs(axis.x) > abs(axis.z)) and (abs(axis.z) >= abs(axis.y))):
|
|
self.u = axis.cross(ref_vec)
|
|
self.u.normalize()
|
|
self.v = DraftVecUtils.rotate(self.u, math.pi/2, self.axis)
|
|
# projcase = "X>Z, View X"
|
|
|
|
elif ((abs(axis.z) >= abs(axis.y)) and (abs(axis.y) > abs(axis.x))):
|
|
ref_vec = Vector(1.0, 0., 0.0)
|
|
self.u = axis.cross(ref_vec)
|
|
self.u.normalize()
|
|
self.v = DraftVecUtils.rotate(self.u, math.pi/2, self.axis)
|
|
# projcase = "Y>X, Case YZ"
|
|
|
|
else:
|
|
self.u = axis.negative().cross(ref_vec)
|
|
self.u.normalize()
|
|
self.v = DraftVecUtils.rotate(self.u, math.pi/2, self.axis)
|
|
# projcase = "else"
|
|
|
|
# spat_vec = self.u.cross(self.v)
|
|
# spat_res = spat_vec.dot(axis)
|
|
# Console.PrintMessage(projcase + " spat Prod = " + str(spat_res) + "\n")
|
|
|
|
offsetVector = Vector(axis)
|
|
offsetVector.multiply(offset)
|
|
self.position = point.add(offsetVector)
|
|
|
|
return True
|
|
|
|
def get_global_coords(self, point, as_vector=False):
|
|
"""Translate a point or vector from the local (WP) coordinate system to
|
|
the global coordinate system.
|
|
|
|
Parameters
|
|
----------
|
|
point: Base.Vector
|
|
Point.
|
|
as_vector: bool, optional
|
|
Defaults to `False`.
|
|
If `True` treat point as a vector.
|
|
|
|
Returns
|
|
-------
|
|
Base.Vector
|
|
"""
|
|
pos = Vector() if as_vector else self.position
|
|
mtx = FreeCAD.Matrix(self.u, self.v, self.axis, pos)
|
|
return mtx.multVec(point)
|
|
|
|
def get_local_coords(self, point, as_vector=False):
|
|
"""Translate a point or vector from the global coordinate system to
|
|
the local (WP) coordinate system.
|
|
|
|
Parameters
|
|
----------
|
|
point: Base.Vector
|
|
Point.
|
|
as_vector: bool, optional
|
|
Defaults to `False`.
|
|
If `True` treat point as a vector.
|
|
|
|
Returns
|
|
-------
|
|
Base.Vector
|
|
"""
|
|
pos = Vector() if as_vector else self.position
|
|
mtx = FreeCAD.Matrix(self.u, self.v, self.axis, pos)
|
|
return mtx.inverse().multVec(point)
|
|
|
|
def get_closest_axis(self, vec):
|
|
"""Return a string indicating the positive or negative WP axis closest
|
|
to a vector.
|
|
|
|
Parameters
|
|
----------
|
|
vec: Base.Vector
|
|
Vector.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
`"x"`, `"y"` or `"z"`.
|
|
"""
|
|
xyz = list(self.get_local_coords(vec, as_vector=True))
|
|
x, y, z = [abs(coord) for coord in xyz]
|
|
if x >= y and x >= z:
|
|
return "x"
|
|
elif y >= x and y >= z:
|
|
return "y"
|
|
else:
|
|
return "z"
|
|
|
|
def get_placement(self):
|
|
"""Return a placement calculated from the WP."""
|
|
return FreeCAD.Placement(self.position, FreeCAD.Rotation(self.u, self.v, self.axis, "ZYX"))
|
|
|
|
def is_global(self):
|
|
"""Return `True` if the WP matches the global coordinate system exactly."""
|
|
return self.u == Vector(1, 0, 0) \
|
|
and self.v == Vector(0, 1, 0) \
|
|
and self.axis == Vector(0, 0, 1) \
|
|
and self.position == Vector()
|
|
|
|
def is_ortho(self):
|
|
"""Return `True` if all WP axes are parallel to a global axis."""
|
|
rot = FreeCAD.Rotation(self.u, self.v, self.axis, "ZYX")
|
|
ypr = [round(ang, 6) for ang in rot.getYawPitchRoll()]
|
|
return all([ang%90 == 0 for ang in ypr])
|
|
|
|
def project_point(self, point, direction=None, force_projection=True):
|
|
"""Project a point onto the WP and return the global coordinates of the
|
|
projected point.
|
|
|
|
Parameters
|
|
----------
|
|
point: Base.Vector
|
|
Point to project.
|
|
direction: Base.Vector, optional
|
|
Defaults to `None` in which case the WP `axis` is used.
|
|
Direction of projection.
|
|
force_projection: bool, optional
|
|
Defaults to `True`.
|
|
See DraftGeomUtils.project_point_on_plane
|
|
|
|
Returns
|
|
-------
|
|
Base.Vector
|
|
"""
|
|
return DraftGeomUtils.project_point_on_plane(point,
|
|
self.position,
|
|
self.axis,
|
|
direction,
|
|
force_projection)
|
|
|
|
def set_to_top(self, offset=0):
|
|
"""Set the WP to the top position with an optional offset."""
|
|
self.u = Vector(1, 0, 0)
|
|
self.v = Vector(0, 1, 0)
|
|
self.axis = Vector(0, 0, 1)
|
|
self.position = self.axis * offset
|
|
|
|
def set_to_front(self, offset=0):
|
|
"""Set the WP to the front position with an optional offset."""
|
|
self.u = Vector(1, 0, 0)
|
|
self.v = Vector(0, 0, 1)
|
|
self.axis = Vector(0, -1, 0)
|
|
self.position = self.axis * offset
|
|
|
|
def set_to_side(self, offset=0):
|
|
"""Set the WP to the right side position with an optional offset."""
|
|
self.u = Vector(0, 1, 0)
|
|
self.v = Vector(0, 0, 1)
|
|
self.axis = Vector(1, 0, 0)
|
|
self.position = self.axis * offset
|
|
|
|
def _axes_from_rotation(self, rot):
|
|
"""Return a tuple with the `u`, `v` and `axis` vectors from a Base.Rotation."""
|
|
mtx = rot.toMatrix()
|
|
return mtx.col(0), mtx.col(1), mtx.col(2)
|
|
|
|
def _axes_from_view_rotation(self, rot):
|
|
"""Return a tuple with the `u`, `v` and `axis` vectors from a Base.Rotation
|
|
derived from a view. The Yaw, Pitch and Roll angles are rounded if they are
|
|
near multiples of 45 degrees.
|
|
"""
|
|
ypr = [round(ang, 3) for ang in rot.getYawPitchRoll()]
|
|
if all([ang%45 == 0 for ang in ypr]):
|
|
rot.setEulerAngles("YawPitchRoll", *ypr)
|
|
return self._axes_from_rotation(rot)
|
|
|
|
def _get_prop_list(self):
|
|
return ["u",
|
|
"v",
|
|
"axis",
|
|
"position"]
|
|
|
|
|
|
class Plane(PlaneBase):
|
|
"""The old Plane class.
|
|
|
|
Parameters
|
|
----------
|
|
u: Base.Vector or WorkingPlane.Plane, optional
|
|
Defaults to Vector(1, 0, 0).
|
|
If a WP is provided:
|
|
A copy of the WP is created, all other parameters are then ignored.
|
|
If a vector is provided:
|
|
Unit vector for the `u` attribute (+X axis).
|
|
|
|
v: Base.Vector, optional
|
|
Defaults to Vector(0, 1, 0).
|
|
Unit vector for the `v` attribute (+Y axis).
|
|
|
|
w: Base.Vector, optional
|
|
Defaults to Vector(0, 0, 1).
|
|
Unit vector for the `axis` attribute (+Z axis).
|
|
|
|
pos: Base.Vector, optional
|
|
Defaults to Vector(0, 0, 0).
|
|
Vector for the `position` attribute (origin).
|
|
|
|
weak: bool, optional
|
|
Defaults to `True`.
|
|
If `True` the WP is in "Auto" mode and will adapt to the current view.
|
|
|
|
Note that the u, v and w vectors are not checked for validity.
|
|
|
|
Other attributes
|
|
----------------
|
|
stored: None/list
|
|
Placeholder for a stored state.
|
|
"""
|
|
|
|
def __init__(self,
|
|
u=Vector(1, 0, 0), v=Vector(0, 1, 0), w=Vector(0, 0, 1),
|
|
pos=Vector(0, 0, 0),
|
|
weak=True):
|
|
|
|
if isinstance(u, Plane):
|
|
self.match(u)
|
|
return
|
|
super().__init__(u, v, w, pos)
|
|
self.weak = weak
|
|
# a placeholder for a stored state
|
|
self.stored = None
|
|
|
|
def copy(self):
|
|
"""See PlaneBase.copy."""
|
|
wp = Plane()
|
|
self.match(source=self, target=wp)
|
|
return wp
|
|
|
|
def offsetToPoint(self, point, direction=None):
|
|
"""Return the signed distance from a point to the plane. The direction
|
|
argument is ignored.
|
|
|
|
The returned value is the negative of the local Z coordinate of the point.
|
|
"""
|
|
return -DraftGeomUtils.distance_to_plane(point, self.position, self.axis)
|
|
|
|
def projectPoint(self, point, direction=None, force_projection=True):
|
|
"""See PlaneBase.project_point."""
|
|
return super().project_point(point, direction, force_projection)
|
|
|
|
def alignToPointAndAxis(self, point, axis, offset=0, upvec=None):
|
|
"""See PlaneBase.align_to_point_and_axis."""
|
|
if upvec is None:
|
|
upvec = Vector(1, 0, 0)
|
|
super().align_to_point_and_axis(point, axis, offset, upvec)
|
|
self.weak = False
|
|
return True
|
|
|
|
def alignToPointAndAxis_SVG(self, point, axis, offset=0):
|
|
"""See PlaneBase.align_to_point_and_axis_svg."""
|
|
super().align_to_point_and_axis_svg(point, axis, offset)
|
|
self.weak = False
|
|
return True
|
|
|
|
def alignToCurve(self, shape, offset=0):
|
|
"""Align the WP to a curve. NOT IMPLEMENTED.
|
|
|
|
Parameters
|
|
----------
|
|
shape: Part.Shape
|
|
Edge or Wire.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`False`
|
|
"""
|
|
return False
|
|
|
|
def alignToEdges(self, shapes, offset=0):
|
|
"""Align the WP to edges with an optional offset.
|
|
|
|
The eges must define a plane.
|
|
|
|
The endpoints of the first edge defines the WP `position` and `u` axis.
|
|
|
|
Parameters
|
|
----------
|
|
shapes: iterable
|
|
Two edges.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
if super().align_to_edges_vertexes(shapes, offset) is False:
|
|
return False
|
|
self.weak = False
|
|
return True
|
|
|
|
def alignToFace(self, shape, offset=0, parent=None):
|
|
"""Align the WP to a face with an optional offset.
|
|
|
|
The face must be planar.
|
|
|
|
The center of gravity of the face defines the WP `position` and the
|
|
normal of the face the WP `axis`. The WP `u` and `v` vectors are
|
|
determined by the DraftGeomUtils.uv_vectors_from_face function.
|
|
See there.
|
|
|
|
Parameters
|
|
----------
|
|
shape: Part.Face
|
|
Face.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
parent: object
|
|
Defaults to `None`.
|
|
The ParentGeoFeatureGroup of the object the face belongs to.
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
if shape.ShapeType == "Face" and shape.Surface.isPlanar():
|
|
place = DraftGeomUtils.placement_from_face(shape)
|
|
if parent:
|
|
place = parent.getGlobalPlacement() * place
|
|
super().align_to_placement(place, offset)
|
|
self.weak = False
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def alignTo3Points(self, p1, p2, p3, offset=0):
|
|
"""Align the plane to a temporary face created from three points.
|
|
|
|
Parameters
|
|
----------
|
|
p1: Base.Vector
|
|
First point.
|
|
p2: Base.Vector
|
|
Second point.
|
|
p3: Base.Vector
|
|
Third point.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
w = Part.makePolygon([p1, p2, p3, p1])
|
|
f = Part.Face(w)
|
|
return self.alignToFace(f, offset)
|
|
|
|
def alignToSelection(self, offset=0):
|
|
"""Align the plane to a selection with an optional offset.
|
|
|
|
The selection must define a plane.
|
|
|
|
Parameter
|
|
---------
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
sel_ex = FreeCADGui.Selection.getSelectionEx()
|
|
if not sel_ex:
|
|
return False
|
|
|
|
shapes = list()
|
|
names = list()
|
|
for obj in sel_ex:
|
|
# check that the geometric property is a Part.Shape object
|
|
geom_is_shape = False
|
|
if isinstance(obj.Object, FreeCAD.GeoFeature):
|
|
geom = obj.Object.getPropertyOfGeometry()
|
|
if isinstance(geom, Part.Shape):
|
|
geom_is_shape = True
|
|
if not geom_is_shape:
|
|
_wrn(translate(
|
|
"draft",
|
|
"Object without Part.Shape geometry:'{}'".format(
|
|
obj.ObjectName)) + "\n")
|
|
return False
|
|
if geom.isNull():
|
|
_wrn(translate(
|
|
"draft",
|
|
"Object with null Part.Shape geometry:'{}'".format(
|
|
obj.ObjectName)) + "\n")
|
|
return False
|
|
if obj.HasSubObjects:
|
|
shapes.extend(obj.SubObjects)
|
|
names.extend([obj.ObjectName + "." + n for n in obj.SubElementNames])
|
|
else:
|
|
shapes.append(geom)
|
|
names.append(obj.ObjectName)
|
|
|
|
normal = None
|
|
for n in range(len(shapes)):
|
|
if not DraftGeomUtils.is_planar(shapes[n]):
|
|
_wrn(translate(
|
|
"draft", "'{}' object is not planar".format(names[n])) + "\n")
|
|
return False
|
|
if not normal:
|
|
normal = DraftGeomUtils.get_normal(shapes[n])
|
|
shape_ref = n
|
|
|
|
# test if all shapes are coplanar
|
|
if normal:
|
|
for n in range(len(shapes)):
|
|
if not DraftGeomUtils.are_coplanar(shapes[shape_ref], shapes[n]):
|
|
_wrn(translate(
|
|
"draft", "{} and {} aren't coplanar".format(
|
|
names[shape_ref],names[n])) + "\n")
|
|
return False
|
|
else:
|
|
# suppose all geometries are straight lines or points
|
|
points = [vertex.Point for shape in shapes for vertex in shape.Vertexes]
|
|
if len(points) >= 3:
|
|
poly = Part.makePolygon(points)
|
|
if not DraftGeomUtils.is_planar(poly):
|
|
_wrn(translate(
|
|
"draft", "All Shapes must be coplanar") + "\n")
|
|
return False
|
|
normal = DraftGeomUtils.get_normal(poly)
|
|
else:
|
|
normal = None
|
|
|
|
if not normal:
|
|
_wrn(translate(
|
|
"draft", "Selected Shapes must define a plane") + "\n")
|
|
return False
|
|
|
|
# set center of mass
|
|
ctr_mass = Vector(0,0,0)
|
|
ctr_pts = Vector(0,0,0)
|
|
mass = 0
|
|
for shape in shapes:
|
|
if hasattr(shape, "CenterOfMass"):
|
|
ctr_mass += shape.CenterOfMass*shape.Mass
|
|
mass += shape.Mass
|
|
else:
|
|
ctr_pts += shape.Point
|
|
if mass > 0:
|
|
ctr_mass /= mass
|
|
# all shapes are vertexes
|
|
else:
|
|
ctr_mass = ctr_pts/len(shapes)
|
|
|
|
super().align_to_point_and_axis(ctr_mass, normal, offset)
|
|
self.weak = False
|
|
return True
|
|
|
|
def setup(self, direction=None, point=None, upvec=None, force=False):
|
|
"""Set up the working plane if it exists but is undefined.
|
|
|
|
If `direction` and `point` are present,
|
|
it calls `align_to_point_and_axis(point, direction, 0, upvec)`.
|
|
|
|
Otherwise, it gets the camera orientation to define
|
|
a working plane that is perpendicular to the current view,
|
|
centered at the origin, and with `v` pointing up on the screen.
|
|
|
|
This method only works when the `weak` attribute is `True`.
|
|
This method also sets `weak` to `True`.
|
|
|
|
This method only works when `FreeCAD.GuiUp` is `True`,
|
|
that is, when the graphical interface is loaded.
|
|
Otherwise it fails silently.
|
|
|
|
Parameters
|
|
----------
|
|
direction: Base.Vector, optional
|
|
It defaults to `None`. It is the new `axis` of the plane.
|
|
point: Base.Vector, optional
|
|
It defaults to `None`. It is the new `position` of the plane.
|
|
upvec: Base.Vector, optional
|
|
It defaults to `None`. It is the new `v` orientation of the plane.
|
|
force: bool
|
|
If True, it sets the plane even if the plane is not in weak mode
|
|
"""
|
|
if self.weak or force:
|
|
if direction is not None and point is not None:
|
|
super().align_to_point_and_axis(point, direction, 0, upvec)
|
|
elif FreeCAD.GuiUp:
|
|
try:
|
|
view = gui_utils.get_3d_view()
|
|
if view is not None:
|
|
cam = view.getCameraNode()
|
|
rot = FreeCAD.Rotation(*cam.getField("orientation").getValue().getValue())
|
|
self.u, self.v, self.axis = self._axes_from_view_rotation(rot)
|
|
self.position = Vector()
|
|
except Exception:
|
|
pass
|
|
if force:
|
|
self.weak = False
|
|
else:
|
|
self.weak = True
|
|
|
|
def reset(self):
|
|
"""Reset the WP.
|
|
|
|
Sets the `weak` attribute to `True`.
|
|
"""
|
|
self.weak = True
|
|
|
|
def setTop(self):
|
|
"""Set the WP to the top position and updates the GUI."""
|
|
super().set_to_top()
|
|
self.weak = False
|
|
if FreeCAD.GuiUp:
|
|
from draftutils.translate import translate
|
|
if hasattr(FreeCADGui,"Snapper"):
|
|
FreeCADGui.Snapper.setGrid()
|
|
if hasattr(FreeCADGui,"draftToolBar"):
|
|
FreeCADGui.draftToolBar.wplabel.setText(translate("draft", "Top"))
|
|
|
|
def setFront(self):
|
|
"""Set the WP to the front position and updates the GUI."""
|
|
super().set_to_front()
|
|
self.weak = False
|
|
if FreeCAD.GuiUp:
|
|
from draftutils.translate import translate
|
|
if hasattr(FreeCADGui,"Snapper"):
|
|
FreeCADGui.Snapper.setGrid()
|
|
if hasattr(FreeCADGui,"draftToolBar"):
|
|
FreeCADGui.draftToolBar.wplabel.setText(translate("draft", "Front"))
|
|
|
|
def setSide(self):
|
|
"""Set the WP to the left side position and updates the GUI.
|
|
|
|
Note that set_to_side from the parent class sets the WP to the right side position.
|
|
Which matches the Side option from Draft_SelectPlane.
|
|
"""
|
|
self.u = Vector(0, -1, 0)
|
|
self.v = Vector(0, 0, 1)
|
|
self.axis = Vector(-1, 0, 0)
|
|
self.position = Vector()
|
|
self.weak = False
|
|
if FreeCAD.GuiUp:
|
|
from draftutils.translate import translate
|
|
if hasattr(FreeCADGui,"Snapper"):
|
|
FreeCADGui.Snapper.setGrid()
|
|
if hasattr(FreeCADGui,"draftToolBar"):
|
|
FreeCADGui.draftToolBar.wplabel.setText(translate("draft", "Side"))
|
|
|
|
def getRotation(self):
|
|
"""Return a placement describing the WP orientation only."""
|
|
return FreeCAD.Placement(Vector(), FreeCAD.Rotation(self.u, self.v, self.axis, "ZYX"))
|
|
|
|
def getPlacement(self, rotated=False):
|
|
"""Return a placement calculated from the WP. The rotated argument is ignored."""
|
|
return super().get_placement()
|
|
|
|
def getNormal(self):
|
|
"""Return the normal vector (axis) of the WP."""
|
|
return self.axis
|
|
|
|
def setFromPlacement(self, place, rebase=False):
|
|
"""Align the WP to a placement.
|
|
|
|
Parameters
|
|
----------
|
|
place: Base.Placement
|
|
Placement.
|
|
rebase: bool, optional
|
|
Defaults to `False`.
|
|
If `False` the `position` of the WP is not changed.
|
|
"""
|
|
if rebase:
|
|
super().align_to_placement(place)
|
|
else:
|
|
super()._axes_from_rotation(place.Rotation)
|
|
|
|
def inverse(self):
|
|
"""Invert the direction of the plane.
|
|
|
|
It inverts the `u` and `axis` vectors.
|
|
"""
|
|
self.u = self.u.negative()
|
|
self.axis = self.axis.negative()
|
|
|
|
def save(self):
|
|
"""Store the plane attributes.
|
|
|
|
Store `u`, `v`, `axis`, `position` and `weak`
|
|
in a list in `stored`.
|
|
"""
|
|
self.stored = [self.u, self.v, self.axis, self.position, self.weak]
|
|
|
|
def restore(self):
|
|
"""Restore the plane attributes that were saved.
|
|
|
|
Restores the attributes `u`, `v`, `axis`, `position` and `weak`
|
|
from `stored`, and set `stored` to `None`.
|
|
"""
|
|
if self.stored is not None:
|
|
self.u, self.v, self.axis, self.position, self.weak = self.stored
|
|
self.stored = None
|
|
|
|
def getLocalCoords(self, point):
|
|
"""Translate a point from the global coordinate system to
|
|
the local (WP) coordinate system.
|
|
"""
|
|
return super().get_local_coords(point)
|
|
|
|
def getGlobalCoords(self, point):
|
|
"""Translate a point from the local (WP) coordinate system to
|
|
the global coordinate system.
|
|
"""
|
|
return super().get_global_coords(point)
|
|
|
|
def getLocalRot(self, vec):
|
|
"""Translate a vector from the global coordinate system to
|
|
the local (WP) coordinate system.
|
|
"""
|
|
return super().get_local_coords(vec, as_vector=True)
|
|
|
|
def getGlobalRot(self, vec):
|
|
"""Translate a vector from the local (WP) coordinate system to
|
|
the global coordinate system.
|
|
"""
|
|
return super().get_global_coords(vec, as_vector=True)
|
|
|
|
def getClosestAxis(self, vec):
|
|
"""Return the closest WP axis to a vector.
|
|
|
|
Parameters
|
|
----------
|
|
vec: Base.Vector
|
|
Vector.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
`'x'`, `'y'` or `'z'`.
|
|
"""
|
|
return super().get_closest_axis(vec)
|
|
|
|
def isGlobal(self):
|
|
"""Return `True` if the WP matches the global coordinate system."""
|
|
return super().is_global()
|
|
|
|
def isOrtho(self):
|
|
"""Return `True` if the WP axes are orthogonal to the global axes."""
|
|
return super().is_ortho()
|
|
|
|
def getDeviation(self):
|
|
"""Return the angle between the u axis and the horizontal plane.
|
|
|
|
It defines a projection of `u` on the horizontal plane
|
|
(without a Z component), and then measures the angle between
|
|
this projection and `u`.
|
|
|
|
It also considers the cross product of the projection
|
|
and `u` to determine the sign of the angle.
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
Angle between the `u` vector, and a projected vector
|
|
on the global horizontal plane.
|
|
|
|
See Also
|
|
--------
|
|
DraftVecUtils.angle
|
|
"""
|
|
proj = Vector(self.u.x, self.u.y, 0)
|
|
if self.u.getAngle(proj) == 0:
|
|
return 0
|
|
else:
|
|
norm = proj.cross(self.u)
|
|
return DraftVecUtils.angle(self.u, proj, norm)
|
|
|
|
def getParameters(self):
|
|
"""Return a dictionary with the data which define the plane:
|
|
`u`, `v`, `axis`, `position`, `weak`.
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
dictionary of the form:
|
|
{"u":x, "v":v, "axis":axis, "position":position, "weak":weak}
|
|
"""
|
|
return super().get_parameters()
|
|
|
|
def setFromParameters(self, data):
|
|
"""Set the plane according to data.
|
|
|
|
Parameters
|
|
----------
|
|
data: dict
|
|
dictionary of the form:
|
|
{"u":x, "v":v, "axis":axis, "position":position, "weak":weak}
|
|
"""
|
|
super().set_parameters(data)
|
|
|
|
def _get_prop_list(self):
|
|
return ["u",
|
|
"v",
|
|
"axis",
|
|
"position",
|
|
"weak"]
|
|
|
|
plane = Plane
|
|
|
|
|
|
class PlaneGui(PlaneBase):
|
|
"""The PlaneGui class.
|
|
The class handles several GUI related aspects of the WP including a history.
|
|
|
|
Parameters
|
|
----------
|
|
u: Base.Vector or WorkingPlane.Plane, optional
|
|
Defaults to Vector(1, 0, 0).
|
|
If a WP is provided:
|
|
A copy of the WP is created, all other parameters are then ignored.
|
|
If a vector is provided:
|
|
Unit vector for the `u` attribute (+X axis).
|
|
|
|
v: Base.Vector, optional
|
|
Defaults to Vector(0, 1, 0).
|
|
Unit vector for the `v` attribute (+Y axis).
|
|
|
|
w: Base.Vector, optional
|
|
Defaults to Vector(0, 0, 1).
|
|
Unit vector for the `axis` attribute (+Z axis).
|
|
|
|
pos: Base.Vector, optional
|
|
Defaults to Vector(0, 0, 0).
|
|
Vector for the `position` attribute (origin).
|
|
|
|
auto: bool, optional
|
|
Defaults to `True`.
|
|
If `True` the WP is in "Auto" mode and will adapt to the current view.
|
|
|
|
icon: str, optional
|
|
Defaults to ":/icons/view-axonometric.svg".
|
|
Path to the icon for the draftToolBar.
|
|
|
|
label: str, optional
|
|
Defaults to "Auto".
|
|
Label for the draftToolBar.
|
|
|
|
tip: str, optional
|
|
Defaults to "Current working plane: Auto".
|
|
Tooltip for the draftToolBar.
|
|
|
|
Note that the u, v and w vectors are not checked for validity.
|
|
|
|
Other attributes
|
|
----------------
|
|
_view: Gui::View3DInventor
|
|
Reference to a 3D view.
|
|
|
|
_stored: dict
|
|
Dictionary for a temporary stored state.
|
|
|
|
_history: dict
|
|
Dictionary that holds up to 10 stored states.
|
|
"""
|
|
|
|
def __init__(self,
|
|
u=Vector(1, 0, 0), v=Vector(0, 1, 0), w=Vector(0, 0, 1),
|
|
pos=Vector(0, 0, 0),
|
|
auto=True,
|
|
icon=":/icons/view-axonometric.svg",
|
|
label=QT_TRANSLATE_NOOP("draft", "Auto"),
|
|
tip=QT_TRANSLATE_NOOP("draft", "Current working plane:") + " " + QT_TRANSLATE_NOOP("draft", "Auto")):
|
|
|
|
if isinstance(u, PlaneGui):
|
|
self.match(u)
|
|
else:
|
|
super().__init__(u, v, w, pos)
|
|
self.auto = auto
|
|
self.icon = icon
|
|
self.label = label
|
|
self.tip = tip
|
|
self._view = None
|
|
self._stored = {}
|
|
self._history = {}
|
|
|
|
def copy(self):
|
|
"""Return a new plane that is a copy of the present object."""
|
|
wp = PlaneGui()
|
|
self.match(source=self, target=wp)
|
|
return wp
|
|
|
|
def _save(self):
|
|
"""Store the WP attributes."""
|
|
self._stored = self.get_parameters()
|
|
|
|
def _restore(self):
|
|
"""Restore the WP attributes that were saved."""
|
|
if self._stored:
|
|
self.set_parameters(self._stored)
|
|
self._stored = {}
|
|
self._update_all(_hist_add=False)
|
|
|
|
def align_to_selection(self, offset=0, _hist_add=True):
|
|
"""Align the WP to a selection with an optional offset.
|
|
|
|
The selection must define a plane.
|
|
|
|
Parameter
|
|
---------
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
if not FreeCAD.GuiUp:
|
|
return False
|
|
|
|
sels = FreeCADGui.Selection.getSelectionEx("", 0)
|
|
if not sels:
|
|
return False
|
|
|
|
objs = []
|
|
for sel in sels:
|
|
for sub in sel.SubElementNames if sel.SubElementNames else [""]:
|
|
objs.append(Part.getShape(sel.Object, sub, needSubElement=True, retType=1))
|
|
|
|
if len(objs) != 1:
|
|
if all([obj[0].isNull() is False and obj[0].ShapeType in ["Edge", "Vertex"] for obj in objs]):
|
|
ret = self.align_to_edges_vertexes([obj[0] for obj in objs], offset, _hist_add)
|
|
else:
|
|
ret = False
|
|
|
|
if ret is False:
|
|
_wrn(translate("draft", "Selected shapes do not define a plane"))
|
|
return ret
|
|
|
|
shape, mtx, obj = objs[0]
|
|
place = FreeCAD.Placement(mtx)
|
|
|
|
typ = utils.get_type(obj)
|
|
if typ in ["App::Part", "PartDesign::Plane", "Axis", "SectionPlane"]:
|
|
ret = self.align_to_obj_placement(obj, offset, place, _hist_add)
|
|
elif typ == "WorkingPlaneProxy":
|
|
ret = self.align_to_wp_proxy(obj, offset, place, _hist_add)
|
|
elif typ == "BuildingPart":
|
|
ret = self.align_to_wp_proxy(obj, offset, place * obj.Placement, _hist_add)
|
|
elif shape.isNull():
|
|
ret = self.align_to_obj_placement(obj, offset, place, _hist_add)
|
|
elif shape.ShapeType == "Face":
|
|
ret = self.align_to_face(shape, offset, _hist_add)
|
|
elif shape.ShapeType == "Edge":
|
|
ret = self.align_to_edge_or_wire(shape, offset, _hist_add)
|
|
elif shape.Solids:
|
|
ret = self.align_to_obj_placement(obj, offset, place, _hist_add)
|
|
else:
|
|
ret = self.align_to_edges_vertexes(shape.Vertexes, offset, _hist_add)
|
|
|
|
if ret is False:
|
|
_wrn(translate("draft", "Selected shapes do not define a plane"))
|
|
return ret
|
|
|
|
def _handle_custom(self, _hist_add):
|
|
self.auto = False
|
|
self.icon = ":/icons/Draft_SelectPlane.svg"
|
|
self.label = self._get_label(translate("draft", "Custom"))
|
|
self.tip = self._get_tip(translate("draft", "Custom"))
|
|
self._update_all(_hist_add)
|
|
|
|
def align_to_3_points(self, p1, p2, p3, offset=0, _hist_add=True):
|
|
"""See PlaneBase.align_to_3_points."""
|
|
if super().align_to_3_points(p1, p2, p3, offset) is False:
|
|
return False
|
|
self._handle_custom(_hist_add)
|
|
return True
|
|
|
|
def align_to_edges_vertexes(self, shapes, offset=0, _hist_add=True):
|
|
"""See PlaneBase.align_to_edges_vertexes."""
|
|
if super().align_to_edges_vertexes(shapes, offset) is False:
|
|
return False
|
|
self._handle_custom(_hist_add)
|
|
return True
|
|
|
|
def align_to_edge_or_wire(self, shape, offset=0, _hist_add=True):
|
|
"""See PlaneBase.align_to_edge_or_wire."""
|
|
if super().align_to_edge_or_wire(shape, offset) is False:
|
|
return False
|
|
self._handle_custom(_hist_add)
|
|
return True
|
|
|
|
def align_to_face(self, shape, offset=0, _hist_add=True):
|
|
"""See PlaneBase.align_to_face."""
|
|
if super().align_to_face(shape, offset) is False:
|
|
return False
|
|
self._handle_custom(_hist_add)
|
|
return True
|
|
|
|
def align_to_placement(self, place, offset=0, _hist_add=True):
|
|
"""See PlaneBase.align_to_placement."""
|
|
super().align_to_placement(place, offset)
|
|
self._handle_custom(_hist_add)
|
|
return True
|
|
|
|
def align_to_point_and_axis(self, point, axis, offset=0, upvec=Vector(1, 0, 0), _hist_add=True):
|
|
"""See PlaneBase.align_to_point_and_axis."""
|
|
if super().align_to_point_and_axis(point, axis, offset, upvec) is False:
|
|
return False
|
|
self._handle_custom(_hist_add)
|
|
return True
|
|
|
|
def align_to_obj_placement(self, obj, offset=0, place=None, _hist_add=True):
|
|
"""Align the WP to an object placement with an optional offset.
|
|
|
|
Parameters
|
|
----------
|
|
obj: App::DocumentObject
|
|
Object to derive the Placement (if place is `None`), the icon and
|
|
the label from.
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
place: Base.Placement, optional
|
|
Defaults to `None`.
|
|
If `None` the Placement from obj is used.
|
|
Argument to be used if obj is inside a container.
|
|
|
|
Returns
|
|
-------
|
|
`True`/`False`
|
|
`True` if successful.
|
|
"""
|
|
if hasattr(obj, "Placement") is False:
|
|
return False
|
|
if place is None:
|
|
place = obj.Placement
|
|
super().align_to_placement(place, offset)
|
|
self.auto = False
|
|
if utils.get_type(obj) == "WorkingPlaneProxy":
|
|
self.icon = ":/icons/Draft_PlaneProxy.svg"
|
|
elif FreeCAD.GuiUp \
|
|
and hasattr(obj, "ViewObject") \
|
|
and hasattr(obj.ViewObject, "Proxy") \
|
|
and hasattr(obj.ViewObject.Proxy, "getIcon"):
|
|
self.icon = obj.ViewObject.Proxy.getIcon()
|
|
else:
|
|
self.icon = ":/icons/Std_Placement.svg"
|
|
self.label = self._get_label(obj.Label)
|
|
self.tip = self._get_tip(obj.Label)
|
|
self._update_all(_hist_add)
|
|
return True
|
|
|
|
def align_to_wp_proxy(self, obj, offset=0, place=None, _hist_add=True):
|
|
"""Align the WP to a WPProxy with an optional offset.
|
|
|
|
See align_to_obj_placement.
|
|
|
|
Also handles several WPProxy related features.
|
|
"""
|
|
if self.align_to_obj_placement(obj, offset, place, _hist_add) is False:
|
|
return False
|
|
|
|
if not FreeCAD.GuiUp:
|
|
return True
|
|
|
|
vobj = obj.ViewObject
|
|
|
|
if hasattr(vobj, "AutoWorkingPlane") \
|
|
and vobj.AutoWorkingPlane is True:
|
|
self.auto = True
|
|
|
|
if hasattr(vobj, "CutView") \
|
|
and hasattr(vobj, "AutoCutView") \
|
|
and vobj.AutoCutView is True:
|
|
vobj.CutView = True
|
|
|
|
if hasattr(vobj, "RestoreView") \
|
|
and vobj.RestoreView is True \
|
|
and hasattr(vobj, "ViewData") \
|
|
and len(vobj.ViewData) >= 12:
|
|
vdat = vobj.ViewData
|
|
if self._view is not None:
|
|
try:
|
|
if len(vdat) == 13 and vdat[12] == 1:
|
|
camtype = "Perspective"
|
|
else:
|
|
camtype = "Orthographic"
|
|
if self._view.getCameraType() != camtype:
|
|
self._view.setCameraType(camtype)
|
|
|
|
cam = self._view.getCameraNode()
|
|
cam.position.setValue([vdat[0], vdat[1], vdat[2]])
|
|
cam.orientation.setValue([vdat[3], vdat[4], vdat[5], vdat[6]])
|
|
cam.nearDistance.setValue(vdat[7])
|
|
cam.farDistance.setValue(vdat[8])
|
|
cam.aspectRatio.setValue(vdat[9])
|
|
cam.focalDistance.setValue(vdat[10])
|
|
if camtype == "Orthographic":
|
|
cam.height.setValue(vdat[11])
|
|
else:
|
|
cam.heightAngle.setValue(vdat[11])
|
|
except Exception:
|
|
pass
|
|
|
|
if hasattr(vobj, "RestoreState") \
|
|
and vobj.RestoreState is True \
|
|
and hasattr(vobj, "VisibilityMap") \
|
|
and vobj.VisibilityMap:
|
|
for name, vis in vobj.VisibilityMap.items():
|
|
obj = FreeCADGui.ActiveDocument.getObject(name)
|
|
if obj:
|
|
obj.Visibility = (vis == "True")
|
|
|
|
return True
|
|
|
|
def auto_align(self):
|
|
"""Align the WP to the current view if self.auto is True."""
|
|
if self.auto and self._view is not None:
|
|
try:
|
|
cam = self._view.getCameraNode()
|
|
rot = FreeCAD.Rotation(*cam.getField("orientation").getValue().getValue())
|
|
self.u, self.v, self.axis = self._axes_from_view_rotation(rot)
|
|
self.position = Vector()
|
|
except Exception:
|
|
pass
|
|
|
|
def set_to_default(self):
|
|
"""Set the WP to the default from the preferences."""
|
|
default_wp = params.get_param("defaultWP")
|
|
if default_wp == 0:
|
|
self.set_to_auto()
|
|
elif default_wp == 1:
|
|
self.set_to_top()
|
|
elif default_wp == 2:
|
|
self.set_to_front()
|
|
elif default_wp == 3:
|
|
self.set_to_side()
|
|
|
|
def set_to_auto(self): # Similar to Plane.reset.
|
|
"""Set the WP to auto."""
|
|
self.auto = True
|
|
self.auto_align()
|
|
self.icon = ":/icons/view-axonometric.svg"
|
|
self.label = self._get_label(translate("draft", "Auto"))
|
|
self.tip = self._get_tip(translate("draft", "Auto"))
|
|
self._update_all()
|
|
|
|
def set_to_top(self, offset=0, center_on_view=False):
|
|
"""Set the WP to the top position with an optional offset.
|
|
|
|
Parameters
|
|
----------
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
center_on_view: bool, optional
|
|
Defaults to `False`.
|
|
If `True` the WP `position` is moved along the (offset) WP to the
|
|
center of the view
|
|
"""
|
|
super().set_to_top(offset)
|
|
if center_on_view:
|
|
self._center_on_view()
|
|
self.auto = False
|
|
self.icon = ":/icons/view-top.svg"
|
|
self.label = self._get_label(translate("draft", "Top"))
|
|
self.tip = self._get_tip(translate("draft", "Top"))
|
|
self._update_all()
|
|
|
|
def set_to_front(self, offset=0, center_on_view=False):
|
|
"""Set the WP to the front position with an optional offset.
|
|
|
|
Parameters
|
|
----------
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
center_on_view: bool, optional
|
|
Defaults to `False`.
|
|
If `True` the WP `position` is moved along the (offset) WP to the
|
|
center of the view
|
|
"""
|
|
super().set_to_front(offset)
|
|
if center_on_view:
|
|
self._center_on_view()
|
|
self.auto = False
|
|
self.icon = ":/icons/view-front.svg"
|
|
self.label = self._get_label(translate("draft", "Front"))
|
|
self.tip = self._get_tip(translate("draft", "Front"))
|
|
self._update_all()
|
|
|
|
def set_to_side(self, offset=0, center_on_view=False):
|
|
"""Set the WP to the right side position with an optional offset.
|
|
|
|
Parameters
|
|
----------
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
center_on_view: bool, optional
|
|
Defaults to `False`.
|
|
If `True` the WP `position` is moved along the (offset) WP to the
|
|
center of the view
|
|
"""
|
|
super().set_to_side(offset)
|
|
if center_on_view:
|
|
self._center_on_view()
|
|
self.auto = False
|
|
self.icon = ":/icons/view-right.svg"
|
|
self.label = self._get_label(translate("draft", "Side"))
|
|
self.tip = self._get_tip(translate("draft", "Side"))
|
|
self._update_all()
|
|
|
|
def set_to_view(self, offset=0, center_on_view=False):
|
|
"""Align the WP to the view with an optional offset.
|
|
|
|
Parameters
|
|
----------
|
|
offset: float, optional
|
|
Defaults to zero.
|
|
Offset along the WP `axis`.
|
|
center_on_view: bool, optional
|
|
Defaults to `False`.
|
|
If `True` the WP `position` is moved along the (offset) WP to the
|
|
center of the view
|
|
"""
|
|
if self._view is not None:
|
|
try:
|
|
cam = self._view.getCameraNode()
|
|
rot = FreeCAD.Rotation(*cam.getField("orientation").getValue().getValue())
|
|
self.u, self.v, self.axis = self._axes_from_view_rotation(rot)
|
|
self.position = self.axis * offset
|
|
if center_on_view:
|
|
self._center_on_view()
|
|
self.auto = False
|
|
self.icon = ":/icons/Draft_SelectPlane.svg"
|
|
self.label = self._get_label(translate("draft", "Custom"))
|
|
self.tip = self._get_tip(translate("draft", "Custom"))
|
|
self._update_all()
|
|
except Exception:
|
|
pass
|
|
|
|
def set_to_position(self, pos):
|
|
"""Set the `position` of the WP."""
|
|
self.position = pos
|
|
label = self.label.rstrip("*")
|
|
self.label = self._get_label(label)
|
|
self.tip = self._get_tip(label)
|
|
self._update_all()
|
|
|
|
def center_on_view(self):
|
|
"""Move the WP `position` along the WP to the center of the view."""
|
|
self._center_on_view()
|
|
label = self.label.rstrip("*")
|
|
self.label = self._get_label(label)
|
|
self.tip = self._get_tip(label)
|
|
self._update_all()
|
|
|
|
def _center_on_view(self):
|
|
if self._view is not None:
|
|
try:
|
|
cam = self._view.getCameraNode()
|
|
pos = self.project_point(Vector(cam.position.getValue().getValue()),
|
|
direction=self._view.getViewDirection(),
|
|
force_projection=False)
|
|
if pos is not None:
|
|
self.position = pos
|
|
except Exception:
|
|
pass
|
|
|
|
def align_view(self):
|
|
"""Align the view to the WP."""
|
|
if self._view is not None:
|
|
try:
|
|
default_cam_dist = abs(params.get_param_view("NewDocumentCameraScale"))
|
|
cam = self._view.getCameraNode()
|
|
cur_cam_dist = abs(self.get_local_coords(Vector(cam.position.getValue().getValue())).z)
|
|
cam_dist = max(default_cam_dist, cur_cam_dist)
|
|
cam.position.setValue(self.position + DraftVecUtils.scaleTo(self.axis, cam_dist))
|
|
cam.orientation.setValue(self.get_placement().Rotation.Q)
|
|
except Exception:
|
|
pass
|
|
|
|
def _previous(self):
|
|
idx = self._history["idx"]
|
|
if idx == 0:
|
|
_wrn(translate("draft", "No previous working plane"))
|
|
return
|
|
idx -= 1
|
|
self.set_parameters(self._history["data_list"][idx])
|
|
self._history["idx"] = idx
|
|
self._update_all(_hist_add=False)
|
|
|
|
def _next(self):
|
|
idx = self._history["idx"]
|
|
if idx == len(self._history["data_list"]) - 1:
|
|
_wrn(translate("draft", "No next working plane"))
|
|
return
|
|
idx += 1
|
|
self.set_parameters(self._history["data_list"][idx])
|
|
self._history["idx"] = idx
|
|
self._update_all(_hist_add=False)
|
|
|
|
def _has_previous(self):
|
|
return bool(self._history) and self._history["idx"] != 0
|
|
|
|
def _has_next(self):
|
|
return bool(self._history) and self._history["idx"] != len(self._history["data_list"]) - 1
|
|
|
|
def _get_prop_list(self):
|
|
return ["u",
|
|
"v",
|
|
"axis",
|
|
"position",
|
|
"auto",
|
|
"icon",
|
|
"label",
|
|
"tip"]
|
|
|
|
def _get_label(self, label):
|
|
if self.auto or self.position.isEqual(Vector(), 0):
|
|
return label
|
|
else:
|
|
return label + "*"
|
|
|
|
def _get_tip(self, label):
|
|
tip = translate("draft", "Current working plane:")
|
|
tip += " " + label
|
|
if self.auto:
|
|
return tip
|
|
tip += "\n" + translate("draft", "Axes:")
|
|
tip += "\n X = "
|
|
tip += self._format_vector(self.u)
|
|
tip += "\n Y = "
|
|
tip += self._format_vector(self.v)
|
|
tip += "\n Z = "
|
|
tip += self._format_vector(self.axis)
|
|
tip += "\n" + translate("draft", "Position:")
|
|
tip += "\n X = "
|
|
tip += self._format_coord(self.position.x)
|
|
tip += "\n Y = "
|
|
tip += self._format_coord(self.position.y)
|
|
tip += "\n Z = "
|
|
tip += self._format_coord(self.position.z)
|
|
return tip
|
|
|
|
def _format_coord(self, coord):
|
|
return FreeCAD.Units.Quantity(coord, FreeCAD.Units.Length).UserString
|
|
|
|
def _format_vector(self, vec):
|
|
dec = params.get_param("Decimals", path="Units")
|
|
return f"({vec.x:.{dec}f} {vec.y:.{dec}f} {vec.z:.{dec}f})"
|
|
|
|
def _update_all(self, _hist_add=True):
|
|
if _hist_add is True:
|
|
self._update_history()
|
|
self._update_old_plane() # Must happen before _update_grid.
|
|
self._update_grid()
|
|
self._update_gui()
|
|
|
|
def _update_history(self):
|
|
data = self.get_parameters()
|
|
if not self._history:
|
|
self._history = {"idx": 0, "data_list": [data]}
|
|
return
|
|
idx = self._history["idx"]
|
|
if data == self._history["data_list"][idx]:
|
|
return
|
|
if self.auto is True and self._history["data_list"][idx]["auto"] is True:
|
|
return
|
|
|
|
max_len = 10 # Max. length of data_list.
|
|
self._history["data_list"] = self._history["data_list"][(idx - (max_len - 2)):(idx + 1)]
|
|
self._history["data_list"].append(data)
|
|
self._history["idx"] = len(self._history["data_list"]) - 1
|
|
|
|
def _update_old_plane(self):
|
|
""" Update the old DraftWorkingPlane for compatibility.
|
|
The tracker and snapper code currently still depend on it.
|
|
"""
|
|
if not hasattr(FreeCAD, "DraftWorkingPlane"):
|
|
FreeCAD.DraftWorkingPlane = Plane()
|
|
for prop in ["u", "v", "axis", "position"]:
|
|
setattr(FreeCAD.DraftWorkingPlane,
|
|
prop,
|
|
self._copy_value(getattr(self, prop)))
|
|
FreeCAD.DraftWorkingPlane.weak = self.auto
|
|
|
|
def _update_grid(self):
|
|
# Check for draftToolBar because the trackers (grid) depend on it for its colors.
|
|
if FreeCAD.GuiUp \
|
|
and hasattr(FreeCADGui, "draftToolBar") \
|
|
and hasattr(FreeCADGui, "Snapper") \
|
|
and self._view is not None:
|
|
FreeCADGui.Snapper.setGrid()
|
|
FreeCADGui.Snapper.restack() # Required??
|
|
|
|
def _update_gui(self):
|
|
if FreeCAD.GuiUp \
|
|
and hasattr(FreeCADGui, "draftToolBar") \
|
|
and self._view is not None:
|
|
from PySide import QtGui
|
|
button = FreeCADGui.draftToolBar.wplabel
|
|
button.setIcon(QtGui.QIcon(self.icon))
|
|
button.setText(self.label)
|
|
button.setToolTip(self.tip)
|
|
|
|
|
|
def get_working_plane(update=True):
|
|
|
|
if not hasattr(FreeCAD, "draft_working_planes"):
|
|
FreeCAD.draft_working_planes = [[], []]
|
|
|
|
view = gui_utils.get_3d_view()
|
|
|
|
if view is not None and view in FreeCAD.draft_working_planes[0]:
|
|
i = FreeCAD.draft_working_planes[0].index(view)
|
|
wp = FreeCAD.draft_working_planes[1][i]
|
|
if update is False:
|
|
wp._update_old_plane() # Currently required for tracker and snapper code.
|
|
return wp
|
|
wp.auto_align()
|
|
wp._update_all(_hist_add=False)
|
|
return wp
|
|
|
|
wp = PlaneGui()
|
|
if FreeCAD.GuiUp:
|
|
wp._view = view # Update _view before call to set_to_default, set_to_auto requires a 3D view.
|
|
wp.set_to_default()
|
|
if view is not None:
|
|
FreeCAD.draft_working_planes[0].append(view)
|
|
FreeCAD.draft_working_planes[1].append(wp)
|
|
|
|
return wp
|
|
|
|
|
|
# View observer code to update the Draft Tray:
|
|
if FreeCAD.GuiUp:
|
|
from PySide import QtWidgets
|
|
from draftutils.todo import ToDo
|
|
|
|
def _update_gui():
|
|
try:
|
|
view = gui_utils.get_3d_view()
|
|
if view is None:
|
|
return
|
|
if not hasattr(FreeCAD, "draft_working_planes"):
|
|
FreeCAD.draft_working_planes = [[], []]
|
|
if view in FreeCAD.draft_working_planes[0]:
|
|
i = FreeCAD.draft_working_planes[0].index(view)
|
|
wp = FreeCAD.draft_working_planes[1][i]
|
|
wp._update_gui()
|
|
else:
|
|
get_working_plane()
|
|
except Exception:
|
|
pass
|
|
|
|
def _view_observer_callback(sub_win):
|
|
if sub_win is None:
|
|
return
|
|
view = gui_utils.get_3d_view()
|
|
if view is None:
|
|
return
|
|
if not hasattr(FreeCADGui, "draftToolBar"):
|
|
return
|
|
tray = FreeCADGui.draftToolBar.tray
|
|
if tray is None:
|
|
return
|
|
if FreeCADGui.draftToolBar.tray.isVisible() is False:
|
|
return
|
|
ToDo.delay(_update_gui, None)
|
|
|
|
_view_observer_active = False
|
|
|
|
def _view_observer_start():
|
|
mw = FreeCADGui.getMainWindow()
|
|
mdi = mw.findChild(QtWidgets.QMdiArea)
|
|
global _view_observer_active
|
|
if not _view_observer_active:
|
|
mdi.subWindowActivated.connect(_view_observer_callback)
|
|
_view_observer_active = True
|
|
_view_observer_callback(mdi.activeSubWindow()) # Trigger initial update.
|
|
|
|
def _view_observer_stop():
|
|
mw = FreeCADGui.getMainWindow()
|
|
mdi = mw.findChild(QtWidgets.QMdiArea)
|
|
global _view_observer_active
|
|
if _view_observer_active:
|
|
mdi.subWindowActivated.disconnect(_view_observer_callback)
|
|
_view_observer_active = False
|
|
|
|
|
|
# Compatibility function (v1.0, 2023):
|
|
def getPlacementFromPoints(points):
|
|
"""Return a placement from a list of 3 or 4 points. The 4th point is no longer used.
|
|
|
|
Calls DraftGeomUtils.placement_from_points(). See there.
|
|
"""
|
|
utils.use_instead("DraftGeomUtils.placement_from_points")
|
|
return DraftGeomUtils.placement_from_points(*points[:3])
|
|
|
|
|
|
# Compatibility function (v1.0, 2023):
|
|
def getPlacementFromFace(face, rotated=False):
|
|
"""Return a placement from a face.
|
|
|
|
Calls DraftGeomUtils.placement_from_face(). See there.
|
|
"""
|
|
utils.use_instead("DraftGeomUtils.placement_from_face")
|
|
return DraftGeomUtils.placement_from_face(face, rotated=rotated)
|