Merge pull request #412 from mlampert/HoldingTags

Path: Holding tags dressup, partially fixes issue 2751
This commit is contained in:
Yorik van Havre 2017-01-03 09:47:03 -02:00 committed by GitHub
commit 7943ce4ce4
12 changed files with 1600 additions and 26 deletions

View File

@ -454,7 +454,7 @@ def findIntersection(edge1,edge2,infinite1=False,infinite2=False,ex1=False,ex2=F
return int
else:
# print("DraftGeomUtils: Unsupported curve type: (" + str(edge1.Curve) + ", " + str(edge2.Curve) + ")")
print("DraftGeomUtils: Unsupported curve type: (" + str(edge1.Curve) + ", " + str(edge2.Curve) + ")")
return []
def wiresIntersect(wire1,wire2):

View File

@ -26,6 +26,7 @@ SET(PathScripts_SRCS
PathScripts/PathDressup.py
PathScripts/PathDressupDogbone.py
PathScripts/PathDressupDragknife.py
PathScripts/PathDressupHoldingTags.py
PathScripts/PathDrilling.py
PathScripts/PathEngrave.py
PathScripts/PathFacePocket.py
@ -73,6 +74,7 @@ SET(PathScripts_SRCS
PathScripts/slic3r_pre.py
PathTests/PathTestUtils.py
PathTests/TestPathDepthParams.py
PathTests/TestPathDressupHoldingTags.py
PathTests/TestPathGeom.py
PathTests/TestPathPost.py
PathTests/__init__.py

View File

@ -53,6 +53,7 @@
<file>panels/DogboneEdit.ui</file>
<file>panels/DrillingEdit.ui</file>
<file>panels/EngraveEdit.ui</file>
<file>panels/HoldingTagsEdit.ui</file>
<file>panels/JobEdit.ui</file>
<file>panels/MillFaceEdit.ui</file>
<file>panels/PocketEdit.ui</file>

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>352</width>
<width>376</width>
<height>387</height>
</rect>
</property>
@ -27,8 +27,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>334</width>
<height>340</height>
<width>358</width>
<height>333</height>
</rect>
</property>
<attribute name="label">

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskPanel</class>
<widget class="QWidget" name="TaskPanel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>363</width>
<height>530</height>
</rect>
</property>
<property name="windowTitle">
<string>Holding Tags</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="0">
<widget class="QToolBox" name="toolBox">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tbpTags">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>345</width>
<height>476</height>
</rect>
</property>
<attribute name="label">
<string>Tags</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QFormLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Width</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="dsbWidth">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Specify the resulting width of tags at the base.&lt;/p&gt;&lt;p&gt;The initial default width is based on the longest edge found in the base path.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Height</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="dsbHeight">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Height of holding tags.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Angle </string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="dsbAngle">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Angle of ascend and descend of the tool for the holding tag cutout.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;If the angle is too flat for the given width to reach the specified height the resulting tag will have a triangular shape and not as high as specified.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QListWidget" name="lwTags">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;List of current tags.&lt;/p&gt;&lt;p&gt;Edit coordinates to move tag or invoker snapper tool.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QPushButton" name="pbDelete">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pbAdd">
<property name="text">
<string>Add...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Auto Generate</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">
<widget class="QSpinBox" name="sbCount"/>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pbGenerate">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Replace All</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Number of Tags</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -73,6 +73,7 @@ class PathWorkbench (Workbench):
from PathScripts import PathProfileEdges
from PathScripts import PathDressupDogbone
from PathScripts import PathMillFace
from PathScripts import PathDressupHoldingTags
import PathCommands
# build commands list
@ -82,7 +83,7 @@ class PathWorkbench (Workbench):
twodopcmdlist = ["Path_Contour", "Path_Profile", "Path_Profile_Edges", "Path_Pocket", "Path_Drilling", "Path_Engrave", "Path_MillFace"]
threedopcmdlist = ["Path_Surfacing"]
modcmdlist = ["Path_Copy", "Path_CompoundExtended", "Path_Array", "Path_SimpleCopy" ]
dressupcmdlist = ["PathDressup_Dogbone", "PathDressup_DragKnife"]
dressupcmdlist = ["PathDressup_Dogbone", "PathDressup_DragKnife", "PathDressup_HoldingTags"]
extracmdlist = ["Path_SelectLoop"]
#modcmdmore = ["Path_Hop",]
#remotecmdlist = ["Path_Remote"]
@ -134,8 +135,8 @@ class PathWorkbench (Workbench):
if len(FreeCADGui.Selection.getSelection()) == 1:
if FreeCADGui.Selection.getSelection()[0].isDerivedFrom("Path::Feature"):
self.appendContextMenu("", ["Path_Inspect"])
if FreeCADGui.Selection.getSelection()[0].Name in ["Profile", "Contour"]:
self.appendContextMenu("", ["Add_Tag"])
if "Profile" or "Contour" in FreeCADGui.Selection.getSelection()[0].Name:
#self.appendContextMenu("", ["Add_Tag"])
self.appendContextMenu("", ["Set_StartPoint"])
self.appendContextMenu("", ["Set_EndPoint"])
if "Remote" in FreeCADGui.Selection.getSelection()[0].Name:

File diff suppressed because it is too large Load Diff

View File

@ -63,7 +63,7 @@ class Side:
class PathGeom:
"""Class to transform Path Commands into Edges and Wire and back again.
The interface might eventuallly become part of Path itself."""
CmdMoveFast = ['G0', 'G00']
CmdMoveRapid = ['G0', 'G00']
CmdMoveStraight = ['G1', 'G01']
CmdMoveCW = ['G2', 'G02']
CmdMoveCCW = ['G3', 'G03']
@ -82,6 +82,18 @@ class PathGeom:
Return True if two points are roughly identical (see also isRoughly)."""
return cls.isRoughly(p1.x, p2.x, error) and cls.isRoughly(p1.y, p2.y, error) and cls.isRoughly(p1.z, p2.z, error)
@classmethod
def edgesMatch(cls, e0, e1, error=0.0000001):
"""(e0, e1, [error=0.0000001]
Return true if the edges start and end at the same point and have the same type of curve."""
if type(e0.Curve) != type(e1.Curve):
return False
if not cls.pointsCoincide(e0.valueAt(e0.FirstParameter), e1.valueAt(e1.FirstParameter)):
return False
if not cls.pointsCoincide(e0.valueAt(e0.LastParameter), e1.valueAt(e1.LastParameter)):
return False
return True
@classmethod
def edgeConnectsTo(cls, edge, vector):
"""(edge, vector)
@ -89,13 +101,13 @@ class PathGeom:
return cls.pointsCoincide(edge.valueAt(edge.FirstParameter), vector) or cls.pointsCoincide(edge.valueAt(edge.LastParameter), vector)
@classmethod
def getAngle(cls, vertex):
"""(vertex)
Returns the angle [-pi,pi] of a vertex using the X-axis as the reference.
def getAngle(cls, vector):
"""(vector)
Returns the angle [-pi,pi] of a vector using the X-axis as the reference.
Positive angles for vertexes in the upper hemishpere (positive y values)
and negative angles for the lower hemishpere."""
a = vertex.getAngle(FreeCAD.Vector(1,0,0))
if vertex.y < 0:
a = vector.getAngle(FreeCAD.Vector(1,0,0))
if vector.y < 0:
return -a
return a
@ -128,13 +140,76 @@ class PathGeom:
Convenience function to return the projection of the Vector in the XY-plane."""
return Vector(point.x, point.y, 0)
@classmethod
def cmdsForEdge(cls, edge, flip = False, useHelixForBSpline = True):
"""(edge, flip = False, useHelixForBSpline = True) -> List(Path.Command)
Returns a list of Path.Command representing the given edge.
If flip is True the edge is considered to be backwards.
If useHelixForBSpline is True an Edge based on a BSplineCurve is considered
to represent a helix and results in G2 or G3 command. Otherwise edge has
no direct Path.Command mapping and will be approximated by straight segments.
Approximation is also the approach for edges that are neither straight lines
nor arcs (nor helixes)."""
pt = edge.valueAt(edge.LastParameter) if not flip else edge.valueAt(edge.FirstParameter)
params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z}
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
commands = [Path.Command('G1', params)]
else:
if not flip:
p1 = edge.valueAt(edge.FirstParameter)
p3 = pt
else:
p1 = pt
p3 = edge.valueAt(edge.LastParameter)
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2)
if type(edge.Curve) == Part.Circle or (useHelixForBSpline and type(edge.Curve) == Part.BSplineCurve):
if Side.Left == Side.of(p2 - p1, p3 - p2):
cmd = 'G3'
else:
cmd = 'G2'
#print("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z))
pd = Part.Circle(PathGeom.xy(p1), PathGeom.xy(p2), PathGeom.xy(p3)).Center
pa = PathGeom.xy(p1)
pb = PathGeom.xy(p2)
pc = PathGeom.xy(p3)
#print("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pa.x, pa.y, pa.z, pc.x, pc.y, pc.z))
#print("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pb.x, pb.y, pb.z, pd.x, pd.y, pd.z))
offset = Part.Circle(PathGeom.xy(p1), PathGeom.xy(p2), PathGeom.xy(p3)).Center - p1
#print("**** (%.2f, %.2f, %.2f)" % (offset.x, offset.y, offset.z))
params.update({'I': offset.x, 'J': offset.y, 'K': (p3.z - p1.z)/2})
commands = [ Path.Command(cmd, params) ]
else:
eStraight = Part.Edge(Part.LineSegment(p1, p3))
esP2 = eStraight.valueAt((eStraight.FirstParameter + eStraight.LastParameter)/2)
deviation = (p2 - esP2).Length
if cls.isRoughly(deviation, 0):
return [ Path.Command('G1', {'X': p3.x, 'Y': p3.y, 'Z': p3.z}) ]
# at this point pixellation is all we can do
commands = []
segments = int(math.ceil((deviation / eStraight.Length) * 1000))
#print("**** pixellation with %d segments" % segments)
dParameter = (edge.LastParameter - edge.FirstParameter) / segments
for i in range(0, segments):
if flip:
p = edge.valueAt(edge.LastParameter - (i + 1) * dParameter)
else:
p = edge.valueAt(edge.FirstParameter + (i + 1) * dParameter)
cmd = Path.Command('G1', {'X': p.x, 'Y': p.y, 'Z': p.z})
#print("***** %s" % cmd)
commands.append(cmd)
#print commands
return commands
@classmethod
def edgeForCmd(cls, cmd, startPoint):
"""(cmd, startPoint).
Returns an Edge representing the given command, assuming a given startPoint."""
endPoint = cls.commandEndPoint(cmd, startPoint)
if (cmd.Name in cls.CmdMoveStraight) or (cmd.Name in cls.CmdMoveFast):
if (cmd.Name in cls.CmdMoveStraight) or (cmd.Name in cls.CmdMoveRapid):
if cls.pointsCoincide(startPoint, endPoint):
return None
return Part.Edge(Part.LineSegment(startPoint, endPoint))
if cmd.Name in cls.CmdMoveArc:
@ -184,13 +259,16 @@ class PathGeom:
"""(path, [startPoint=Vector(0,0,0)])
Returns a wire representing all move commands found in the given path."""
edges = []
rapid = []
if hasattr(path, "Commands"):
for cmd in path.Commands:
edge = cls.edgeForCmd(cmd, startPoint)
if edge:
if cmd.Name in cls.CmdMoveRapid:
rapid.append(edge)
edges.append(edge)
startPoint = cls.commandEndPoint(cmd, startPoint)
return Part.Wire(edges)
return (Part.Wire(edges), rapid)
@classmethod
def wiresForPath(cls, path, startPoint = FreeCAD.Vector(0, 0, 0)):
@ -203,7 +281,7 @@ class PathGeom:
if cmd.Name in cls.CmdMove:
edges.append(cls.edgeForCmd(cmd, startPoint))
startPoint = cls.commandEndPoint(cmd, startPoint)
elif cmd.Name in cls.CmdMoveFast:
elif cmd.Name in cls.CmdMoveRapid:
wires.append(Part.Wire(edges))
edges = []
startPoint = cls.commandEndPoint(cmd, startPoint)
@ -211,3 +289,81 @@ class PathGeom:
wires.append(Part.Wire(edges))
return wires
@classmethod
def arcToHelix(cls, edge, z0, z1):
"""(edge, z0, z1)
Assuming edge is an arc it'll return a helix matching the arc starting at z0 and rising/falling to z1."""
p1 = edge.valueAt(edge.FirstParameter)
p2 = edge.valueAt(edge.LastParameter)
cmd = cls.cmdsForEdge(edge)[0]
params = cmd.Parameters
params.update({'Z': z1, 'K': (z1 - z0)/2})
command = Path.Command(cmd.Name, params)
#print("- (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f): %.2f:%.2f" % (edge.Vertexes[0].X, edge.Vertexes[0].Y, edge.Vertexes[0].Z, edge.Vertexes[1].X, edge.Vertexes[1].Y, edge.Vertexes[1].Z, z0, z1))
#print("- %s -> %s" % (cmd, command))
return cls.edgeForCmd(command, FreeCAD.Vector(p1.x, p1.y, z0))
@classmethod
def helixToArc(cls, edge, z = 0):
"""(edge, z=0)
Returns the projection of the helix onto the XY-plane with a given offset."""
p1 = edge.valueAt(edge.FirstParameter)
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2)
p3 = edge.valueAt(edge.LastParameter)
p01 = FreeCAD.Vector(p1.x, p1.y, z)
p02 = FreeCAD.Vector(p2.x, p2.y, z)
p03 = FreeCAD.Vector(p3.x, p3.y, z)
return Part.Edge(Part.Arc(p01, p02, p03))
@classmethod
def splitArcAt(cls, edge, pt):
"""(edge, pt)
Returns a list of 2 edges which together form the original arc split at the given point.
The Vector pt has to represnt a point on the given arc."""
p1 = edge.valueAt(edge.FirstParameter)
p2 = pt
p3 = edge.valueAt(edge.LastParameter)
edges = []
p = edge.Curve.parameter(p2)
#print("splitArcAt(%.2f, %.2f, %.2f): %.2f - %.2f - %.2f" % (pt.x, pt.y, pt.z, edge.FirstParameter, p, edge.LastParameter))
p12 = edge.Curve.value((edge.FirstParameter + p)/2)
p23 = edge.Curve.value((p + edge.LastParameter)/2)
#print("splitArcAt: p12=(%.2f, %.2f, %.2f) p23=(%.2f, %.2f, %.2f)" % (p12.x, p12.y, p12.z, p23.x, p23.y, p23.z))
edges.append(Part.Edge(Part.Arc(p1, p12, p2)))
edges.append(Part.Edge(Part.Arc(p2, p23, p3)))
return edges
@classmethod
def splitEdgeAt(cls, edge, pt):
"""(edge, pt)
Returns a list of 2 edges, forming the original edge split at the given point.
The results are undefined if the Vector representing the point is not part of the edge."""
# I could not get the OCC parameterAt and split to work ...
# pt HAS to be on the edge, otherwise the results are undefined
p1 = edge.valueAt(edge.FirstParameter)
p2 = pt
p3 = edge.valueAt(edge.LastParameter)
edges = []
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
# it's a line
return [Part.Edge(Part.LineSegment(p1, p2)), Part.Edge(Part.LineSegment(p2, p3))]
elif type(edge.Curve) == Part.Circle:
# it's an arc
return cls.splitArcAt(edge, pt)
else:
# it's a helix
arc = cls.helixToArc(edge, 0)
aes = cls.splitArcAt(arc, FreeCAD.Vector(pt.x, pt.y, 0))
return [cls.arcToHelix(aes[0], p1.z, p2.z), cls.arcToHelix(aes[1], p2.z, p3.z)]

View File

@ -27,6 +27,7 @@ import Part
import math
import unittest
from FreeCAD import Vector
from PathScripts.PathGeom import Side
class PathTestBase(unittest.TestCase):
@ -44,28 +45,39 @@ class PathTestBase(unittest.TestCase):
def assertLine(self, edge, pt1, pt2):
"""Verify that edge is a line from pt1 to pt2."""
self.assertIs(type(edge.Curve), Part.LineSegment)
self.assertCoincide(edge.Curve.StartPoint, pt1)
self.assertCoincide(edge.Curve.EndPoint, pt2)
self.assertIs(type(edge.Curve), Part.Line)
self.assertCoincide(edge.valueAt(edge.FirstParameter), pt1)
self.assertCoincide(edge.valueAt(edge.LastParameter), pt2)
def assertLines(self, edgs, tail, points):
"""Verify that the edges match the polygon resulting from points."""
edges = list(edgs)
if tail:
edges.append(tail)
self.assertEqual(len(edges), len(points) - 1)
for i in range(0, len(edges)):
self.assertLine(edges[i], points[i], points[i+1])
def assertArc(self, edge, pt1, pt2, direction = 'CW'):
"""Verify that edge is an arc between pt1 and pt2 with the given direction."""
# If an Arc is wrapped into edge, then it's curve is represented as a circle
# and not as an Arc (GeomTrimmedCurve)
#self.assertIs(type(edge.Curve), Part.Arc)
self.assertIs(type(edge.Curve), Part.Circle)
self.assertCoincide(edge.valueAt(edge.FirstParameter), pt1)
self.assertCoincide(edge.valueAt(edge.LastParameter), pt2)
ptm = edge.valueAt((edge.LastParameter + edge.FirstParameter)/2)
side = Side.of(pt2 - pt1, ptm - pt1)
#print("(%.2f, %.2f) (%.2f, %.2f) (%.2f, %.2f)" % (pt1.x, pt1.y, ptm.x, ptm.y, pt2.x, pt2.y))
#print(" (%.2f, %.2f) (%.2f, %.2f) -> %s" % ((pt2-pt1).x, (pt2-pt1).y, (ptm-pt1).x, (ptm-pt1).y, Side.toString(side)))
#print(" (%.2f, %.2f) (%.2f, %.2f) -> (%.2f, %.2f)" % (pf.x,pf.y, pl.x,pl.y, pm.x, pmy))
if 'CW' == direction:
self.assertEqual(side, Side.Left)
else:
self.assertEqual(side, Side.Right)
def assertCircle(self, edge, pt, r):
"""Verivy that edge is a circle at given location."""
curve = edge.Curve
self.assertIs(type(curve), Part.Circle)
self.assertCoincide(curve.Center, Vector(pt.x, pt.y, pt.z))
self.assertRoughly(curve.Radius, r)
def assertCurve(self, edge, p1, p2, p3):
"""Verify that the edge goes through the given 3 points, representing start, mid and end point respectively."""
@ -73,3 +85,27 @@ class PathTestBase(unittest.TestCase):
self.assertCoincide(edge.valueAt(edge.LastParameter), p3)
self.assertCoincide(edge.valueAt((edge.FirstParameter + edge.LastParameter)/2), p2)
def assertCylinderAt(self, solid, pt, r, h):
"""Verify that solid is a cylinder at the specified location."""
self.assertEqual(len(solid.Edges), 3)
lid = solid.Edges[0]
hull = solid.Edges[1]
base = solid.Edges[2]
self.assertCircle(lid, Vector(pt.x, pt.y, pt.z+h), r)
self.assertLine(hull, Vector(pt.x+r, pt.y, pt.z), Vector(pt.x+r, pt.y, pt.z+h))
self.assertCircle(base, Vector(pt.x, pt.y, pt.z), r)
def assertConeAt(self, solid, pt, r1, r2, h):
"""Verify that solid is a cone at the specified location."""
self.assertEqual(len(solid.Edges), 3)
lid = solid.Edges[0]
hull = solid.Edges[1]
base = solid.Edges[2]
self.assertCircle(lid, Vector(pt.x, pt.y, pt.z+h), r2)
self.assertLine(hull, Vector(pt.x+r1, pt.y, pt.z), Vector(pt.x+r2, pt.y, pt.z+h))
self.assertCircle(base, Vector(pt.x, pt.y, pt.z), r1)

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.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
import Part
import Path
import PathScripts
import math
import unittest
from FreeCAD import Vector
from PathScripts.PathDressupHoldingTags import *
from PathTests.PathTestUtils import PathTestBase
class TestHoldingTags(PathTestBase):
"""Unit tests for the HoldingTags dressup."""
def test00(self):
"""Check Tag origin, serialization and de-serialization."""
tag = Tag(77, 13, 4, 5, 90, True)
self.assertCoincide(tag.originAt(3), Vector(77, 13, 3))
s = tag.toString()
tagCopy = Tag.FromString(s)
self.assertEqual(tag.x, tagCopy.x)
self.assertEqual(tag.y, tagCopy.y)
self.assertEqual(tag.height, tagCopy.height)
self.assertEqual(tag.width, tagCopy.width)
self.assertEqual(tag.enabled, tagCopy.enabled)
def test01(self):
"""Verify solid for a 90 degree tag is a cylinder."""
tag = Tag(100, 200, 4, 5, 90, True)
tag.createSolidsAt(17, 0)
self.assertIsNotNone(tag.solid)
self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5)
def test02(self):
"""Verify trapezoidal tag has a cone shape with a lid."""
tag = Tag(0, 0, 18, 5, 45, True)
tag.createSolidsAt(0, 0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5)
def test03(self):
"""Verify pointy cone shape of tag with pointy end if width, angle and height match up."""
tag = Tag(0, 0, 10, 5, 45, True)
tag.createSolidsAt(0, 0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5)
def test04(self):
"""Verify height adjustment if tag isn't wide eough for angle."""
tag = Tag(0, 0, 5, 17, 60, True)
tag.createSolidsAt(0, 0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi))

