Fixed alignment issue and unit tests.

The trick is really to over-extend edges before creationg shapes for the common operation, and trying to avoid alignment of the edge with the cone's seam.
This commit is contained in:
Markus Lampert 2016-12-30 19:16:27 -08:00
parent 27b71ab1ae
commit cb85072bbd
6 changed files with 355 additions and 387 deletions

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

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>352</width>
<height>387</height>
<width>363</width>
<height>530</height>
</rect>
</property>
<property name="windowTitle">
@ -27,8 +27,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>334</width>
<height>311</height>
<width>345</width>
<height>476</height>
</rect>
</property>
<attribute name="label">
@ -36,199 +36,118 @@
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableWidget" name="twTags">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>80</number>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>X</string>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QFormLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
</column>
<column>
<property name="text">
<string>Y</string>
</property>
</column>
<column>
<property name="text">
<string>Width</string>
</property>
</column>
<column>
<property name="text">
<string>Height</string>
</property>
</column>
<column>
<property name="text">
<string>Angle</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pbDelete">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Delete</string>
<string>Width</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pbDisable">
<property name="text">
<string>Disable</string>
<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>
<widget class="QPushButton" name="pbAdd">
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Add</string>
<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>
</layout>
</widget>
<widget class="QWidget" name="tbpGenerate">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>334</width>
<height>311</height>
</rect>
</property>
<attribute name="label">
<string>Generate</string>
</attribute>
<layout class="QFormLayout" name="formLayout_2">
<item row="4" column="0">
<widget class="QLabel" name="lWidth">
<property name="text">
<string>Width</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="dsbWidth">
<item>
<widget class="QListWidget" name="lwTags">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Width of each tag.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<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 row="5" column="0">
<widget class="QLabel" name="lHeight">
<property name="text">
<string>Height</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QDoubleSpinBox" name="dsbHeight">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The height of the holding tag measured from the bottom of the path. By default this is set to the (estimated) height of the path.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="lAngle">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Angle </string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QDoubleSpinBox" name="dsbAngle">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Angle of tag walls.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Layout</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="lCount">
<widget class="QPushButton" name="pbDelete">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Count</string>
<string>Delete</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="sbCount">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter the number of tags you wish to have.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tags you don't want in order to get the holding tag layout you want.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<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>Spacing </string>
<string>Number of Tags</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="dsbSpacing"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="cbAutoApply">
<property name="text">
<string>Auto Apply</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

View File

