cadquery-freecad-module/ThirdParty/cqparts_fasteners/utils/evaluator.py

269 lines
8.1 KiB
Python

import cadquery
from copy import copy
import logging
from cqparts.utils import CoordSystem
from cqparts.utils.misc import property_buffered
from . import _casting
log = logging.getLogger(__name__)
# --------------------- Effect ----------------------
class Effect(object):
pass
class VectorEffect(Effect):
"""
An evaluator effect is the conclusion to an evaluation with regard to
a single solid.
Effects are sortable (based on proximity to evaluation origin)
"""
def __init__(self, location, part, result):
"""
:param location: where the fastener is to be applied (eg: for a screw
application will be along the -Z axis)
:type location: :class:`CoordSystem`
:param part: effected solid
:type part: cadquery.Workplane
:param result: result of evaluation
:type result: cadquery.Workplane
"""
self.location = location
self.part = part
self.result = result
@property
def start_point(self):
"""
Start vertex of effect
:return: vertex (as vector)
:rtype: :class:`cadquery.Vector`
"""
edge = self.result.wire().val().Edges()[0]
return edge.Vertices()[0].Center()
@property
def start_coordsys(self):
"""
Coordinate system at start of effect.
All axes are parallel to the original vector evaluation location, with
the origin moved to this effect's start point.
:return: coordinate system at start of effect
:rtype: :class:`CoordSys`
"""
coordsys = copy(self.location)
coordsys.origin = self.start_point
return coordsys
@property
def end_point(self):
"""
End vertex of effect
:return: vertex (as vector)
:rtype: :class:`cadquery.Vector`
"""
edge = self.result.wire().val().Edges()[-1]
return edge.Vertices()[-1].Center()
@property
def end_coordsys(self):
"""
Coordinate system at end of effect.
All axes are parallel to the original vector evaluation location, with
the origin moved to this effect's end point.
:return: coordinate system at end of effect
:rtype: :class:`CoordSys`
"""
coordsys = copy(self.location)
coordsys.origin = self.end_point
return coordsys
@property
def origin_displacement(self):
"""
planar distance of start point from self.location along :math:`-Z` axis
"""
return self.start_point.sub(self.location.origin).dot(-self.location.zDir)
@property
def wire(self):
edge = cadquery.Edge.makeLine(self.start_point, self.end_point)
return cadquery.Wire.assembleEdges([edge])
@property
def _wire_wp(self):
"""Put self.wire in it's own workplane for display purposes"""
return cadquery.Workplane('XY').newObject([self.wire])
# bool
def __bool__(self):
if self.result.edges().objects:
return True
return False
__nonzero__ = __bool__
# Comparisions
def __lt__(self, other):
return self.origin_displacement < other.origin_displacement
def __le__(self, other):
return self.origin_displacement <= other.origin_displacement
def __gt__(self, other):
return self.origin_displacement > other.origin_displacement
def __ge__(self, other):
return self.origin_displacement >= other.origin_displacement
# --------------------- Evaluator ----------------------
class Evaluator(object):
"""
An evaluator determines which parts may be effected by a fastener, and how.
"""
# Constructor
def __init__(self, parts, parent=None):
"""
:param parts: parts involved in fastening
:type parts: list of :class:`cqparts.Part`
:param parent: parent object
:type parent: :class:`Fastener <cqparts_fasteners.fasteners.base.Fastener>`
"""
# All evaluators will take a list of parts
self.parts = parts
self.parent = parent
def perform_evaluation(self):
"""
Evaluate the given parts using any additional parameters passed
to this instance.
.. note::
Override this funciton in your *evaluator* class to assess what
parts are effected, and how.
Default behaviour: do nothing, return nothing
:return: ``None``
"""
return None
@property_buffered
def eval(self):
"""
Return the result of :meth:`perform_evaluation`, and buffer it so it's
only run once per :class:`Evaluator` instance.
:return: result from :meth:`perform_evaluation`
"""
return self.perform_evaluation()
class VectorEvaluator(Evaluator):
effect_class = VectorEffect
def __init__(self, parts, location, parent=None):
"""
:param parts: parts involved in fastening
:type parts: list of :class:`cqparts.Part`
:param location: where the fastener is to be applied (eg: for a screw
application will be along the -Z axis)
:type location: :class:`CoordSystem`
:param parent: parent object
:type parent: :class:`Fastener <cqparts_fasteners.fasteners.base.Fastener>`
**Location**
The orientation of ``location`` may not be important; it may be for a
basic application of a screw, in which case the :math:`-Z` axis will be
used to perform the evaluation, and the :math:`X` and :math`Y` axes are
of no consequence.
For *some* fasteners, the orientation of ``location`` will be
important.
"""
super(VectorEvaluator, self).__init__(
parts=parts,
parent=parent,
)
self.location = location
@property_buffered
def max_effect_length(self):
"""
:return: The longest possible effect vector length.
:rtype: float
In other words, the *radius* of a sphere:
- who's center is at ``start``.
- all ``parts`` are contained within the sphere.
"""
# Method: using each solid's bounding box:
# - get vector from start to bounding box center
# - get vector from bounding box center to any corner
# - add the length of both vectors
# - return the maximum of these from all solids
def max_length_iter():
for part in self.parts:
if part.local_obj.findSolid():
bb = part.local_obj.findSolid().BoundingBox()
yield abs(bb.center - self.location.origin) + (bb.DiagonalLength / 2)
try:
return max(max_length_iter())
except ValueError as e:
# if iter returns before yielding anything
return 0
def perform_evaluation(self):
"""
Determine which parts lie along the given vector, and what length
:return: effects on the given parts (in order of the distance from
the start point)
:rtype: list(:class:`VectorEffect`)
"""
# Create effect vector (with max length)
if not self.max_effect_length:
# no effect is possible, return an empty list
return []
edge = cadquery.Edge.makeLine(
self.location.origin,
self.location.origin + (self.location.zDir * -(self.max_effect_length + 1)) # +1 to avoid rounding errors
)
wire = cadquery.Wire.assembleEdges([edge])
wp = cadquery.Workplane('XY').newObject([wire])
effect_list = [] # list of self.effect_class instances
for part in self.parts:
solid = part.world_obj.translate((0, 0, 0))
intersection = solid.intersect(copy(wp))
effect = self.effect_class(
location=self.location,
part=part,
result=intersection,
)
if effect:
effect_list.append(effect)
return sorted(effect_list)
class CylinderEvaluator(Evaluator):
effect_class = VectorEffect