Draft: Introduction of the PlaneBase class

This PR introduces the new PlaneBase class.

Most of its functions are rewritten versions of the functions in the original Plane class, with the exception of align_to_point_and_axis_svg.

Note that after this PR the original Plane class does not yet inherit from PlaneBase. This will be done in several following PRs. I will then also explain the reasons behind some of the revised functions in PlaneBase.

The introduction of the PlaneGui class will follow after that.

Issue: #5603.
This commit is contained in:
Roy-043
2023-09-21 15:18:35 +02:00
parent 30e4fac88a
commit bb4f5c7eee

View File

@@ -1,5 +1,6 @@
# ***************************************************************************
# * 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) *
@@ -52,6 +53,544 @@ __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):
"""Sets 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):
"""Sets 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):
"""Sets 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:
"""A WorkPlane object.