View File

@ -130,12 +130,14 @@ class TestPathGeom(PathTestBase):
commands.append(Path.Command('G0', {'X': 0}))
commands.append(Path.Command('G1', {'Y': 0}))
wire = PathGeom.wireForPath(Path.Path(commands))
wire,rapid = PathGeom.wireForPath(Path.Path(commands))
self.assertEqual(len(wire.Edges), 4)
self.assertLine(wire.Edges[0], Vector(0,0,0), Vector(1,0,0))
self.assertLine(wire.Edges[1], Vector(1,0,0), Vector(1,1,0))
self.assertLine(wire.Edges[2], Vector(1,1,0), Vector(0,1,0))
self.assertLine(wire.Edges[3], Vector(0,1,0), Vector(0,0,0))
self.assertEqual(len(rapid), 1)
self.assertTrue(PathGeom.edgesMatch(rapid[0], wire.Edges[2]))
wires = PathGeom.wiresForPath(Path.Path(commands))
self.assertEqual(len(wires), 2)
@ -145,3 +147,96 @@ class TestPathGeom(PathTestBase):
self.assertEqual(len(wires[1].Edges), 1)
self.assertLine(wires[1].Edges[0], Vector(0,1,0), Vector(0,0,0))
def test60(self):
"""Verify arcToHelix returns proper helix."""
p1 = Vector(10,-10,0)
p2 = Vector(0,0,0)
p3 = Vector(10,10,0)
e = PathGeom.arcToHelix(Part.Edge(Part.Arc(p1, p2, p3)), 0, 2)
self.assertCurve(e, p1, p2 + Vector(0,0,1), p3 + Vector(0,0,2))
e = PathGeom.arcToHelix(Part.Edge(Part.Arc(p1, p2, p3)), 3, 7)
self.assertCurve(e, p1 + Vector(0,0,3), p2 + Vector(0,0,5), p3 + Vector(0,0,7))
e = PathGeom.arcToHelix(Part.Edge(Part.Arc(p1, p2, p3)), 9, 1)
self.assertCurve(e, p1 + Vector(0,0,9), p2 + Vector(0,0,5), p3 + Vector(0,0,1))
dz = Vector(0,0,3)
p11 = p1 + dz
p12 = p2 + dz
p13 = p3 + dz
e = PathGeom.arcToHelix(Part.Edge(Part.Arc(p11, p12, p13)), 0, 8)
self.assertCurve(e, p1, p2 + Vector(0,0,4), p3 + Vector(0,0,8))
e = PathGeom.arcToHelix(Part.Edge(Part.Arc(p11, p12, p13)), 2, -2)
self.assertCurve(e, p1 + Vector(0,0,2), p2, p3 + Vector(0,0,-2))
o = 10*math.sin(math.pi/4)
p1 = Vector(10, -10, 1)
p2 = Vector(10 - 10*math.sin(math.pi/4), -10*math.cos(math.pi/4), 1)
p3 = Vector(0, 0, 1)
e = PathGeom.arcToHelix(Part.Edge(Part.Arc(p1, p2, p3)), 0, 5)
self.assertCurve(e, Vector(10,-10,0), Vector(p2.x,p2.y,2.5), Vector(0, 0, 5))
def test62(self):
"""Verify splitArcAt returns proper subarcs."""
p1 = Vector(10,-10,0)
p2 = Vector(0,0,0)
p3 = Vector(10,10,0)
arc = Part.Edge(Part.Arc(p1, p2, p3))
o = 10*math.sin(math.pi/4)
p12 = Vector(10 - o, -o, 0)
p23 = Vector(10 - o, +o, 0)
e = PathGeom.splitArcAt(arc, p2)
self.assertCurve(e[0], p1, p12, p2)
self.assertCurve(e[1], p2, p23, p3)
p34 = Vector(10 - 10*math.sin(1*math.pi/8), -10*math.cos(1*math.pi/8), 0)
p45 = Vector(10 - 10*math.sin(5*math.pi/8), -10*math.cos(5*math.pi/8), 0)
e = PathGeom.splitArcAt(arc, p12)
self.assertCurve(e[0], p1, p34, p12)
self.assertCurve(e[1], p12, p45, p3)
def test65(self):
"""Verify splitEdgeAt."""
e = PathGeom.splitEdgeAt(Part.Edge(Part.LineSegment(Vector(), Vector(2, 4, 6))), Vector(1, 2, 3))
self.assertLine(e[0], Vector(), Vector(1,2,3))
self.assertLine(e[1], Vector(1,2,3), Vector(2,4,6))
# split an arc
p1 = Vector(10,-10,1)
p2 = Vector(0,0,1)
p3 = Vector(10,10,1)
arc = Part.Edge(Part.Arc(p1, p2, p3))
e = PathGeom.splitEdgeAt(arc, p2)
o = 10*math.sin(math.pi/4)
p12 = Vector(10 - o, -o, 1)
p23 = Vector(10 - o, +o, 1)
self.assertCurve(e[0], p1, p12, p2)
self.assertCurve(e[1], p2, p23, p3)
# split a helix
p1 = Vector(10,-10,0)
p2 = Vector(0,0,5)
p3 = Vector(10,10,10)
h = PathGeom.arcToHelix(arc, 0, 10)
self.assertCurve(h, p1, p2, p3)
e = PathGeom.splitEdgeAt(h, p2)
o = 10*math.sin(math.pi/4)
p12 = Vector(10 - o, -o, 2.5)
p23 = Vector(10 - o, +o, 7.5)
pf = e[0].valueAt((e[0].FirstParameter + e[0].LastParameter)/2)
pl = e[1].valueAt((e[1].FirstParameter + e[1].LastParameter)/2)
self.assertCurve(e[0], p1, p12, p2)
self.assertCurve(e[1], p2, p23, p3)

View File

@ -29,3 +29,4 @@ from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathGeom import TestPathGeom
from PathTests.TestPathDepthParams import depthTestCases
from PathTests.TestPathDressupHoldingTags import TestHoldingTags