@ -29,6 +29,9 @@ import Part
import copy
import math
import cProfile
import time
from PathScripts import PathUtils
from PathScripts.PathGeom import *
from PySide import QtCore, QtGui
@ -57,7 +60,11 @@ def debugEdge(edge, prefix, force = False):
pf = edge.valueAt(edge.FirstParameter)
pl = edge.valueAt(edge.LastParameter)
if force or debugDressup:
print("%s %s((%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f))" % (prefix, type(edge.Curve), pf.x, pf.y, pf.z, pl.x, pl.y, pl.z))
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
print("%s %s((%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f))" % (prefix, type(edge.Curve), pf.x, pf.y, pf.z, pl.x, pl.y, pl.z))
else:
pm = edge.valueAt((edge.FirstParameter+edge.LastParameter)/2)
print("%s %s((%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f))" % (prefix, type(edge.Curve), pf.x, pf.y, pf.z, pm.x, pm.y, pm.z, pl.x, pl.y, pl.z))
def debugMarker(vector, label, color = None, radius = 0.5):
if debugDressup:
@ -104,18 +111,18 @@ class Tag:
def toString(self):
return str((self.x, self.y, self.width, self.height, self.angle, self.enabled))
def __init__(self, x, y, width, height, angle, enabled=True, z=None):
debugPrint("Tag(%.2f, %.2f, %.2f, %.2f, %.2f, %d, %s)" % (x, y, width, height, angle/math.pi, enabled, z))
def __init__(self, x, y, width, height, angle, enabled=True):
debugPrint("Tag(%.2f, %.2f, %.2f, %.2f, %.2f, %d)" % (x, y, width, height, angle/math.pi, enabled))
self.x = x
self.y = y
self.z = z
self.width = math.fabs(width)
self.height = math.fabs(height)
self.actualHeight = self.height
self.angle = math.fabs(angle)
self.enabled = enabled
if z is not None:
self.createSolidsAt(z)
def fullWidth(self):
return 2 * self.toolRadius + self.width
def originAt(self, z):
return FreeCAD.Vector(self.x, self.y, z)
@ -126,14 +133,16 @@ class Tag:
def top(self):
return self.z + self.actualHeight
def createSolidsAt(self, z):
def createSolidsAt(self, z, R):
self.z = z
r1 = self.width / 2
self.toolRadius = R
r1 = self.fullWidth() / 2
self.r1 = r1
self.r2 = r1
height = self.height
if self.angle == 90 and height > 0:
self.solid = Part.makeCylinder(r1, height)
print("Part.makeCone(%f, %f)" % (r1, height))
elif self.angle > 0.0 and height > 0.0:
tangens = math.tan(math.radians(self.angle))
dr = height / tangens
@ -144,10 +153,17 @@ class Tag:
height = r1 * tangens
self.actualHeight = height
self.r2 = r2
print("Part.makeCone(%f, %f, %f)" % (r1, r2, height))
self.solid = Part.makeCone(r1, r2, height)
else:
# degenerated case - no tag
print("Part.makeSphere(%f / 10000)" % (r1))
self.solid = Part.makeSphere(r1 / 10000)
if not R == 0: # testing is easier if the solid is not rotated
angle = -PathGeom.getAngle(self.originAt(0)) * 180 / math.pi
print("solid.rotate(%f)" % angle)
self.solid.rotate(FreeCAD.Vector(0,0,0), FreeCAD.Vector(0,0,1), angle)
print("solid.translate(%s)" % self.originAt(z))
self.solid.translate(self.originAt(z))
def filterIntersections(self, pts, face):
@ -206,8 +222,9 @@ class Tag:
return None
def intersects(self, edge, param):
if edge.valueAt(edge.FirstParameter).z < self.top() or edge.valueAt(edge.LastParameter).z < self.top():
return self.nextIntersectionClosestTo(edge, self.solid, edge.valueAt(param))
if self.enabled:
if edge.valueAt(edge.FirstParameter).z < self.top() or edge.valueAt(edge.LastParameter).z < self.top():
return self.nextIntersectionClosestTo(edge, self.solid, edge.valueAt(param))
return None
class MapWireToTag:
@ -231,105 +248,131 @@ class MapWireToTag:
self.edges = []
self.entry = i
self.complete = False
self.wire = None
self.haveProblem = False
def addEdge(self, edge):
debugEdge(edge, '..........')
if self.wire:
self.wire.add(edge)
else:
self.wire = Part.Wire(edge)
self.edges.append(edge)
def needToFlipEdge(self, edge, p):
if PathGeom.pointsCoincide(edge.valueAt(edge.LastParameter), p):
return True, edge.valueAt(edge.FirstParameter)
return False, edge.valueAt(edge.LastParameter)
def isEntryOrExitStrut(self, p1, p2):
p = PathGeom.xy(p1)
pEntry0 = PathGeom.xy(self.entry)
pExit0 = PathGeom.xy(self.exit)
# it can only be an entry strut if the strut coincides with the entry point and is above it
if PathGeom.pointsCoincide(p, pEntry0) and p1.z >= self.entry.z and p2.z >= self.entry.z:
return True
if PathGeom.pointsCoincide(p, pExit0) and p1.z >= self.exit.z and p2.z >= self.exit.z:
return True
return False
def isEntryOrExitStrut(self, e):
p1 = e.valueAt(e.FirstParameter)
p2 = e.valueAt(e.LastParameter)
if PathGeom.pointsCoincide(p1, self.entry) and p2.z >= self.entry.z:
return 1
if PathGeom.pointsCoincide(p2, self.entry) and p1.z >= self.entry.z:
return 1
if PathGeom.pointsCoincide(p1, self.exit) and p2.z >= self.exit.z:
return 2
if PathGeom.pointsCoincide(p2, self.exit) and p1.z >= self.exit.z:
return 2
return 0
def cleanupEdges(self, edges):
# first remove all internal struts
debugEdge(Part.Edge(Part.LineSegment(self.entry, self.exit)), '------> cleanupEdges')
inputEdges = copy.copy(edges)
plinths = []
def cleanupEdges(self, edges, baseEdge):
# want to remove all edges from the wire itself, and all internal struts
print("+cleanupEdges")
print(" base:")
debugEdge(baseEdge, ' ', True)
print(" edges:")
for e in edges:
debugEdge(e, '........ cleanup')
p1 = e.valueAt(e.FirstParameter)
p2 = e.valueAt(e.LastParameter)
if PathGeom.pointsCoincide(PathGeom.xy(p1), PathGeom.xy(p2)):
#it's a strut
if not self.isEntryOrExitStrut(p1, p2):
debugEdge(e, '......... X0 %d/%d' % (PathGeom.edgeConnectsTo(e, self.entry), PathGeom.edgeConnectsTo(e, self.exit)))
inputEdges.remove(e)
if p1.z > p2.z:
plinths.append(p2)
else:
plinths.append(p1)
# remove all edges that are connected to the plinths of the (former) internal struts
for e in copy.copy(inputEdges):
for p in plinths:
if PathGeom.edgeConnectsTo(e, p):
debugEdge(e, '......... X1')
inputEdges.remove(e)
break
# if there are any edges beside a direct edge remaining, the direct edge between
# entry and exit is redundant
if len(inputEdges) > 1:
for e in copy.copy(inputEdges):
if PathGeom.edgeConnectsTo(e, self.entry) and PathGeom.edgeConnectsTo(e, self.exit):
debugEdge(e, '......... X2')
inputEdges.remove(e)
debugEdge(e, ' ', True)
print(":")
# the remaining edges form a walk around the tag
# they need to be ordered and potentially flipped though
haveEntry = False
for e in copy.copy(edges):
if PathGeom.edgesMatch(e, baseEdge):
debugEdge(e, '......... X0')
edges.remove(e)
elif self.isStrut(e):
typ = self.isEntryOrExitStrut(e)
debugEdge(e, '......... |%d' % typ)
if 0 == typ: # neither entry nor exit
debugEdge(e, '......... X1')
edges.remove(e)
elif 1 == typ:
haveEntry = True
print("entry(%.2f, %.2f, %.2f), exit(%.2f, %.2f, %.2f)" % (self.entry.x, self.entry.y, self.entry.z, self.exit.x, self.exit.y, self.exit.z))
# the remaininng edges for the path from xy(baseEdge) along the tags surface
outputEdges = []
p = self.entry
lastP = p
while inputEdges:
for e in inputEdges:
p0 = baseEdge.valueAt(baseEdge.FirstParameter)
ignoreZ = False
if not haveEntry:
ignoreZ = True
p0 = PathGeom.xy(p0)
lastP = p0
while edges:
print("(%.2f, %.2f, %.2f) %d %d" % (p0.x, p0.y, p0.z, haveEntry, ignoreZ))
for e in edges:
p1 = e.valueAt(e.FirstParameter)
p2 = e.valueAt(e.LastParameter)
if PathGeom.pointsCoincide(p1, p):
outputEdges.append((e,False))
inputEdges.remove(e)
lastP = p
p = p2
if PathGeom.pointsCoincide(PathGeom.xy(p1) if ignoreZ else p1, p0):
outputEdges.append((e, False))
edges.remove(e)
lastP = None
ignoreZ = False
p0 = p2
debugEdge(e, ">>>>> no flip")
break
elif PathGeom.pointsCoincide(p2, p):
outputEdges.append((e,True))
inputEdges.remove(e)
lastP = p
p = p1
elif PathGeom.pointsCoincide(PathGeom.xy(p2) if ignoreZ else p2, p0):
outputEdges.append((e, True))
edges.remove(e)
lastP = None
ignoreZ = False
p0 = p1
debugEdge(e, ">>>>> flip")
break
#else:
# debugEdge(e, "<<<<< (%.2f, %.2f, %.2f)" % (p.x, p.y, p.z))
if lastP == p:
raise ValueError("No connection to %s" % (p))
#else:
# print("xxxxxx (%.2f, %.2f, %.2f)" % (p.x, p.y, p.z))
else:
debugEdge(e, "<<<<< (%.2f, %.2f, %.2f)" % (p0.x, p0.y, p0.z))
if lastP == p0:
raise ValueError("No connection to %s" % (p0))
elif lastP:
print("xxxxxx (%.2f, %.2f, %.2f) (%.2f, %.2f, %.2f)" % (p0.x, p0.y, p0.z, lastP.x, lastP.y, lastP.z))
else:
print("xxxxxx (%.2f, %.2f, %.2f) -" % (p0.x, p0.y, p0.z))
lastP = p0
print("-cleanupEdges")
return outputEdges
def shell(self):
shell = self.wire.extrude(FreeCAD.Vector(0, 0, 10))
redundant = filter(lambda f: f.Area == 0, shell.childShapes())
if redundant:
return shell.removeShape(redundant)
return shell
def isStrut(self, edge):
p1 = PathGeom.xy(edge.valueAt(edge.FirstParameter))
p2 = PathGeom.xy(edge.valueAt(edge.LastParameter))
return PathGeom.pointsCoincide(p1, p2)
def cmdsForEdge(self, edge):
cmds = []
# OCC doesn't like it very much if the shapes align with each other. So if we have a slightly
# extended edge for the last edge in list we'll use that instead for stable results.
if PathGeom.pointsCoincide(edge.valueAt(edge.FirstParameter), self.lastEdge.valueAt(self.lastEdge.FirstParameter)):
shell = self.lastEdge.extrude(FreeCAD.Vector(0, 0, self.tag.height + 1))
else:
shell = edge.extrude(FreeCAD.Vector(0, 0, self.tag.height + 1))
shape = shell.common(self.tag.solid)
if not shape.Edges:
self.haveProblem = True
for e,flip in self.cleanupEdges(shape.Edges, edge):
debugEdge(e, '++++++++ %s' % ('.' if not flip else '@'))
cmds.extend(PathGeom.cmdsForEdge(e, flip, False))
return cmds
def commandsForEdges(self):
commands = []
for e in self.edges:
if self.isStrut(e):
continue
commands.extend(self.cmdsForEdge(e))
return commands
def add(self, edge):
self.tail = None
self.lastEdge = edge # see cmdsForEdge
if self.tag.solid.isInside(edge.valueAt(edge.LastParameter), 0.000001, True):
self.addEdge(edge)
else:
@ -341,13 +384,8 @@ class MapWireToTag:
self.addEdge(e)
self.tail = tail
self.exit = i
if self.wire:
face = self.shell().common(self.tag.solid)
for e,flip in self.cleanupEdges(face.Edges):
debugEdge(e, '++++++++ %s' % ('.' if not flip else '@'))
self.commands.extend(PathGeom.cmdsForEdge(e, flip, False))
self.complete = True
self.commands.extend(self.commandsForEdges())
def mappingComplete(self):
return self.complete
@ -399,9 +437,11 @@ class PathData:
def findZLimits(self, edges):
# not considering arcs and spheres in Z direction, find the highes and lowest Z values
minZ = edges[0].Vertexes[0].Point.z
maxZ = minZ
minZ = 99999999999
maxZ = -99999999999
for e in edges:
if self.rapid.isRapid(e):
continue
for v in e.Vertexes:
if v.Point.z < minZ:
minZ = v.Point.z
@ -476,10 +516,11 @@ class PathData:
debugPrint(" %d: %d" % (i, count))
#debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2)
#debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2)
distance = (edge.LastParameter - edge.FirstParameter) / count
for j in range(0, count):
tag = edge.Curve.value((j+0.5) * distance)
tags.append(Tag(tag.x, tag.y, W, H, angle, True))
if 0 != count:
distance = (edge.LastParameter - edge.FirstParameter) / count
for j in range(0, count):
tag = edge.Curve.value((j+0.5) * distance)
tags.append(Tag(tag.x, tag.y, W, H, angle, True))
return tags
@ -554,6 +595,7 @@ class ObjectDressup:
return True
def createPath(self, edges, tags, rapid):
print("createPath")
commands = []
lastEdge = 0
lastTag = 0
@ -562,10 +604,11 @@ class ObjectDressup:
inters = None
edge = None
self.mappers = []
mapper = None
while edge or lastEdge < len(edges):
#print("------- lastEdge = %d/%d.%d/%d" % (lastEdge, lastTag, t, len(tags)))
debugPrint("------- lastEdge = %d/%d.%d/%d" % (lastEdge, lastTag, t, len(tags)))
if not edge:
edge = edges[lastEdge]
debugEdge(edge, "======= new edge: %d/%d" % (lastEdge, len(edges)))
@ -587,6 +630,7 @@ class ObjectDressup:
i = tags[tIndex].intersects(edge, edge.FirstParameter)
if i and self.isValidTagStartIntersection(edge, i):
mapper = MapWireToTag(edge, tags[tIndex], i)
self.mappers.append(mapper)
edge = mapper.tail
@ -606,8 +650,17 @@ class ObjectDressup:
# print(cmd)
return Path.Path(commands)
def problems(self):
return filter(lambda m: m.haveProblem, self.mappers)
def execute(self, obj):
#pr = cProfile.Profile()
#pr.enable()
self.doExecute(obj)
#pr.disable()
#pr.print_stats()
def doExecute(self,obj):
if not obj.Base:
return
if not obj.Base.isDerivedFrom("Path::Feature"):
@ -642,7 +695,7 @@ class ObjectDressup:
tags = pathData.sortedTags(tags)
self.setTags(obj, tags, False)
for tag in tags:
tag.createSolidsAt(pathData.minZ)
tag.createSolidsAt(pathData.minZ, self.toolRadius)
tagID = 0
for tag in tags:
@ -653,12 +706,13 @@ class ObjectDressup:
if tag.angle != 90:
debugCone(tag.originAt(pathData.minZ), tag.r1, tag.r2, tag.actualHeight, "tag-%02d" % tagID)
else:
debugCylinder(tag.originAt(pathData.minZ), tag.width/2, tag.actualHeight, "tag-%02d" % tagID)
debugCylinder(tag.originAt(pathData.minZ), tag.fullWidth()/2, tag.actualHeight, "tag-%02d" % tagID)
self.fingerprint = [tag.toString() for tag in tags]
self.tags = tags
obj.Path = self.createPath(pathData.edges, tags, pathData.rapid)
print("execute - done")
def setTags(self, obj, tags, update = True):
print("setTags(%d, %d)" % (len(tags), update))
@ -681,32 +735,16 @@ class ObjectDressup:
FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Cannot insert holding tags for this path - please select a Profile path\n"))
return None
## setup the object's properties, in case they're not set yet
#obj.Count = self.tagCount(obj)
#obj.Angle = self.tagAngle(obj)
#obj.Blacklist = self.tagBlacklist(obj)
# if the heigt isn't set, use the height of the path
#if not hasattr(obj, "Height") or not obj.Height:
# obj.Height = pathData.maxZ - pathData.minZ
# try and take an educated guess at the width
#if not hasattr(obj, "Width") or not obj.Width:
# width = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length / 10
# while obj.Count > len([e for e in pathData.base.Edges if e.Length > 3*width]):
# width = widht / 2
# obj.Width = width
# and the tool radius, not sure yet if it's needed
#self.toolRadius = 5
#toolLoad = PathUtils.getLastToolLoad(obj)
#if toolLoad is None or toolLoad.ToolNumber == 0:
# self.toolRadius = 5
#else:
# tool = PathUtils.getTool(obj, toolLoad.ToolNumber)
# if not tool or tool.Diameter == 0:
# self.toolRadius = 5
# else:
# self.toolRadius = tool.Diameter / 2
self.toolRadius = 5
toolLoad = PathUtils.getLastToolLoad(obj)
if toolLoad is None or toolLoad.ToolNumber == 0:
self.toolRadius = 5
else:
tool = PathUtils.getTool(obj, toolLoad.ToolNumber)
if not tool or tool.Diameter == 0:
self.toolRadius = 5
else:
self.toolRadius = tool.Diameter / 2
self.pathData = pathData
return self.pathData
@ -723,8 +761,8 @@ class ObjectDressup:
return self.pathData.pathLength()
class TaskPanel:
DataTag = QtCore.Qt.ItemDataRole.UserRole
DataValue = QtCore.Qt.ItemDataRole.DisplayRole
DataX = QtCore.Qt.ItemDataRole.UserRole
DataY = QtCore.Qt.ItemDataRole.UserRole + 1
def __init__(self, obj):
self.obj = obj
@ -738,6 +776,7 @@ class TaskPanel:
FreeCADGui.Selection.removeObserver(self.s)
def accept(self):
self.getFields()
FreeCAD.ActiveDocument.commitTransaction()
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
@ -750,40 +789,38 @@ class TaskPanel:
# install the function mode resident
FreeCADGui.Selection.addObserver(self.s)
def tableWidgetItem(self, tag, val):
item = QtGui.QTableWidgetItem()
item.setTextAlignment(QtCore.Qt.AlignRight)
item.setData(self.DataTag, tag)
item.setData(self.DataValue, val)
return item
def getFields(self):
width = self.form.dsbWidth.value()
height = self.form.dsbHeight.value()
angle = self.form.dsbAngle.value()
tags = []
for row in range(0, self.form.twTags.rowCount()):
x = self.form.twTags.item(row, 0).data(self.DataValue)
y = self.form.twTags.item(row, 1).data(self.DataValue)
w = self.form.twTags.item(row, 2).data(self.DataValue)
h = self.form.twTags.item(row, 3).data(self.DataValue)
a = self.form.twTags.item(row, 4).data(self.DataValue)
tags.append(Tag(x, y, w, h, a, True))
print("getFields: %d" % (len(tags)))
for i in range(0, self.form.lwTags.count()):
item = self.form.lwTags.item(i)
enabled = item.checkState() == QtCore.Qt.CheckState.Checked
x = item.data(self.DataX)
y = item.data(self.DataY)
tags.append(Tag(x, y, width, height, angle, enabled))
self.obj.Proxy.setTags(self.obj, tags)
def updateTags(self):
def updateTagsView(self):
self.tags = self.obj.Proxy.getTags(self.obj)
self.form.twTags.blockSignals(True)
self.form.twTags.setSortingEnabled(False)
self.form.twTags.clearSpans()
print("updateTags: %d" % (len(self.tags)))
self.form.twTags.setRowCount(len(self.tags))
for row, tag in enumerate(self.tags):
self.form.twTags.setItem(row, 0, self.tableWidgetItem(tag, tag.x))
self.form.twTags.setItem(row, 1, self.tableWidgetItem(tag, tag.y))
self.form.twTags.setItem(row, 2, self.tableWidgetItem(tag, tag.width))
self.form.twTags.setItem(row, 3, self.tableWidgetItem(tag, tag.height))
self.form.twTags.setItem(row, 4, self.tableWidgetItem(tag, tag.angle))
self.form.twTags.setSortingEnabled(True)
self.form.twTags.blockSignals(False)
print("updateTagsView: %d" % (len(self.tags)))
self.form.lwTags.blockSignals(True)
self.form.lwTags.clear()
for tag in self.tags:
lbl = "(%.2f, %.2f)" % (tag.x, tag.y)
item = QtGui.QListWidgetItem(lbl)
item.setData(self.DataX, tag.x)
item.setData(self.DataY, tag.y)
if tag.enabled:
item.setCheckState(QtCore.Qt.CheckState.Checked)
else:
item.setCheckState(QtCore.Qt.CheckState.Unchecked)
flags = QtCore.Qt.ItemFlag.ItemIsSelectable
flags |= QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable
item.setFlags(flags)
self.form.lwTags.addItem(item)
self.form.lwTags.blockSignals(False)
def cleanupUI(self):
print("cleanupUI")
@ -792,67 +829,50 @@ class TaskPanel:
if obj.Name.startswith('tag'):
FreeCAD.ActiveDocument.removeObject(obj.Name)
def updateUI(self):
print("updateUI")
self.cleanupUI()
self.getFields()
if debugDressup:
FreeCAD.ActiveDocument.recompute()
def whenApplyClicked(self):
print("whenApplyClicked")
def generateNewTags(self):
print("generateNewTags")
self.cleanupUI()
count = self.form.sbCount.value()
spacing = self.form.dsbSpacing.value()
width = self.form.dsbWidth.value()
height = self.form.dsbHeight.value()
angle = self.form.dsbAngle.value()
tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle, spacing * 0.99)
tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle)
self.obj.Proxy.setTags(self.obj, tags)
self.updateTags()
if debugDressup:
# this causes a big of an echo and a double click on the spin buttons, don't know why though
FreeCAD.ActiveDocument.recompute()
self.updateTagsView()
#if debugDressup:
# # this causes a big of an echo and a double click on the spin buttons, don't know why though
# FreeCAD.ActiveDocument.recompute()
def autoApply(self):
print("autoApply")
if self.form.cbAutoApply.checkState() == QtCore.Qt.CheckState.Checked:
self.whenApplyClicked()
# def autoApply(self):
# print("autoApply")
# if self.form.cbAutoApply.checkState() == QtCore.Qt.CheckState.Checked:
# self.whenApplyClicked()
def updateTagSpacing(self, count):
print("updateTagSpacing")
if count == 0:
spacing = 0
else:
spacing = self.pathLength / count
self.form.dsbSpacing.blockSignals(True)
self.form.dsbSpacing.setValue(spacing)
self.form.dsbSpacing.blockSignals(False)
def updateModel(self):
self.getFields()
self.updateTagsView()
#FreeCAD.ActiveDocument.recompute()
def whenCountChanged(self):
print("whenCountChanged")
self.updateTagSpacing(self.form.sbCount.value())
self.autoApply()
count = self.form.sbCount.value()
self.form.pbGenerate.setEnabled(count)
def whenSpacingChanged(self):
print("whenSpacingChanged")
if self.form.dsbSpacing.value() == 0:
count = 0
else:
count = int(self.pathLength / self.form.dsbSpacing.value())
self.form.sbCount.blockSignals(True)
self.form.sbCount.setValue(count)
self.form.sbCount.blockSignals(False)
self.autoApply()
def whenTagSelectionChanged(self):
print('whenTagSelectionChanged')
item = self.form.lwTags.currentItem()
self.form.pbDelete.setEnabled(not item is None)
def whenOkClicked(self):
print("whenOkClicked")
self.whenApplyClicked()
self.form.toolBox.setCurrentWidget(self.form.tbpTags)
def deleteSelectedTag(self):
item = self.form.lwTags.currentItem()
x = item.data(self.DataX)
y = item.data(self.DataY)
tags = filter(lambda t: t.x != x or t.y != y, self.tags)
self.obj.Proxy.setTags(self.obj, tags)
self.updateTagsView()
def setupSpinBox(self, widget, val, decimals = 2):
widget.setMinimum(0)
@ -861,29 +881,44 @@ class TaskPanel:
widget.setValue(val)
def setFields(self):
self.pathLength = self.obj.Proxy.getPathLength(self.obj)
vHeader = self.form.twTags.verticalHeader()
vHeader.setResizeMode(QtGui.QHeaderView.Fixed)
vHeader.setDefaultSectionSize(20)
self.updateTags()
self.setupSpinBox(self.form.sbCount, self.form.twTags.rowCount(), None)
self.setupSpinBox(self.form.dsbSpacing, 0)
self.updateTagsView()
self.setupSpinBox(self.form.sbCount, len(self.tags), None)
self.setupSpinBox(self.form.dsbHeight, self.obj.Proxy.getHeight(self.obj))
self.setupSpinBox(self.form.dsbWidth, self.obj.Proxy.getWidth(self.obj))
self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.getAngle(self.obj))
self.updateTagSpacing(self.form.twTags.rowCount())
self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.getAngle(self.obj), 0)
self.form.dsbAngle.setMaximum(90)
self.form.dsbAngle.setSingleStep(5.)
def updateModelHeight(self):
print('updateModelHeight')
self.updateModel()
def updateModelWidth(self):
print('updateModelWidth')
self.updateModel()
def updateModelAngle(self):
print('updateModelAngle')
self.updateModel()
def updateModelTags(self):
print('updateModelTags')
self.updateModel()
def setupUi(self):
self.setFields()
self.whenCountChanged()
self.form.sbCount.valueChanged.connect(self.whenCountChanged)
self.form.dsbSpacing.valueChanged.connect(self.whenSpacingChanged)
self.form.dsbHeight.valueChanged.connect(self.autoApply)
self.form.dsbWidth.valueChanged.connect(self.autoApply)
self.form.dsbAngle.valueChanged.connect(self.autoApply)
#self.form.pbAdd.clicked.connect(self.)
self.form.buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.whenApplyClicked)
self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).clicked.connect(self.whenOkClicked)
self.form.twTags.itemChanged.connect(self.updateUI)
self.form.pbGenerate.clicked.connect(self.generateNewTags)
self.form.dsbHeight.editingFinished.connect(self.updateModelHeight)
self.form.dsbWidth.editingFinished.connect(self.updateModelWidth)
self.form.dsbAngle.editingFinished.connect(self.updateModelAngle)
self.form.lwTags.itemChanged.connect(self.updateModelTags)
self.form.lwTags.itemSelectionChanged.connect(self.whenTagSelectionChanged)
self.form.pbDelete.clicked.connect(self.deleteSelectedTag)
class SelObserver:
def __init__(self):

View File

@ -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)

View File

@ -52,7 +52,7 @@ class TestHoldingTags(PathTestBase):
def test01(self):
"""Verify solid for a 90 degree tag is a cylinder."""
tag = Tag(100, 200, 4, 5, 90, True)
tag.createSolidsAt(17)
tag.createSolidsAt(17, 0)
self.assertIsNotNone(tag.solid)
self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5)
@ -60,7 +60,7 @@ class TestHoldingTags(PathTestBase):
def test02(self):
"""Verify trapezoidal tag has a cone shape with a lid."""
tag = Tag(0, 0, 18, 5, 45, True)
tag.createSolidsAt(0)
tag.createSolidsAt(0, 0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5)
@ -68,14 +68,14 @@ class TestHoldingTags(PathTestBase):
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)
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)
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)