Skip to content

Commit

Permalink
Merge pull request #62 from gumyr/dev
Browse files Browse the repository at this point in the history
Fixes and Draft diagonal dimensions
  • Loading branch information
gumyr authored Aug 18, 2022
2 parents 8ef22b5 + 6696d53 commit 580833a
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 56 deletions.
29 changes: 29 additions & 0 deletions docs/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ Shape class extensions
***********************
Sketch class extensions
***********************
The Sketch extensions introduce a new way of specifying a point: a "snap" defined
as `"<tag>@<position>"` which allows one to easily create features relative to previously
tagged features.

.. glossary::

<tag>
<tag> refers to a previously defined tagged Edge or Wire.

<position>
A float or int value between 0 and 1 which refers to a relative position along
the object. A value of 0 (or 0.0) refers to the start of the Edge while
1 (or 1.0) refers to the end of the Edge. The middle of the Edge would be "my_edge@0.5".

In addition to the numeric values, three string values can be used: `start`, `middle`,
and `end`. For example: "perimeter@middle" will return the point half way along the
previously tagged object `perimeter`.

Here is an example of using a pair of "snaps" to define a chord:

.. code-block:: python
chord = (
cq.Sketch()
.center_arc(center=(0, 0), radius=10, start_angle=0, arc_size=60, tag="c")
.polyline("c@1", "c@0")
.assemble()
)
.. autoclass:: Sketch
:members:
Expand Down
63 changes: 63 additions & 0 deletions examples/drafting_dimension_diagonal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
An example of a documented a cadquery part with diagonal lines
name: drafting_dimension_diagonal.py
by: Gumyr
date: July 20th 2022
license:
Copyright 2022 Gumyr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import cadquery as cq
from cq_warehouse.drafting import Draft

# Create an object with two diagonal edges and extract them
target = (
cq.Workplane("XY").sketch().trapezoid(80, 60, 120, angle=180).finalize().extrude(-1)
)
top_left_edge = target.faces(">Z").edges("<X").val()
top_right_edge = target.faces(">Z").edges(">X").val()

# Initialize the Drafting class - with a new custom extension_gap
metric_drawing = Draft(decimal_precision=1, extension_gap=2)

# Create a diagonal extension line
left_extension_line = metric_drawing.extension_line(
object_edge=top_left_edge, offset=10
)

# Create two perpendicular extension lines
right_horizontal_extension_line = metric_drawing.extension_line(
object_edge=top_right_edge,
offset=40,
project_line=(1, 0),
)
right_vertical_extension_line = metric_drawing.extension_line(
object_edge=top_right_edge,
offset=25,
project_line=(0, 1),
)

