Files
create/src/Mod/Draft/WorkingPlane.py

971 lines
35 KiB
Python

## @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.
"""
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.
"""
# ***************************************************************************
# * *
# * Copyright (c) 2009, 2010 *
# * Ken Cline <cline@frii.com> *
# * *
# * 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 *
# * *
# ***************************************************************************
import FreeCAD, math, DraftVecUtils
from FreeCAD import Vector
__title__ = "FreeCAD Working Plane utility"
__author__ = "Ken Cline"
__url__ = "http://www.freecadweb.org"
class plane:
"""A WorkPlane object.
Attributes
----------
doc : App::Document
The active document. Reset view when `doc` changes.
weak : bool
It is `True` if the plane has been defined by `setup()`
or has been reset.
u : Base::Vector3
An axis (vector) that helps define the working plane.
v : Base::Vector3
An axis (vector) that helps define the working plane.
axis : Base::Vector3
A vector that is supposed to be perpendicular to `u` and `v`;
it is helpful although redundant.
position : Base::Vector3
A point throught which the plane goes through,
that helps define the working plane.
stored : bool
A 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)):
"""Initialize the working plane.
Parameters
----------
u : Base::Vector3, optional
An axis (vector) that helps define the working plane.
It defaults to `(1, 0, 0)`, or the +X axis.
v : Base::Vector3, optional
An axis (vector) that helps define the working plane.
It defaults to `(0, 1, 0)`, or the +Y axis.
w : Base::Vector3, optional
An axis that is supposed to be perpendicular to `u` and `v`;
it is redundant.
It defaults to `(0, 0, 1)`, or the +Z axis.
pos : Base::Vector3, optional
A point through which the plane goes through.
It defaults to the origin `(0, 0, 0)`.
"""
# keep track of active document. Reset view when doc changes.
self.doc = None
self.weak = True
self.u = u
self.v = v
self.axis = w
self.position = pos
# a placeholder for a stored state
self.stored = None
def __repr__(self):
"""Show the string representation of the object."""
return "Workplane x="+str(DraftVecUtils.rounded(self.u))+" y="+str(DraftVecUtils.rounded(self.v))+" z="+str(DraftVecUtils.rounded(self.axis))
def copy(self):
"""Return a new plane that is a copy of the present object."""
return plane(u=self.u, v=self.v, w=self.axis, pos=self.position)
def offsetToPoint(self, p, direction=None):
"""Return the signed distance from a point to the plane.
Parameters
----------
p : Base::Vector3
The external point to consider.
direction : Base::Vector3, optional
The unit vector that indicates the direction of the distance.
It defaults to `None`, which then uses the `plane.axis` (normal)
value, meaning that the measured distance is perpendicular
to the plane.
Returns
-------
float
The distance from the point to the plane.
Notes
-----
The signed distance `d`, from `p` to the plane, is such that
::
x = p + d*direction,
where `x` is a point that lies on the plane.
The `direction` is a unit vector that specifies the direction
in which the distance is measured.
It defaults to `plane.axis`,
meaning that it is the perpendicular distance.
A picture will help explain the computation
::
p
//|
/ / |
d / / | axis
/ / |
/ / |
-------- plane -----x-----c-----a--------
The points are as follows
* `p` is an arbitraty point outside the plane.
* `c` is a known point on the plane,
for example, `plane.position`.
* `x` is the intercept on the plane from `p` in
the desired `direction`.
* `a` is the perpendicular intercept on the plane,
i.e. along `plane.axis`.
The distance is calculated through the dot product
of the vector `pc` (going from point `p` to point `c`,
both of which are known) with the unit vector `direction`
(which is provided or defaults to `plane.axis`).
::
d = pc . direction
d = (c - p) . direction
**Warning:** this implementation doesn't calculate the entire
distance `|xp|`, only the distance `|pc|` projected onto `|xp|`.
Trigonometric relationships
---------------------------
In 2D the distances can be calculated by trigonometric relationships
::
|ap| = |cp| cos(apc) = |xp| cos(apx)
Then the desired distance is `d = |xp|`
::
|xp| = |cp| cos(apc) / cos(apx)
The cosines can be obtained from the definition of the dot product
::
A . B = |A||B| cos(angleAB)
If one vector is a unit vector
::
A . uB = |A| cos(angleAB)
cp . axis = |cp| cos(apc)
and if both vectors are unit vectors
::
uA . uB = cos(angleAB).
direction . axis = cos(apx)
Then
::
d = (cp . axis) / (direction . axis)
**Note:** for 2D these trigonometric operations
produce the full `|xp|` distance.
"""
if direction == None: direction = self.axis
return direction.dot(self.position.sub(p))
def projectPoint(self, p, direction=None):
"""Project a point onto the plane, by default orthogonally.
Parameters
----------
p : Base::Vector3
The point to project.
direction : Base::Vector3, optional
The unit vector that indicates the direction of projection.
It defaults to `None`, which then uses the `plane.axis` (normal)
value, meaning that the point is projected perpendicularly
to the plane.
Returns
-------
Base::Vector3
The projected vector, scaled to the appropriate distance.
"""
if not direction:
direction = self.axis
lp = self.getLocalCoords(p)
gp = self.getGlobalCoords(Vector(lp.x, lp.y, 0))
a = direction.getAngle(gp.sub(p))
if a > math.pi/2:
direction = direction.negative()
a = math.pi - a
ld = self.getLocalRot(direction)
gd = self.getGlobalRot(Vector(ld.x, ld.y, 0))
hyp = abs(math.tan(a) * lp.z)
return gp.add(DraftVecUtils.scaleTo(gd, hyp))
def projectPointOld(self, p, direction=None):
"""Project a point onto the plane. OBSOLETE.
Parameters
----------
p : Base::Vector3
The point to project.
direction : Base::Vector3, optional
The unit vector that indicates the direction of projection.
It defaults to `None`, which then uses the `plane.axis` (normal)
value, meaning that the point is projected perpendicularly
to the plane.
Returns
-------
Base::Vector3
The projected point,
or the original point if the angle between the `direction`
and the `plane.axis` is 90 degrees.
"""
if not direction:
direction = self.axis
t = Vector(direction)
#t.normalize()
a = round(t.getAngle(self.axis), DraftVecUtils.precision())
pp = round((math.pi)/2, DraftVecUtils.precision())
if a == pp:
return p
t.multiply(self.offsetToPoint(p, direction))
return p.add(t)
def alignToPointAndAxis(self, point, axis, offset=0, upvec=None):
"""Align the working plane to a point and an axis (vector).
Set `v` as the cross product of `axis` with `(1, 0, 0)` or `+X`,
and `u` as `v` rotated -90 degrees around the `axis`.
Also set `weak` to `False`.
Parameters
----------
point : Base::Vector3
The new `position` of the plane, adjusted by
the `offset`.
axis : Base::Vector3
A vector whose unit vector will be used as the new `axis`
of the plane.
If it is very close to the `X` or `-X` axes,
it will use this axis exactly, and will adjust `u` and `v`
to `+Y` and `+Z`, or `-Y` and `+Z`, respectively.
offset : float, optional
Defaults to zero. A value which will be used to offset
the plane in the direction of its `axis`.
upvec : Base::Vector3, optional
Defaults to `None`.
If it exists, its unit vector will be used as `v`,
and will set `u` as the cross product of `v` with `axis`.
"""
self.doc = FreeCAD.ActiveDocument
self.axis = axis
self.axis.normalize()
if axis.getAngle(Vector(1, 0, 0)) < 0.00001:
self.axis = Vector(1, 0, 0)
self.u = Vector(0, 1, 0)
self.v = Vector(0, 0, 1)
elif axis.getAngle(Vector(-1, 0, 0)) < 0.00001:
self.axis = Vector(-1, 0, 0)
self.u = Vector(0, -1, 0)
self.v = Vector(0, 0, 1)
elif upvec:
self.v = upvec
self.v.normalize()
self.u = self.v.cross(self.axis)
else:
self.v = axis.cross(Vector(1, 0, 0))
self.v.normalize()
self.u = DraftVecUtils.rotate(self.v, -math.pi/2, self.axis)
offsetVector = Vector(axis)
offsetVector.multiply(offset)
self.position = point.add(offsetVector)
self.weak = False
# FreeCAD.Console.PrintMessage("(position = " + str(self.position) + ")\n")
# FreeCAD.Console.PrintMessage("Current workplane: x="+str(DraftVecUtils.rounded(self.u))+" y="+str(DraftVecUtils.rounded(self.v))+" z="+str(DraftVecUtils.rounded(self.axis))+"\n")
def alignToPointAndAxis_SVG(self, point, axis, offset=0):
"""Align the working plane to a point and an axis (vector).
It aligns `u` and `v` based on the magnitude of the components
of `axis`.
Also set `weak` to `False`.
Parameters
----------
point : Base::Vector3
The new `position` of the plane, adjusted by
the `offset`.
axis : Base::Vector3
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`.
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.doc = FreeCAD.ActiveDocument
self.axis = axis
self.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)
#FreeCAD.Console.PrintMessage(projcase + " spat Prod = " + str(spat_res) + "\n")
offsetVector = Vector(axis); offsetVector.multiply(offset)
self.position = point.add(offsetVector)
self.weak = False
# FreeCAD.Console.PrintMessage("(position = " + str(self.position) + ")\n")
# FreeCAD.Console.PrintMessage("Current workplane: x="+str(DraftVecUtils.rounded(self.u))+" y="+str(DraftVecUtils.rounded(self.v))+" z="+str(DraftVecUtils.rounded(self.axis))+"\n")
def alignToCurve(self, shape, offset=0):
"""Align plane to curve. NOT YET IMPLEMENTED.
Parameters
----------
shape : Part.Shape
A curve that will serve to align the plane.
It can be an `'Edge'` or `'Wire'`.
offset : float
Defaults to zero. A value which will be used to offset
the plane in the direction of its `axis`.
Returns
-------
False
Returns `False` if the shape is null.
Currently it always returns `False`.
"""
if shape.isNull():
return False
elif shape.ShapeType == 'Edge':
#??? TODO: process curve here. look at shape.edges[0].Curve
return False
elif shape.ShapeType == 'Wire':
#??? TODO: determine if edges define a plane
return False
else:
return False
def alignToEdges(self, edges):
"""Align plane to two edges.
Uses the two points of the first edge to define the direction
of the unit vector `u`, the other two points of the other edge
to define the other unit vector `v`, and then the cross product
of `u` with `v` to define the `axis`.
Parameters
----------
edges : list
A list of two edges.
Returns
-------
False
Return `False` if `edges` is a list of more than 2 elements.
"""
# use a list of edges to find a plane position
if len(edges) > 2:
return False
# for axes systems, we suppose the 2 first edges are parallel
# ??? TODO: exclude other cases first
v1 = edges[0].Vertexes[-1].Point.sub(edges[0].Vertexes[0].Point)
v2 = edges[1].Vertexes[0].Point.sub(edges[0].Vertexes[0].Point)
v3 = v1.cross(v2)
v1.normalize()
v2.normalize()
v3.normalize()
#print v1,v2,v3
self.u = v1
self.v = v2
self.axis = v3
def alignToFace(self, shape, offset=0):
"""Align the plane to a face.
It uses the center of mass of the face as `position`,
and its normal in the center of the face as `axis`,
then calls `alignToPointAndAxis(position, axis, offset)`.
If the face is a quadrilateral, then it adjusts the position
of the plane according to its reported X direction and Y direction.
Also set `weak` to `False`.
Parameter
--------
shape : Part.Shape
A shape of type `'Face'`.
offset : float
Defaults to zero. A value which will be used to offset
the plane in the direction of its `axis`.
Returns
-------
bool
`True` if the operation was succesful, and `False` if the shape
is not a `'Face'`.
See Also
--------
alignToPointAndAxis, DraftGeomUtils.getQuad
"""
# Set face to the unique selected face, if found
if shape.ShapeType == 'Face':
self.alignToPointAndAxis(shape.Faces[0].CenterOfMass, shape.Faces[0].normalAt(0, 0), offset)
import DraftGeomUtils
q = DraftGeomUtils.getQuad(shape)
if q:
self.u = q[1]
self.v = q[2]
if not DraftVecUtils.equals(self.u.cross(self.v), self.axis):
self.u = q[2]
self.v = q[1]
if DraftVecUtils.equals(self.u, Vector(0, 0, 1)):
# the X axis is vertical: rotate 90 degrees
self.u, self.v = self.v.negative(), self.u
elif DraftVecUtils.equals(self.u, Vector(0, 0, -1)):
self.u, self.v = self.v, self.u.negative()
self.weak = False
return True
else:
return False
def alignTo3Points(self, p1, p2, p3, offset=0):
"""Align the plane to three points.
It makes a closed quadrilateral face with the three points,
and then calls `alignToFace(shape, offset)`.
Parameter
---------
p1 : Base::Vector3
The first point.
p2 : Base::Vector3
The second point.
p3 : Base::Vector3
The third point.
offset : float
Defaults to zero. A value which will be used to offset
the plane in the direction of its `axis`.
Returns
-------
bool
`True` if the operation was succesful, and `False` otherwise.
"""
import Part
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 if it defines a plane.
If the selection uniquely defines a plane it will be used.
Currently it only works with one object selected, a `'Face'`.
It extracts the shape of the object or subobject
and then calls `alignToFace(shape, offset)`.
This method only works when `FreeCAD.GuiUp` is `True`,
that is, when the graphical interface is loaded.
Parameter
---------
offset : float
Defaults to zero. A value which will be used to offset
the plane in the direction of its `axis`.
Returns
-------
bool
`True` if the operation was succesful, and `False` otherwise.
It returns `False` if the selection has no elements,
or if it has more than one element,
or if the object is not derived from `'Part::Feature'`
or if the object doesn't have a `Shape`.
To do
-----
The method returns `False` if the selection list has more than
one element.
The method should search the list for objects like faces, points,
edges, wires, etc., and call the appropriate aligning submethod.
The method could work for curves (`'Edge'` or `'Wire'`) but
`alignToCurve()` isn't fully implemented.
When the interface is not loaded it should fail and print
a message, `FreeCAD.Console.PrintError()`.
See also
--------
alignToFace, alignToCurve
"""
import FreeCADGui
sex = FreeCADGui.Selection.getSelectionEx(FreeCAD.ActiveDocument.Name)
if len(sex) == 0:
return False
elif len(sex) == 1:
if not sex[0].Object.isDerivedFrom("Part::Feature") \
or not sex[0].Object.Shape:
return False
return self.alignToCurve(sex[0].Object.Shape, offset) \
or self.alignToFace(sex[0].Object.Shape, offset) \
or (len(sex[0].SubObjects) == 1 and self.alignToFace(sex[0].SubObjects[0], offset))
else:
# len(sex) > 2, look for point and line, three points, etc.
return False
def setup(self, direction=None, point=None, upvec=None):
"""Setup the working plane if it exists but is undefined.
If `direction` and `point` are present,
it calls `alignToPointAndAxis(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::Vector3, optional
It defaults to `None`. It is the new `axis` of the plane.
point : Base::Vector3, optional
It defaults to `None`. It is the new `position` of the plane.
upvec : Base::Vector3, optional
It defaults to `None`. It is the new `v` orientation of the plane.
To do
-----
When the interface is not loaded it should fail and print
a message, `FreeCAD.Console.PrintError()`.
"""
if self.weak:
if direction and point:
self.alignToPointAndAxis(point, direction, 0, upvec)
else:
try:
import FreeCADGui
from pivy import coin
rot = FreeCADGui.ActiveDocument.ActiveView.getCameraNode().getField("orientation").getValue()
upvec = Vector(rot.multVec(coin.SbVec3f(0, 1, 0)).getValue())
vdir = FreeCADGui.ActiveDocument.ActiveView.getViewDirection()
if (vdir.getAngle(self.axis) > 0.001) and (vdir.getAngle(self.axis) < 3.14159):
# don't change the WP if it is already perpendicular to the current view
self.alignToPointAndAxis(Vector(0, 0, 0), vdir.negative(), 0, upvec)
except:
pass
self.weak = True
def reset(self):
"""Reset the plane.
Set the `doc` attribute to `None`, and `weak` to `True`.
"""
self.doc = None
self.weak = True
def getRotation(self):
"""Return a placement describing the plane orientation only.
If `FreeCAD.GuiUp` is `True`, that is, if the graphical interface
is loaded, it will test if the active object is an `Arch` container
and will calculate the placement accordingly.
Returns
-------
Base::Placement
A placement, comprised of a `Base` (`Base::Vector3`),
and a `Rotation` (`Base::Rotation`).
"""
m = DraftVecUtils.getPlaneRotation(self.u, self.v, self.axis)
p = FreeCAD.Placement(m)
# Arch active container
if FreeCAD.GuiUp:
import FreeCADGui
if FreeCADGui.ActiveDocument.ActiveView:
a = FreeCADGui.ActiveDocument.ActiveView.getActiveObject("Arch")
if a:
p = a.Placement.inverse().multiply(p)
return p
def getPlacement(self, rotated=False):
"""Return the placement of the plane.
Parameters
----------
rotated : bool, optional
It defaults to `False`. If it is `True`, it switches `axis`
with `-v` to produce a rotated placement.
Returns
-------
Base::Placement
A placement, comprised of a `Base` (`Base::Vector3`),
and a `Rotation` (`Base::Rotation`).
"""
if rotated:
m = FreeCAD.Matrix(
self.u.x, self.axis.x, -self.v.x, self.position.x,
self.u.y, self.axis.y, -self.v.y, self.position.y,
self.u.z, self.axis.z, -self.v.z, self.position.z,
0.0, 0.0, 0.0, 1.0)
else:
m = FreeCAD.Matrix(
self.u.x, self.v.x, self.axis.x, self.position.x,
self.u.y, self.v.y, self.axis.y, self.position.y,
self.u.z, self.v.z, self.axis.z, self.position.z,
0.0, 0.0, 0.0, 1.0)
p = FreeCAD.Placement(m)
# Arch active container if based on App Part
#if FreeCAD.GuiUp:
# import FreeCADGui
# a = FreeCADGui.ActiveDocument.ActiveView.getActiveObject("Arch")
# if a:
# p = a.Placement.inverse().multiply(p)
return p
def getNormal(self):
"""Return the normal vector of the plane (axis).
Returns
-------
Base::Vector3
The `axis` attribute of the plane.
"""
n = self.axis
# Arch active container if based on App Part
#if FreeCAD.GuiUp:
# import FreeCADGui
# a = FreeCADGui.ActiveDocument.ActiveView.getActiveObject("Arch")
# if a:
# n = a.Placement.inverse().Rotation.multVec(n)
return n
def setFromPlacement(self, pl, rebase=False):
"""Set the plane from a placement.
It normally uses only the rotation, unless `rebase` is `True`.
Parameters
----------
pl : Base::Placement or Base::Matrix4D
A placement, comprised of a `Base` (`Base::Vector3`),
and a `Rotation` (`Base::Rotation`),
or a `Base::Matrix4D` that defines a placement.
rebase : bool, optional
It defaults to `False`.
If `True`, it will use `pl.Base` as the new `position`
of the plane. Otherwise it will only consider `pl.Rotation`.
To do
-----
If `pl` is a `Base::Matrix4D`, it shouldn't try to use `pl.Base`
because a matrix has no `Base`.
"""
rot = FreeCAD.Placement(pl).Rotation
self.u = rot.multVec(FreeCAD.Vector(1, 0, 0))
self.v = rot.multVec(FreeCAD.Vector(0, 1, 0))
self.axis = rot.multVec(FreeCAD.Vector(0, 0, 1))
if rebase:
self.position = pl.Base
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`.
"""
"restores a previously saved plane state, if exists"
if self.stored:
self.u = self.stored[0]
self.v = self.stored[1]
self.axis = self.stored[2]
self.position = self.stored[3]
self.weak = self.stored[4]
self.stored = None
def getLocalCoords(self, point):
"""Return the coordinates of a given point projected on the plane.
A vector is calculated from the plane's `position`
to the external `point`, and this vector is projected into
each of the `u`, `v` and `axis` of the plane.
Parameters
----------
point : Base::Vector3
The point external to the plane.
Returns
-------
Base::Vector3
The point projected on the plane.
"""
pt = point.sub(self.position)
xv = DraftVecUtils.project(pt, self.u)
x = xv.Length
# If the angle between the projection xv and u
# is larger than 1 radian (57.29 degrees), use the negative
# of the magnitude. Why exactly 1 radian?
if xv.getAngle(self.u) > 1:
x = -x
yv = DraftVecUtils.project(pt, self.v)
y = yv.Length
if yv.getAngle(self.v) > 1:
y = -y
zv = DraftVecUtils.project(pt, self.axis)
z = zv.Length
if zv.getAngle(self.axis) > 1:
z = -z
return Vector(x, y, z)
def getGlobalCoords(self, point):
"returns the global coordinates of the given point, taken relatively to this working plane"
vx = Vector(self.u).multiply(point.x)
vy = Vector(self.v).multiply(point.y)
vz = Vector(self.axis).multiply(point.z)
pt = (vx.add(vy)).add(vz)
return pt.add(self.position)
def getLocalRot(self, point):
"Same as getLocalCoords, but discards the WP position"
xv = DraftVecUtils.project(point, self.u)
x = xv.Length
if xv.getAngle(self.u) > 1:
x = -x
yv = DraftVecUtils.project(point, self.v)
y = yv.Length
if yv.getAngle(self.v) > 1:
y = -y
zv = DraftVecUtils.project(point, self.axis)
z = zv.Length
if zv.getAngle(self.axis) > 1:
z = -z
return Vector(x, y, z)
def getGlobalRot(self, point):
"Same as getGlobalCoords, but discards the WP position"
vx = Vector(self.u).multiply(point.x)
vy = Vector(self.v).multiply(point.y)
vz = Vector(self.axis).multiply(point.z)
pt = (vx.add(vy)).add(vz)
return pt
def getClosestAxis(self, point):
"returns which of the workingplane axes is closest from the given vector"
ax = point.getAngle(self.u)
ay = point.getAngle(self.v)
az = point.getAngle(self.axis)
bx = point.getAngle(self.u.negative())
by = point.getAngle(self.v.negative())
bz = point.getAngle(self.axis.negative())
b = min(ax, ay, az, bx, by, bz)
if b in [ax, bx]:
return "x"
elif b in [ay, by]:
return "y"
elif b in [az, bz]:
return "z"
else:
return None
def isGlobal(self):
"returns True if the plane axes are equal to the global axes"
if self.u != Vector(1, 0, 0):
return False
if self.v != Vector(0, 1, 0):
return False
if self.axis != Vector(0, 0, 1):
return False
return True
def isOrtho(self):
"returns True if the plane axes are following the global axes"
if round(self.u.getAngle(Vector(0, 1, 0)), 6) in [0, -1.570796, 1.570796, -3.141593, 3.141593, -4.712389, 4.712389, 6.283185]:
if round(self.v.getAngle(Vector(0, 1, 0)), 6) in [0, -1.570796, 1.570796, -3.141593, 3.141593, -4.712389, 4.712389, 6.283185]:
if round(self.axis.getAngle(Vector(0, 1, 0)), 6) in [0, -1.570796, 1.570796, -3.141593, 3.141593, -4.712389, 4.712389, 6.283185]:
return True
return False
def getDeviation(self):
"returns the deviation angle between the u axis and the horizontal plane"
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 getPlacementFromPoints(points):
"returns a placement from a list of 3 or 4 vectors"
pl = plane()
try:
pl.position = points[0]
pl.u = (points[1].sub(points[0]).normalize())
pl.v = (points[2].sub(points[0]).normalize())
if len(points) == 4:
pl.axis = (points[3].sub(points[0]).normalize())
else:
pl.axis = ((pl.u).cross(pl.v)).normalize()
except:
return None
p = pl.getPlacement()
del pl
return p
def getPlacementFromFace(face, rotated=False):
"returns a placement from a face"
pl = plane()
try:
pl.alignToFace(face)
except:
return None
p = pl.getPlacement(rotated)
del pl
return p