# If running from within the cq-editor, show the extension lines
if "show_object" in locals():
show_object(target, name="target")
show_object(top_left_edge, name="bottom_left_edge")
show_object(top_right_edge, name="bottom_right_edge")
show_object(left_extension_line, name="left_extension_line")
show_object(right_horizontal_extension_line, name="right_horizontal_extension_line")
show_object(right_vertical_extension_line, name="right_vertical_extension_line")
75 changes: 65 additions & 10 deletions src/cq_warehouse/drafting.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Draft:
display_units (bool): control the display of units with numbers. Defaults to True.
decimal_precision (int): number of decimal places when displaying numbers. Defaults to 2.
fractional_precision (int): maximum fraction denominator - must be a factor of 2. Defaults to 64.
extension_gap (float): gap between the point and start of extension line in extension_line.
Example:
Expand Down Expand Up @@ -110,6 +111,7 @@ def __init__(
display_units: bool = True,
decimal_precision: int = 2,
fractional_precision: int = 64,
extension_gap: float = 0,
):
self.font_size = font_size
self.color = color
Expand All @@ -121,6 +123,7 @@ def __init__(
self.display_units = display_units
self.decimal_precision = decimal_precision
self.fractional_precision = fractional_precision
self.extension_gap = extension_gap

if not log2(fractional_precision).is_integer():
raise ValueError(
Expand Down Expand Up @@ -256,6 +259,23 @@ def _segment_line(path: Union[Edge, Wire], tip_pos: float, tail_pos: float) -> E
)
return sub_path

@staticmethod
def _project_wire(path: Wire, line: VectorLike) -> Wire:
"""Project a Wire to a line."""
if isinstance(line, Tuple):
line = Vector(line)
path_as_wire = Wire.assembleEdges(
Workplane()
.polyline(
[
path.startPoint().projectToLine(line),
path.endPoint().projectToLine(line),
]
)
.vals()
)
return path_as_wire

@staticmethod
def _path_to_wire(path: PathDescriptor) -> Wire:
"""Convert a PathDescriptor into a Wire"""
Expand Down Expand Up @@ -519,6 +539,7 @@ def extension_line(
arrows: Tuple[bool, bool] = (True, True),
tolerance: Optional[Union[float, Tuple[float, float]]] = None,
label_angle: bool = False,
project_line: Optional[VectorLike] = None,
) -> Assembly:
"""Extension Line
Expand All @@ -544,13 +565,19 @@ def extension_line(
label_angle (bool, optional): a flag indicating that instead of an extracted length
value, the size of the circular arc extracted from the path should be displayed
in degrees. Defaults to False.
project_line (Vector, optional): Vector line which to project dimension against.
Returns:
Assembly: the extension line
"""

# Create a wire modelling the path of the dimension lines from a variety of input types
object_path = Draft._path_to_wire(object_edge)
object_start = object_path.startPoint()
object_end = object_path.endPoint()
object_mid = 0.5 * object_start.add(object_end)
if project_line:
object_path = Draft._project_wire(object_path, project_line)
object_length = object_path.Length()

# Determine if the provided object edge is a circular arc and if so extract its radius
Expand Down Expand Up @@ -588,22 +615,50 @@ def extension_line(
else:
extension_tangent = object_path.tangentAt(0).cross(self._label_normal)
dimension_plane = Plane(
# origin=object_path.positionAt(0),
object_path.positionAt(0),
xDir=extension_tangent,
normal=self._label_normal,
)
ext_line = [
(
Workplane(dimension_plane)
.moveTo(copysign(1, offset) * 1.5 * MM, l)
.lineTo(offset + copysign(1, offset) * 3 * MM, l)
)
for l in [0, object_length]
]
# Extension line starts in the middle of the object.
extension_path = object_path.translate(
extension_tangent.normalized() * offset
-object_path.positionAt(0.5) + object_mid + extension_tangent * offset
)

projected_extension = dimension_plane.toLocalCoords(extension_path)
extension_start = projected_extension.startPoint()
extension_end = projected_extension.endPoint()

obj_start = dimension_plane.toLocalCoords(object_start)
obj_end = dimension_plane.toLocalCoords(object_end)

# If we can't get direction of extension lines then a dimension_line is better suited.
if obj_start == extension_start or obj_end == extension_end:
return self.dimension_line(
object_edge, label, arrows, tolerance, label_angle
)

start_extension_direction = (-obj_start + extension_start).normalized()
end_extension_direction = (-obj_end + extension_end).normalized()

if self.extension_gap:
obj_start = obj_start + (start_extension_direction * self.extension_gap)
obj_end = obj_end + (end_extension_direction * self.extension_gap)

# Extend the line past the arrow slightly.
extension_start = extension_start + start_extension_direction
extension_end = extension_end + end_extension_direction

ext_line1 = (
Workplane(dimension_plane)
.moveTo(*obj_start.toTuple()[:2])
.lineTo(*extension_start.toTuple()[:2])
)
ext_line2 = (
Workplane(dimension_plane)
.moveTo(*obj_end.toTuple()[:2])
.lineTo(*extension_end.toTuple()[:2])
)
ext_line = [ext_line1, ext_line2]

# Create the assembly
d_line = self.dimension_line(
Expand Down
61 changes: 32 additions & 29 deletions src/cq_warehouse/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@
from OCP.NCollection import NCollection_Utf8String

# Logging configuration - all cq_warehouse logs are level DEBUG or WARNING
logging.basicConfig(
filename="cq_warehouse.log",
encoding="utf-8",
# level=logging.DEBUG,
level=logging.CRITICAL,
format="%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)s - %(funcName)20s() ] - %(message)s",
)
# logging.basicConfig(
# filename="cq_warehouse.log",
# encoding="utf-8",
# # level=logging.DEBUG,
# level=logging.CRITICAL,
# format="%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)s - %(funcName)20s() ] - %(message)s",
# )
logging.getLogger("cq_warehouse").addHandler(logging.NullHandler())
logger = logging.getLogger("cq_warehouse")

"""
Expand Down Expand Up @@ -347,31 +349,32 @@ def _areObjectsValid(self) -> bool:
Assembly.areObjectsValid = _areObjectsValid


# def _crossSection_Assembly(self, plane: "Plane") -> "Assembly":
# """Cross Section
def _crossSection_Assembly(self, plane: "Plane") -> "Assembly":
"""Cross Section
# Generate a 2D slice of an assembly as a colorize Assembly
Generate a 2D slice of an assembly as a colorize Assembly
# Args:
# plane (Plane): the plane with which to slice the Assembly
Args:
plane (Plane): the plane with which to slice the Assembly
Returns:
Assembly: The cross section assembly with original colors
"""
plane_as_face = Face.makePlane(basePnt=plane.origin, dir=plane.zDir)

cross_section = cq.Assembly(None, name=self.name)
for name, part in self.traverse():
location = self.findLocation(name)
for shape in part.shapes:
cross_section.add(
shape.located(location).intersect(plane_as_face),
color=part.color,
name=name,
)
return cross_section

Assembly.section = _crossSection_Assembly

# Returns:
# Assembly: The cross section assembly with original colors
# """
# plane_as_face = Face.makePlane(basePnt=plane.origin, dir=plane.zDir)

# cross_section = cq.Assembly(None, name=self.name)
# for name, part in self.traverse():
# for shape in part.shapes:
# cross_section.add(
# shape.intersect(plane_as_face),
# loc=part.loc,
# color=part.color,
# name=name,
# )
# return cross_section

# Assembly.section = _crossSection_Assembly
"""
Plane extensions: toLocalCoords(), toWorldCoords()
Expand Down
11 changes: 11 additions & 0 deletions src/cq_warehouse/extensions_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ def areObjectsValid(self) -> bool:
Returns:
bool: all objects are valid
"""
def section(self, plane: "Plane") -> "Assembly":
"""Cross Section
Generate a 2D slice of an assembly as a colorize Assembly
Args:
plane (Plane): the plane with which to slice the Assembly
Returns:
Assembly: The cross section assembly with original colors
"""
class Plane(object):
def _toFromLocalCoords(
self, obj: Union["VectorLike", "Shape", "BoundBox"], to: bool = True
Expand Down
Loading

0 comments on commit 580833a

Please sign in to comment.