Merge pull request #2724 from mlampert/feature/tool-bit-poc

Path: Feature/tool bit poc
This commit is contained in:
sliptonic
2019-12-10 16:38:07 -06:00
committed by GitHub
61 changed files with 3892 additions and 214 deletions

View File

@@ -101,6 +101,12 @@ SET(PathScripts_SRCS
PathScripts/PathStop.py
PathScripts/PathSurface.py
PathScripts/PathSurfaceGui.py
PathScripts/PathToolBit.py
PathScripts/PathToolBitCmd.py
PathScripts/PathToolBitEdit.py
PathScripts/PathToolBitGui.py
PathScripts/PathToolBitLibraryCmd.py
PathScripts/PathToolBitLibraryGui.py
PathScripts/PathToolController.py
PathScripts/PathToolControllerGui.py
PathScripts/PathToolEdit.py
@@ -133,6 +139,29 @@ SET(PathScripts_post_SRCS
PathScripts/post/smoothie_post.py
)
SET(Tools_Bit_SRCS
Tools/Bit/t1.fctb
Tools/Bit/t2.fctb
Tools/Bit/t3.fctb
Tools/Bit/t4.fctb
Tools/Bit/t5.fctb
Tools/Bit/t6.fctb
Tools/Bit/t7.fctb
Tools/Bit/t8.fctb
Tools/Bit/t9.fctb
)
SET(Tools_Library_SRCS
Tools/Library/endmills.fctl
)
SET(Tools_Shape_SRCS
Tools/Shape/ballend.fcstd
Tools/Shape/bullnose.fcstd
Tools/Shape/drill.fcstd
Tools/Shape/endmill.fcstd
Tools/Shape/v-bit.fcstd
)
SET(PathTests_SRCS
PathTests/__init__.py
@@ -147,9 +176,11 @@ SET(PathTests_SRCS
PathTests/TestPathLog.py
PathTests/TestPathOpTools.py
PathTests/TestPathPost.py
PathTests/TestPathPreferences.py
PathTests/TestPathSetupSheet.py
PathTests/TestPathStock.py
PathTests/TestPathTool.py
PathTests/TestPathToolBit.py
PathTests/TestPathToolController.py
PathTests/TestPathTooltable.py
PathTests/TestPathUtil.py
@@ -178,6 +209,9 @@ SET(Path_Images
SET(all_files
${PathScripts_SRCS}
${PathScripts_post_SRCS}
${Tools_Bit_SRCS}
${Tools_Library_SRCS}
${Tools_Shape_SRCS}
${Path_Images}
)
@@ -218,6 +252,27 @@ INSTALL(
Mod/Path/PathScripts/post
)
INSTALL(
FILES
${Tools_Bit_SRCS}
DESTINATION
Mod/Path/Tools/Bit
)
INSTALL(
FILES
${Tools_Library_SRCS}
DESTINATION
Mod/Path/Tools/Library
)
INSTALL(
FILES
${Tools_Shape_SRCS}
DESTINATION
Mod/Path/Tools/Shape
)
INSTALL(
FILES
${PathImages_Ops}

View File

@@ -48,9 +48,10 @@
<file>icons/Path-Speed.svg</file>
<file>icons/Path-Stock.svg</file>
<file>icons/Path-Stop.svg</file>
<file>icons/Path-ToolBit.svg</file>
<file>icons/Path-ToolChange.svg</file>
<file>icons/Path-ToolController.svg</file>
<file>icons/Path-ToolDuplicate.svg</file>
<file>icons/Path-ToolDuplicate.svg</file>
<file>icons/Path-Toolpath.svg</file>
<file>icons/Path-ToolTable.svg</file>
<file>icons/Path-Area.svg</file>
@@ -107,6 +108,9 @@
<file>panels/PointEdit.ui</file>
<file>panels/SetupGlobal.ui</file>
<file>panels/SetupOp.ui</file>
<file>panels/ToolBitEditor.ui</file>
<file>panels/ToolBitLibraryEdit.ui</file>
<file>panels/ToolBitSelector.ui</file>
<file>panels/ToolEditor.ui</file>
<file>panels/ToolLibraryEditor.ui</file>
<file>panels/TaskPathSimulator.ui</file>

View File

@@ -0,0 +1,933 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64px"
height="64px"
id="svg2816"
version="1.1"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="Path-Tool.svg">
<defs
id="defs2818">
<linearGradient
inkscape:collect="always"
id="linearGradient3898">
<stop
style="stop-color:#888a85;stop-opacity:1"
offset="0"
id="stop3900" />
<stop
style="stop-color:#d3d7cf;stop-opacity:1"
offset="1"
id="stop3902" />
</linearGradient>
<linearGradient
id="linearGradient4513">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4515" />
<stop
style="stop-color:#999999;stop-opacity:1;"
offset="1"
id="stop4517" />
</linearGradient>
<linearGradient
id="linearGradient3681">
<stop
id="stop3697"
offset="0"
style="stop-color:#fff110;stop-opacity:1;" />
<stop
style="stop-color:#cf7008;stop-opacity:1;"
offset="1"
id="stop3685" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 32 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="64 : 32 : 1"
inkscape:persp3d-origin="32 : 21.333333 : 1"
id="perspective2824" />
<inkscape:perspective
id="perspective3622"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3622-9"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3653"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3675"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3697"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3720"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3742"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3764"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3785"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3806"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3806-3"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3835"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3614"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3614-8"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3643"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3643-3"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3672"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3672-5"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3701"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3701-8"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3746"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<pattern
patternTransform="matrix(0.67643728,-0.81829155,2.4578314,1.8844554,-26.450606,18.294947)"
id="pattern5231"
xlink:href="#Strips1_1-4"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5224"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<pattern
inkscape:stockid="Stripes 1:1"
id="Strips1_1-4"
patternTransform="matrix(0.66772843,-1.0037085,2.4261878,2.3114548,3.4760987,3.534923)"
height="1"
width="2"
patternUnits="userSpaceOnUse"
inkscape:collect="always">
<rect
id="rect4483-4"
height="2"
width="1"
y="-0.5"
x="0"
style="fill:black;stroke:none" />
</pattern>
<inkscape:perspective
id="perspective5224-9"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<pattern
patternTransform="matrix(0.66772843,-1.0037085,2.4261878,2.3114548,39.618381,8.9692804)"
id="pattern5231-4"
xlink:href="#Strips1_1-6"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5224-3"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<pattern
inkscape:stockid="Stripes 1:1"
id="Strips1_1-6"
patternTransform="matrix(0.66772843,-1.0037085,2.4261878,2.3114548,3.4760987,3.534923)"
height="1"
width="2"
patternUnits="userSpaceOnUse"
inkscape:collect="always">
<rect
id="rect4483-0"
height="2"
width="1"
y="-0.5"
x="0"
style="fill:black;stroke:none" />
</pattern>
<pattern
patternTransform="matrix(0.66513382,-1.0631299,2.4167603,2.4482973,-49.762569,2.9546807)"
id="pattern5296"
xlink:href="#pattern5231-3"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5288"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<pattern
patternTransform="matrix(0.66772843,-1.0037085,2.4261878,2.3114548,-26.336284,10.887197)"
id="pattern5231-3"
xlink:href="#Strips1_1-4-3"
inkscape:collect="always" />
<pattern
inkscape:stockid="Stripes 1:1"
id="Strips1_1-4-3"
patternTransform="matrix(0.66772843,-1.0037085,2.4261878,2.3114548,3.4760987,3.534923)"
height="1"
width="2"
patternUnits="userSpaceOnUse"
inkscape:collect="always">
<rect
id="rect4483-4-6"
height="2"
width="1"
y="-0.5"
x="0"
style="fill:black;stroke:none" />
</pattern>
<pattern
patternTransform="matrix(0.42844886,-0.62155849,1.5567667,1.431396,27.948414,13.306456)"
id="pattern5330"
xlink:href="#Strips1_1-9"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5323"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<pattern
inkscape:stockid="Stripes 1:1"
id="Strips1_1-9"
patternTransform="matrix(0.66772843,-1.0037085,2.4261878,2.3114548,3.4760987,3.534923)"
height="1"
width="2"
patternUnits="userSpaceOnUse"
inkscape:collect="always">
<rect
id="rect4483-3"
height="2"
width="1"
y="-0.5"
x="0"
style="fill:black;stroke:none" />
</pattern>
<inkscape:perspective
id="perspective5361"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective5383"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective5411"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3681"
id="linearGradient3687"
x1="37.89756"
y1="41.087898"
x2="4.0605712"
y2="40.168594"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(127.27273,-51.272729)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3681"
id="linearGradient3695"
x1="37.894287"
y1="40.484772"
x2="59.811455"
y2="43.558987"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(127.27273,-51.272729)" />
<linearGradient
id="linearGradient3681-3">
<stop
id="stop3697-3"
offset="0"
style="stop-color:#fff110;stop-opacity:1;" />
<stop
style="stop-color:#cf7008;stop-opacity:1;"
offset="1"
id="stop3685-4" />
</linearGradient>
<linearGradient
y2="43.558987"
x2="59.811455"
y1="40.484772"
x1="37.894287"
gradientTransform="translate(-37.00068,-20.487365)"
gradientUnits="userSpaceOnUse"
id="linearGradient3608"
xlink:href="#linearGradient3681-3"
inkscape:collect="always" />
<linearGradient
id="linearGradient4513-2">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4515-2" />
<stop
style="stop-color:#999999;stop-opacity:1;"
offset="1"
id="stop4517-4" />
</linearGradient>
<radialGradient
r="23.634638"
fy="7.9319997"
fx="32.151962"
cy="7.9319997"
cx="32.151962"
gradientTransform="matrix(1,0,0,1.1841158,-8.5173246,-3.4097568)"
gradientUnits="userSpaceOnUse"
id="radialGradient4538"
xlink:href="#linearGradient4513-2"
inkscape:collect="always" />
<linearGradient
id="linearGradient4513-1">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4515-8" />
<stop
style="stop-color:#999999;stop-opacity:1;"
offset="1"
id="stop4517-6" />
</linearGradient>
<radialGradient
r="23.634638"
fy="7.9319997"
fx="32.151962"
cy="7.9319997"
cx="32.151962"
gradientTransform="matrix(1,0,0,1.1841158,-8.5173246,-3.4097568)"
gradientUnits="userSpaceOnUse"
id="radialGradient4538-6"
xlink:href="#linearGradient4513-1"
inkscape:collect="always" />
<linearGradient
id="linearGradient4513-1-3">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4515-8-7" />
<stop
style="stop-color:#999999;stop-opacity:1;"
offset="1"
id="stop4517-6-5" />
</linearGradient>
<radialGradient
r="23.634638"
fy="35.869175"
fx="32.151962"
cy="35.869175"
cx="32.151962"
gradientTransform="matrix(0.39497909,0,0,1.1841158,-2.716491,-26.067007)"
gradientUnits="userSpaceOnUse"
id="radialGradient3069"
xlink:href="#linearGradient4513-1-3"
inkscape:collect="always" />
<linearGradient
id="linearGradient4513-1-2">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4515-8-6" />
<stop
style="stop-color:#999999;stop-opacity:1;"
offset="1"
id="stop4517-6-6" />
</linearGradient>
<radialGradient
r="23.634638"
fy="35.869175"
fx="32.151962"
cy="35.869175"
cx="32.151962"
gradientTransform="matrix(0.39497909,0,0,1.1841158,-2.716491,-26.067007)"
gradientUnits="userSpaceOnUse"
id="radialGradient3102"
xlink:href="#linearGradient4513-1-2"
inkscape:collect="always" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4513-1"
id="radialGradient3132"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.39497909,0,0,1.1841158,41.43166,2.0473921)"
cx="32.151962"
cy="27.950663"
fx="32.151962"
fy="27.950663"
r="23.634638" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4513-1-27"
id="radialGradient3132-0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.39497909,0,0,1.1841158,38.088973,2.175957)"
cx="32.151962"
cy="27.950663"
fx="32.151962"
fy="27.950663"
r="23.634638" />
<linearGradient
id="linearGradient4513-1-27">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4515-8-1" />
<stop
style="stop-color:#999999;stop-opacity:1;"
offset="1"
id="stop4517-6-0" />
</linearGradient>
<radialGradient
r="23.634638"
fy="27.950663"
fx="32.151962"
cy="27.950663"
cx="32.151962"
gradientTransform="matrix(0.39497909,0,0,1.1841158,18.770923,-9.8531183)"
gradientUnits="userSpaceOnUse"
id="radialGradient3101"
xlink:href="#linearGradient4513-1-27"
inkscape:collect="always" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4513-1-5"
id="radialGradient3132-02"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.39497909,0,0,1.1841158,38.088973,2.175957)"
cx="32.151962"
cy="27.950663"
fx="32.151962"
fy="27.950663"
r="23.634638" />
<linearGradient
id="linearGradient4513-1-5">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4515-8-10" />
<stop
style="stop-color:#999999;stop-opacity:1;"
offset="1"
id="stop4517-6-1" />
</linearGradient>
<radialGradient
r="23.634638"
fy="27.950663"
fx="32.151962"
cy="27.950663"
cx="32.151962"
gradientTransform="matrix(0.39497909,0,0,1.1841158,-1.4349938,4.2466976)"
gradientUnits="userSpaceOnUse"
id="radialGradient3101-6"
xlink:href="#linearGradient4513-1-5"
inkscape:collect="always" />
<linearGradient
gradientTransform="translate(22,2)"
inkscape:collect="always"
xlink:href="#linearGradient3989"
id="linearGradient3987"
x1="31"
y1="15"
x2="33"
y2="23"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3989">
<stop
id="stop3991"
offset="0"
style="stop-color:#d3d7cf;stop-opacity:1" />
<stop
id="stop3993"
offset="1"
style="stop-color:#888a85;stop-opacity:1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4031"
id="linearGradient4029"
x1="27.909092"
y1="27.90909"
x2="36"
y2="54.227272"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient4031">
<stop
id="stop4033"
offset="0"
style="stop-color:#d3d7cf;stop-opacity:1" />
<stop
id="stop4035"
offset="1"
style="stop-color:#888a85;stop-opacity:1" />
</linearGradient>
<linearGradient
gradientTransform="translate(20,3)"
y2="54"
x2="35"
y1="11"
x1="29"
gradientUnits="userSpaceOnUse"
id="linearGradient4154"
xlink:href="#linearGradient4031"
inkscape:collect="always" />
<linearGradient
gradientTransform="translate(20,3)"
y2="54"
x2="35"
y1="11"
x1="29"
gradientUnits="userSpaceOnUse"
id="linearGradient4154-7"
xlink:href="#linearGradient4031-5"
inkscape:collect="always" />
<linearGradient
id="linearGradient4031-5">
<stop
id="stop4033-3"
offset="0"
style="stop-color:#d3d7cf;stop-opacity:1" />
<stop
id="stop4035-5"
offset="1"
style="stop-color:#888a85;stop-opacity:1" />
</linearGradient>
<linearGradient
y2="54"
x2="35"
y1="11"
x1="29"
gradientTransform="translate(-2,-8)"
gradientUnits="userSpaceOnUse"
id="linearGradient3118"
xlink:href="#linearGradient4031-5"
inkscape:collect="always" />
<linearGradient
gradientTransform="translate(20,3)"
y2="54"
x2="35"
y1="11"
x1="29"
gradientUnits="userSpaceOnUse"
id="linearGradient4154-9"
xlink:href="#linearGradient4031-1"
inkscape:collect="always" />
<linearGradient
id="linearGradient4031-1">
<stop
id="stop4033-2"
offset="0"
style="stop-color:#d3d7cf;stop-opacity:1" />
<stop
id="stop4035-7"
offset="1"
style="stop-color:#888a85;stop-opacity:1" />
</linearGradient>
<linearGradient
y2="54"
x2="35"
y1="11"
x1="29"
gradientTransform="translate(-24,10)"
gradientUnits="userSpaceOnUse"
id="linearGradient3118-0"
xlink:href="#linearGradient4031-1"
inkscape:collect="always" />
<linearGradient
gradientTransform="translate(20,3)"
y2="54"
x2="35"
y1="11"
x1="29"
gradientUnits="userSpaceOnUse"
id="linearGradient4154-8"
xlink:href="#linearGradient4031-7"
inkscape:collect="always" />
<linearGradient
id="linearGradient4031-7">
<stop
id="stop4033-9"
offset="0"
style="stop-color:#d3d7cf;stop-opacity:1" />
<stop
id="stop4035-2"
offset="1"
style="stop-color:#888a85;stop-opacity:1" />
</linearGradient>
<linearGradient
gradientTransform="translate(20,3)"
y2="54"
x2="35"
y1="11"
x1="29"
gradientUnits="userSpaceOnUse"
id="linearGradient4154-5"
xlink:href="#linearGradient4031-9"
inkscape:collect="always" />
<linearGradient
id="linearGradient4031-9">
<stop
id="stop4033-22"
offset="0"
style="stop-color:#d3d7cf;stop-opacity:1" />
<stop
id="stop4035-8"
offset="1"
style="stop-color:#888a85;stop-opacity:1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4031"
id="linearGradient4030"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(20,3)"
x1="29"
y1="11"
x2="35"
y2="54" />
<linearGradient
id="linearGradient4031-74">
<stop
id="stop4033-0"
offset="0"
style="stop-color:#d3d7cf;stop-opacity:1" />
<stop
id="stop4035-9"
offset="1"
style="stop-color:#888a85;stop-opacity:1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3898"
id="linearGradient3904"
x1="35.05999"
y1="53.008698"
x2="27.286415"
y2="7.311924"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3898-1"
id="linearGradient3920-6"
gradientUnits="userSpaceOnUse"
x1="35.05999"
y1="53.008698"
x2="27.286415"
y2="7.311924" />
<linearGradient
inkscape:collect="always"
id="linearGradient3898-1">
<stop
style="stop-color:#888a85;stop-opacity:1"
offset="0"
id="stop3900-4" />
<stop
style="stop-color:#d3d7cf;stop-opacity:1"
offset="1"
id="stop3902-2" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3898-1"
id="linearGradient3916-3"
gradientUnits="userSpaceOnUse"
x1="35.05999"
y1="53.008698"
x2="27.286415"
y2="7.311924" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3898-1"
id="linearGradient3918-2"
gradientUnits="userSpaceOnUse"
x1="35.05999"
y1="53.008698"
x2="27.286415"
y2="7.311924" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3898-8-9"
id="linearGradient3920-8-8"
gradientUnits="userSpaceOnUse"
x1="35.05999"
y1="53.008698"
x2="27.286415"
y2="7.311924" />
<linearGradient
inkscape:collect="always"
id="linearGradient3898-8-9">
<stop
style="stop-color:#888a85;stop-opacity:1"
offset="0"
id="stop3900-2-2" />
<stop
style="stop-color:#d3d7cf;stop-opacity:1"
offset="1"
id="stop3902-4-7" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3898-8-9"
id="linearGradient3916-5-9"
gradientUnits="userSpaceOnUse"
x1="35.05999"
y1="53.008698"
x2="27.286415"
y2="7.311924" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3898-8-9"
id="linearGradient3918-5-5"
gradientUnits="userSpaceOnUse"
x1="35.05999"
y1="53.008698"
x2="27.286415"
y2="7.311924" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="8.0000004"
inkscape:cx="-26.037067"
inkscape:cy="38.278529"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:document-units="px"
inkscape:grid-bbox="true"
inkscape:snap-bbox="true"
inkscape:bbox-paths="false"
inkscape:bbox-nodes="false"
inkscape:snap-bbox-edge-midpoints="false"
inkscape:snap-bbox-midpoints="false"
inkscape:object-paths="true"
inkscape:object-nodes="true"
inkscape:window-width="3064"
inkscape:window-height="1598"
inkscape:window-x="0"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:snap-global="true"
inkscape:snap-nodes="false">
<inkscape:grid
type="xygrid"
id="grid3067"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" />
</sodipodi:namedview>
<metadata
id="metadata2821">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<dc:title>Path-ToolTable</dc:title>
<dc:date>2015-07-04</dc:date>
<dc:relation>http://www.freecadweb.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-ToolTable.svg</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<g
id="g3908-2"
transform="translate(-41.849711,-4)">
<g
id="g3859-1"
transform="matrix(0.78612717,0,0,1,48.768786,3)">
<g
style="fill:url(#linearGradient3920-6);fill-opacity:1"
id="g3126-6">
<path
sodipodi:nodetypes="ccccccccccccccc"
inkscape:connector-curvature="0"
id="rect4417-8"
d="m 23,28 0,9 18,-8 0,-9 z m 18,3 -18,8 0,9 18,-8 z m 0,11 -18,8 9,6 9,-6 z"
style="color:#000000;fill:url(#linearGradient3916-3);fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2.25571179;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path3085-57"
d="m 23.191177,26 0,-20 L 41,6 41,18 z"
style="fill:url(#linearGradient3918-2);fill-opacity:1;stroke:#2e3436;stroke-width:2.25571179;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" />
</g>
<path
style="color:#000000;fill:none;stroke:#d3d7cf;stroke-width:2.25571179;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 25.536535,29.21875 0.02839,4.267045 L 38.41613,27.875 38.45588,23.40625 z m 12.925366,5.17498 -13.004096,5.795099 0.04815,4.413423 13.036237,-5.775011 z m -0.06224,11.048212 -10.969048,4.881281 4.537301,3.001812 L 38.455882,49 z"
id="rect4417-1-4-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccc" />
</g>
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path3906-1"
d="M 69,11 69,25.53033 79.044194,19.779029 79,11 z"
style="fill:none;stroke:#d3d7cf;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,233 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>587</width>
<height>744</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QToolBox" name="toolBox">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>559</width>
<height>626</height>
</rect>
</property>
<attribute name="label">
<string>Shape</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Tool Bit</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="toolName">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Display name of the Tool Bit (initial value taken from the shape file).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maxLength">
<number>50</number>
</property>
<property name="placeholderText">
<string>Display Name</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="shapePath">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The file which defines the type and shape of the Tool Bit.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="shapeSet">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Change file defining type and shape of Tool Bit.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="bitParams">
<property name="title">
<string>Bit Parameter</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Point/Tip Angle</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="Gui::InputField" name="toolCuttingEdgeAngle">
<property name="text">
<string>180°</string>
</property>
<property name="unit" stdset="0">
<string>°</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Cutting Edge Height</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::InputField" name="toolCuttingEdgeHeight">
<property name="text">
<string>0.00</string>
</property>
<property name="unit" stdset="0">
<string>mm</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="image">
<property name="maximumSize">
<size>
<width>210</width>
<height>297</height>
</size>
</property>
<property name="text">
<string>Image</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>277</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>559</width>
<height>626</height>
</rect>
</property>
<attribute name="label">
<string>Attributes</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableView" name="attrTable">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>2</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::AllEditTriggers</set>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::InputField</class>
<extends>QLineEdit</extends>
<header>Gui/InputField.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>958</width>
<height>508</height>
</rect>
</property>
<property name="windowTitle">
<string>ToolBit Library</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="libraryNew">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Create a new library with an empty list of Tool Bits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../Gui/Icons/resource.qrc">
<normaloff>:/icons/document-new.svg</normaloff>:/icons/document-new.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="libraryOpen">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Open an existing Tool Bit library.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../Gui/Icons/resource.qrc">
<normaloff>:/icons/document-open.svg</normaloff>:/icons/document-open.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="librarySave">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Save Tool Bit library.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../Gui/Icons/resource.qrc">
<normaloff>:/icons/document-save.svg</normaloff>:/icons/document-save.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="librarySaveAs">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Save Tool Bit library under new name.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../Gui/Icons/resource.qrc">
<normaloff>:/icons/document-save-as.svg</normaloff>:/icons/document-save-as.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="libraryPref">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Edit Tool Bit library editor settings.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../Gui/Icons/resource.qrc">
<normaloff>:/icons/preferences-system.svg</normaloff>:/icons/preferences-system.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="toolTableGroup" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="toolTable">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Table of Tool Bits of the library.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_3" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="toolAdd">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Add another Tool Bit to this library.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add ...</string>
</property>
<property name="icon">
<iconset resource="../../../../../Gui/Icons/resource.qrc">
<normaloff>:/icons/list-add.svg</normaloff>:/icons/list-add.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="toolDelete">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Delete selected Tool Bit(s) from the library.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="icon">
<iconset resource="../../../../../Gui/Icons/resource.qrc">
<normaloff>:/icons/list-remove.svg</normaloff>:/icons/list-remove.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="toolEnumerate">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Assigne numbers to each Tool Bit according to its current position in the library. The first Tool Bit is assigned the ID 1.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enumerate</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>115</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../../Gui/Icons/resource.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>588</width>
<height>396</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="tools">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Available Tool Bits to choose from.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="toolLoad">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Load an existing Tool Bit from a file.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Load...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="toolCreate">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Create a new Tool Bit based on an existing shape.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>New</string>
</property>
</widget>
</item>
<item>
<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>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

File diff suppressed because one or more lines are too long

View File

@@ -71,6 +71,16 @@ class PathWorkbench (Workbench):
FreeCADGui.addIconPath(":/icons")
from PathScripts import PathGuiInit
from PathScripts import PathJobCmd
from PathScripts import PathToolBitCmd
from PathScripts import PathToolBitLibraryCmd
if PathPreferences.experimentalFeaturesEnabled():
toolbitcmdlist = PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"]
self.toolbitctxmenu = ["Path_ToolBitLibraryLoad", "Path_ToolController"]
else:
toolbitcmdlist = []
self.toolbitctxmenu = []
import PathCommands
PathGuiInit.Startup()
@@ -112,7 +122,7 @@ class PathWorkbench (Workbench):
if extracmdlist:
self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist)
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"])
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolbitcmdlist + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"])
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
"Path", "Path Dressup")], dressupcmdlist)
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
@@ -143,6 +153,7 @@ class PathWorkbench (Workbench):
def ContextMenu(self, recipient):
import PathScripts
menuAppended = False
if len(FreeCADGui.Selection.getSelection()) == 1:
obj = FreeCADGui.Selection.getSelection()[0]
if obj.isDerivedFrom("Path::Feature"):
@@ -152,9 +163,11 @@ class PathWorkbench (Workbench):
if "Remote" in selectedName:
self.appendContextMenu("", ["Refresh_Path"])
if "Job" in selectedName:
self.appendContextMenu("", ["Path_ExportTemplate"])
if isinstance (obj.Proxy, PathScripts.PathOp.ObjectOp):
self.appendContextMenu("", ["Path_ExportTemplate"] + self.toolbitctxmenu)
menuAppended = True
if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp):
self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"])
menuAppended = True
if obj.isDerivedFrom("Path::Feature"):
if "Profile" in selectedName or "Contour" in selectedName or "Dressup" in selectedName:
self.appendContextMenu("", "Separator")
@@ -162,6 +175,12 @@ class PathWorkbench (Workbench):
#self.appendContextMenu("", ["Set_EndPoint"])
for cmd in self.dressupcmds:
self.appendContextMenu("", [cmd])
menuAppended = True
if isinstance(obj.Proxy, PathScripts.PathToolBit.ToolBit):
self.appendContextMenu("", ["Path_ToolBitSave", "Path_ToolBitSaveAs"])
menuAppended = True
if menuAppended:
self.appendContextMenu("", "Separator")
Gui.addWorkbench(PathWorkbench())

View File

@@ -143,7 +143,7 @@ class ObjectOp(PathOp.ObjectOp):
return shape.Curve.Radius * 2
if shape.ShapeType == 'Face':
for i in range(len(shape.Edges)):
for i in range(len(shape.Edges)):
if (type(shape.Edges[i].Curve) == Part.Circle and
shape.Edges[i].Curve.Radius * 2 < shape.BoundBox.XLength*1.1 and
shape.Edges[i].Curve.Radius * 2 > shape.BoundBox.XLength*0.9):
@@ -384,7 +384,7 @@ class ObjectOp(PathOp.ObjectOp):
if 1 == len(self.model) and self.baseIsArchPanel(obj, self.model[0]):
panel = self.model[0]
holeshapes = panel.Proxy.getHoles(panel, transform=True)
tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter
tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter)
for holeNr, hole in enumerate(holeshapes):
PathLog.debug('Entering new HoleShape')
for wireNr, wire in enumerate(hole.Wires):
@@ -405,7 +405,7 @@ class ObjectOp(PathOp.ObjectOp):
PathLog.track('obj: {} shape: {}'.format(obj, shape))
holelist = []
features = []
# tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter
# tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter)
tooldiameter = None
PathLog.debug('search for holes larger than tooldiameter: {}: '.format(tooldiameter))
if DraftGeomUtils.isPlanar(shape):

View File

@@ -48,15 +48,15 @@ def translate(context, text, disambig=None):
def toolDepthAndOffset(width, extraDepth, tool):
'''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given parameters.'''
angle = tool.CuttingEdgeAngle
angle = float(tool.CuttingEdgeAngle)
if 0 == angle:
angle = 180
tan = math.tan(math.radians(angle / 2))
toolDepth = 0 if 0 == tan else width / tan
depth = toolDepth + extraDepth
toolOffset = tool.FlatRadius
extraOffset = tool.Diameter / 2 - width if 180 == angle else extraDepth / tan
toolOffset = float(tool.FlatRadius)
extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan
offset = toolOffset + extraOffset
return (depth, offset)

View File

@@ -860,10 +860,10 @@ class ObjectDressup:
self.toolRadius = 5
else:
tool = tc.Proxy.getTool(tc) # PathUtils.getTool(obj, tc.ToolNumber)
if not tool or tool.Diameter == 0:
if not tool or float(tool.Diameter) == 0:
self.toolRadius = 5
else:
self.toolRadius = tool.Diameter / 2
self.toolRadius = float(tool.Diameter) / 2
self.shapes = {}
self.dbg = []

View File

@@ -671,7 +671,7 @@ class PathData:
print("tag[%d]" % i)
if not i in fromObj.Disabled:
dist = self.baseWire.distToShape(Part.Vertex(FreeCAD.Vector(pos.x, pos.y, self.minZ)))
if dist[0] < W:
if True or dist[0] < W:
print("tag[%d/%d]: (%.2f, %.2f, %.2f)" % (i, j, pos.x, pos.y, self.minZ))
at = dist[1][0][0]
tags.append(Tag(j, at.x, at.y, W, H, A, R, True))
@@ -759,7 +759,13 @@ class ObjectTagDressup:
obj.addProperty("App::PropertyIntegerList", "Disabled", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "IDs of disabled holding tags"))
obj.addProperty("App::PropertyInteger", "SegmentationFactor", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "Factor determining the # of segments used to approximate rounded tags."))
self.__setstate__(obj)
# for pylint ...
self.obj = obj
self.solids = []
self.tags = []
self.pathData = None
self.toolRadius = None
self.mappers = []
obj.Proxy = self
obj.Base = base
@@ -1021,7 +1027,7 @@ class ObjectTagDressup:
# traceback.print_exc()
return None
self.toolRadius = PathDressup.toolController(obj.Base).Tool.Diameter / 2
self.toolRadius = float(PathDressup.toolController(obj.Base).Tool.Diameter) / 2
self.pathData = pathData
if generate:
obj.Height = self.pathData.defaultTagHeight()

View File

@@ -220,7 +220,7 @@ class ObjectDressup:
PathLog.track()
def toolRadius(self):
return PathDressup.toolController(self.obj.Base).Tool.Diameter / 2.0
return float(PathDressup.toolController(self.obj.Base).Tool.Diameter) / 2.0
def addTagsToDocuemnt(self):
for i, solid in enumerate(self.solids):

View File

@@ -51,6 +51,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
'''Controller for the drilling operation's page'''
def initPage(self, obj):
# pylint: disable=attribute-defined-outside-init
self.peckDepthSpinBox = PathGui.QuantitySpinBox(self.form.peckDepth, obj, 'PeckDepth')
self.peckRetractSpinBox = PathGui.QuantitySpinBox(self.form.peckRetractHeight, obj, 'RetractHeight')
self.dwellTimeSpinBox = PathGui.QuantitySpinBox(self.form.dwellTime, obj, 'DwellTime')
@@ -80,6 +81,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpDrillingEdit.ui")
def updateQuantitySpinBoxes(self, index = None):
# pylint: disable=unused-argument
self.peckDepthSpinBox.updateSpinBox()
self.peckRetractSpinBox.updateSpinBox()
self.dwellTimeSpinBox.updateSpinBox()

View File

@@ -25,6 +25,7 @@
import FreeCAD
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathUtil as PathUtil
import PySide
@@ -44,34 +45,6 @@ if LOGLEVEL:
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def _getProperty(obj, prop):
o = obj
attr = obj
name = None
for name in prop.split('.'):
o = attr
if not hasattr(o, name):
break
attr = getattr(o, name)
if o == attr:
PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name))
return (None, None, None)
#PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr))
return(o, attr, name)
def getProperty(obj, prop):
'''getProperty(obj, prop) ... answer obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
return attr
def setProperty(obj, prop, value):
'''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
if o and name:
setattr(o, name, value)
def updateInputField(obj, prop, widget, onBeforeChange=None):
'''updateInputField(obj, prop, widget) ... update obj's property prop with the value of widget.
The property's value is only assigned if the new value differs from the current value.
@@ -82,13 +55,13 @@ If onBeforeChange is specified it is called before a new value is assigned to th
Returns True if a new value was assigned, False otherwise (new value is the same as the current).
'''
value = FreeCAD.Units.Quantity(widget.text()).Value
attr = getProperty(obj, prop)
attr = PathUtil.getProperty(obj, prop)
attrValue = attr.Value if hasattr(attr, 'Value') else attr
if not PathGeom.isRoughly(attrValue, value):
PathLog.debug("updateInputField(%s, %s): %.2f -> %.2f" % (obj.Label, prop, attr, value))
if onBeforeChange:
onBeforeChange(obj)
setProperty(obj, prop, value)
PathUtil.setProperty(obj, prop, value)
return True
return False
@@ -107,7 +80,7 @@ The spin box gets bound to a given property and supports update in both directio
self.widget = widget
self.prop = prop
self.onBeforeChange = onBeforeChange
attr = getProperty(self.obj, self.prop)
attr = PathUtil.getProperty(self.obj, self.prop)
if attr is not None:
if hasattr(attr, 'Value'):
widget.setProperty('unit', attr.getUserPreferred()[2])
@@ -134,7 +107,7 @@ If no value is provided the value of the bound property is used.
quantity can be of type Quantity or Float.'''
if self.valid:
if quantity is None:
quantity = getProperty(self.obj, self.prop)
quantity = PathUtil.getProperty(self.obj, self.prop)
value = quantity.Value if hasattr(quantity, 'Value') else quantity
self.widget.setProperty('rawValue', value)

View File

@@ -222,6 +222,7 @@ class ObjectJob:
PathLog.debug('taking down tool controller')
for tc in obj.ToolController:
PathUtil.clearExpressionEngine(tc)
tc.Proxy.onDelete(tc)
doc.removeObject(tc.Name)
obj.ToolController = []
# SetupSheet

View File

@@ -39,13 +39,8 @@ from PySide import QtCore, QtGui
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
LOGLEVEL = False
if LOGLEVEL:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
class CommandJobCreate:
'''
@@ -186,5 +181,5 @@ if FreeCAD.GuiUp:
FreeCADGui.addCommand('Path_Job', CommandJobCreate())
FreeCADGui.addCommand('Path_ExportTemplate', CommandJobTemplateExport())
FreeCAD.Console.PrintLog("Loading PathJobGui... done\n")
FreeCAD.Console.PrintLog("Loading PathJobCmd... done\n")

View File

@@ -80,7 +80,8 @@ class ObjectFace(PathPocketBase.ObjectPocket):
# default depths calculation not correct for facing
if prop == "Base":
job = PathUtils.findParentJob(obj)
obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax
if job:
obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax
if len(obj.Base) >= 1:
print('processing')
@@ -95,7 +96,7 @@ class ObjectFace(PathPocketBase.ObjectPocket):
# Otherwise, top of part.
obj.OpFinalDepth = Part.makeCompound(sublist).BoundBox.ZMax
else:
elif job:
obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax
def areaOpShapes(self, obj):
@@ -130,7 +131,7 @@ class ObjectFace(PathPocketBase.ObjectPocket):
else:
holes.append((b[0].Shape, wire))
else:
PathLog.error('The base subobject, "{}," is not a face. Ignoring "{}."'.format(sub, sub))
PathLog.error('The base subobject, "{0}," is not a face. Ignoring "{0}."'.format(sub))
if obj.ExcludeRaisedAreas is True and len(holes) > 0:
for shape, wire in holes:

View File

@@ -498,10 +498,10 @@ class ObjectOp(object):
self.vertRapid = tc.VertRapid.Value
self.horizRapid = tc.HorizRapid.Value
tool = tc.Proxy.getTool(tc)
if not tool or tool.Diameter == 0:
if not tool or float(tool.Diameter) == 0:
FreeCAD.Console.PrintError("No Tool found or diameter is zero. We need a tool to build a Path.")
return
self.radius = tool.Diameter/2
self.radius = float(tool.Diameter) /2
self.tool = tool
obj.OpToolDiameter = tool.Diameter

View File

@@ -38,7 +38,7 @@ class PostProcessor:
def load(cls, processor):
PathLog.track(processor)
syspath = sys.path
paths = PathPreferences.searchPaths()
paths = PathPreferences.searchPathsPost()
paths.extend(sys.path)
sys.path = paths

View File

@@ -41,6 +41,13 @@ PostProcessorBlacklist = "PostProcessorBlacklist"
PostProcessorOutputFile = "PostProcessorOutputFile"
PostProcessorOutputPolicy = "PostProcessorOutputPolicy"
LastPathToolBit = "LastPathToolBit"
LastPathToolLibrary = "LastPathToolLibrary"
LastPathToolShape = "LastPathToolShape"
UseLegacyTools = "UseLegacyTools"
UseAbsoluteToolPaths = "UseAbsoluteToolPaths"
# Linear tolerance to use when generating Paths, eg when tessellating geometry
GeometryTolerance = "GeometryTolerance"
LibAreaCurveAccuracy = "LibAreaCurveAccuarcy"
@@ -52,14 +59,16 @@ def preferences():
return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path")
def pathScriptsSourcePath():
return FreeCAD.getHomePath() + ("Mod/Path/PathScripts/")
return os.path.join(FreeCAD.getHomePath(), "Mod/Path/PathScripts/")
def pathScriptsPostSourcePath():
return pathScriptsSourcePath() + ("/post/")
def pathDefaultToolsPath(sub=None):
if sub:
return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/", sub)
return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/")
def allAvailablePostProcessors():
allposts = []
for path in searchPaths():
for path in searchPathsPost():
posts = [ str(os.path.split(os.path.splitext(p)[0])[1][:-5]) for p in glob.glob(path + '/*_post.py')]
allposts.extend(posts)
allposts.sort()
@@ -108,10 +117,52 @@ def searchPaths():
if p:
paths.append(p)
paths.append(macroFilePath())
paths.append(pathScriptsPostSourcePath())
return paths
def searchPathsPost():
paths = []
p = defaultFilePath()
if p:
paths.append(p)
paths.append(macroFilePath())
paths.append(os.path.join(pathScriptsSourcePath(), "post/"))
paths.append(pathScriptsSourcePath())
return paths
def searchPathsTool(sub='Bit'):
paths = []
if 'Bit' == sub:
paths.append(lastPathToolBit())
if 'Library' == sub:
paths.append(lastPathToolLibrary())
if 'Shape' == sub:
paths.append(lastPathToolShape())
def appendPath(p, sub):
if p:
paths.append(os.path.join(p, 'Tools', sub))
paths.append(os.path.join(p, sub))
paths.append(p)
appendPath(defaultFilePath(), sub)
appendPath(macroFilePath(), sub)
appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub)
return paths
def toolsUseLegacyTools():
return preferences().GetBool(UseLegacyTools, True)
def toolsReallyUseLegacyTools():
return toolsUseLegacyTools() or not experimentalFeaturesEnabled()
def toolsStoreAbsolutePaths():
return preferences().GetBool(UseAbsoluteToolPaths, False)
def setToolsSettings(legacy, relative):
pref = preferences()
pref.SetBool(UseLegacyTools, legacy)
pref.SetBool(UseAbsoluteToolPaths, relative)
def defaultJobTemplate():
template = preferences().GetString(DefaultJobTemplate)
if 'xml' not in template:
@@ -165,3 +216,19 @@ def setDefaultTaskPanelLayout(style):
def experimentalFeaturesEnabled():
return preferences().GetBool(EnableExperimentalFeatures, False)
def lastPathToolBit():
return preferences().GetString(LastPathToolBit, pathDefaultToolsPath('Bit'))
def setLastPathToolBit(path):
return preferences().SetString(LastPathToolBit, path)
def lastPathToolLibrary():
return preferences().GetString(LastPathToolLibrary, pathDefaultToolsPath('Library'))
def setLastPathToolLibrary(path):
return preferences().SetString(LastPathToolLibrary, path)
def lastPathToolShape():
return preferences().GetString(LastPathToolShape, pathDefaultToolsPath('Shape'))
def setLastPathToolShape(path):
return preferences().SetString(LastPathToolShape, path)

View File

@@ -71,6 +71,7 @@ class JobPreferencesPage:
policy = str(self.form.cboOutputPolicy.currentText())
PathPreferences.setOutputFileDefaults(path, policy)
self.saveStockSettings()
self.saveToolsSettings()
def saveStockSettings(self):
if self.form.stockGroup.isChecked():
@@ -107,6 +108,9 @@ class JobPreferencesPage:
else:
PathPreferences.setDefaultStockTemplate('')
def saveToolsSettings(self):
PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsAbsolutePaths.isChecked())
def selectComboEntry(self, widget, text):
index = widget.findText(text, QtCore.Qt.MatchFixedString)
if index >= 0:
@@ -167,6 +171,7 @@ class JobPreferencesPage:
self.form.tbOutputFile.clicked.connect(self.browseOutputFile)
self.loadStockSettings()
self.loadToolSettings()
def loadStockSettings(self):
stock = PathPreferences.defaultStockTemplate()
@@ -244,6 +249,10 @@ class JobPreferencesPage:
self.form.stockCreateBox.hide()
self.form.stockCreateCylinder.hide()
def loadToolSettings(self):
self.form.toolsUseLegacy.setChecked(PathPreferences.toolsUseLegacyTools())
self.form.toolsAbsolutePaths.setChecked(PathPreferences.toolsStoreAbsolutePaths())
def getPostProcessor(self, name):
if not name in self.processor.keys():
processor = PostProcessor.load(name)

View File

@@ -209,11 +209,7 @@ class SetupSheet:
for propName in op.properties():
prop = OpPropertyName(opName, propName)
if hasattr(self.obj, prop):
attr = getattr(self.obj, prop)
if hasattr(attr, 'UserString'):
settings[propName] = attr.UserString
else:
settings[propName] = attr
settings[propName] = PathUtil.getPropertyValueString(self.obj, prop)
attrs[opName] = settings
return attrs

View File

@@ -306,9 +306,9 @@ class GlobalEditor(object):
def getFields(self):
def updateExpression(name, widget):
value = str(widget.text())
val = PathGui.getProperty(self.obj, name)
val = PathUtil.getProperty(self.obj, name)
if val != value:
PathGui.setProperty(self.obj, name, value)
PathUtil.setProperty(self.obj, name, value)
updateExpression('StartDepthExpression', self.form.setupStartDepthExpr)
updateExpression('FinalDepthExpression', self.form.setupFinalDepthExpr)

View File

@@ -106,6 +106,10 @@ class PropertyQuantity(Property):
return Property.displayString(self)
return self.value.getUserPreferred()[0]
class PropertyAngle(PropertyQuantity):
def typeString(self):
return "Angle"
class PropertyDistance(PropertyQuantity):
def typeString(self):
return "Distance"
@@ -122,14 +126,23 @@ class PropertyFloat(Property):
def typeString(self):
return "Float"
def valueFromString(self, string):
return float(string)
class PropertyInteger(Property):
def typeString(self):
return "Integer"
def valueFromString(self, string):
return int(string)
class PropertyBool(Property):
def typeString(self):
return "Bool"
def valueFromString(self, string):
return bool(string)
class PropertyString(Property):
def typeString(self):
return "String"
@@ -137,24 +150,25 @@ class PropertyString(Property):
class OpPrototype(object):
PropertyType = {
'App::PropertyBool': PropertyBool,
'App::PropertyDistance': PropertyDistance,
'App::PropertyEnumeration': PropertyEnumeration,
'App::PropertyFloat': PropertyFloat,
'App::PropertyFloatConstraint': Property,
'App::PropertyFloatList': Property,
'App::PropertyInteger': PropertyInteger,
'App::PropertyIntegerList': PropertyInteger,
'App::PropertyLength': PropertyLength,
'App::PropertyLink': Property,
'App::PropertyLinkList': Property,
'App::PropertyLinkSubListGlobal': Property,
'App::PropertyPercent': PropertyPercent,
'App::PropertyString': PropertyString,
'App::PropertyStringList': Property,
'App::PropertyVectorDistance': Property,
'App::PropertyVectorList': Property,
'Part::PropertyPartShape': Property,
'App::PropertyAngle': PropertyAngle,
'App::PropertyBool': PropertyBool,
'App::PropertyDistance': PropertyDistance,
'App::PropertyEnumeration': PropertyEnumeration,
'App::PropertyFloat': PropertyFloat,
'App::PropertyFloatConstraint': Property,
'App::PropertyFloatList': Property,
'App::PropertyInteger': PropertyInteger,
'App::PropertyIntegerList': PropertyInteger,
'App::PropertyLength': PropertyLength,
'App::PropertyLink': Property,
'App::PropertyLinkList': Property,
'App::PropertyLinkSubListGlobal': Property,
'App::PropertyPercent': PropertyPercent,
'App::PropertyString': PropertyString,
'App::PropertyStringList': Property,
'App::PropertyVectorDistance': Property,
'App::PropertyVectorList': Property,
'Part::PropertyPartShape': Property,
}
def __init__(self, name):

View File

@@ -112,6 +112,21 @@ class _PropertyStringEditor(_PropertyEditor):
def setModelData(self, widget):
self.prop.setValue(widget.text())
class _PropertyAngleEditor(_PropertyEditor):
'''Editor for angle values - uses a line edit'''
def widget(self, parent):
return QtGui.QLineEdit(parent)
def setEditorData(self, widget):
quantity = self.prop.getValue()
if quantity is None:
quantity = FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle)
widget.setText(quantity.getUserPreferred()[0])
def setModelData(self, widget):
self.prop.setValue(FreeCAD.Units.Quantity(widget.text()))
class _PropertyLengthEditor(_PropertyEditor):
'''Editor for length values - uses a line edit.'''
@@ -174,15 +189,16 @@ class _PropertyFloatEditor(_PropertyEditor):
self.prop.setValue(widget.value())
_EditorFactory = {
PathSetupSheetOpPrototype.Property: None,
PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor,
PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor,
PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor,
PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor,
PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor,
PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor,
PathSetupSheetOpPrototype.Property: None,
PathSetupSheetOpPrototype.PropertyAngle: _PropertyAngleEditor,
PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor,
PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor,
PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor,
PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor,
PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor,
PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor,
PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor,
}
def Editor(prop):

View File

@@ -125,7 +125,7 @@ class PathSimulation:
# if hasattr(self.operation, "ToolController"):
# self.tool = self.operation.ToolController.Tool
if (self.tool is not None):
toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), self.tool.Diameter / 2.0)
toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), float(self.tool.Diameter) / 2.0)
self.cutTool.Shape = Part.makeSolid(toolProf.revolve(Vector(0, 0, 0), Vector(0, 0, 1)))
self.cutTool.ViewObject.show()
self.voxSim.SetCurrentTool(self.tool)
@@ -298,7 +298,7 @@ class PathSimulation:
# except:
# return (None, e1.valueAt(e1.LastParameter))
# height = self.height
# rad = tool.Diameter / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug
# rad = float(tool.Diameter) / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug
# if type(e1.Curve) is Part.Circle and e1.Curve.Radius <= rad: # hack to overcome occ bug
# rad = e1.Curve.Radius - 0.001
# # return (None, e1.valueAt(e1.LastParameter))
@@ -350,7 +350,7 @@ class PathSimulation:
# height = self.height
# hack to overcome occ bugs
rad = tool.Diameter / 2.0 - 0.001 * pos[2]
rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2]
# rad = rad + 0.001 * self.icmd
if type(toolPath.Curve) is Part.Circle and toolPath.Curve.Radius <= rad:
rad = toolPath.Curve.Radius - 0.01 * (pos[2] + 1)
@@ -386,7 +386,7 @@ class PathSimulation:
# create radial profile of the tool (90 degrees to the direction of the path)
def CreateToolProfile(self, tool, dir, pos, rad):
type = tool.ToolType
# rad = tool.Diameter / 2.0 - 0.001 * pos[2] # hack to overcome occ bug
# rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2] # hack to overcome occ bug
xf = dir[0] * rad
yf = dir[1] * rad
xp = pos[0]

View File

@@ -1792,10 +1792,11 @@ class ObjectSurface(PathOp.ObjectOp):
def setOclCutter(self, obj):
# Set cutter details
# https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
diam_1 = obj.ToolController.Tool.Diameter
lenOfst = obj.ToolController.Tool.LengthOffset
FR = obj.ToolController.Tool.FlatRadius
CEH = obj.ToolController.Tool.CuttingEdgeHeight
diam_1 = float(obj.ToolController.Tool.Diameter)
lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
if obj.ToolController.Tool.ToolType == 'EndMill':
# Standard End Mill
@@ -1817,13 +1818,13 @@ class ObjectSurface(PathOp.ObjectOp):
# Bull Nose or Corner Radius cutter
# Reference: https://www.fine-tools.com/halbstabfraeser.html
# OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst)
self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
elif obj.ToolController.Tool.ToolType == 'ChamferMill':
# Bull Nose or Corner Radius cutter
# Reference: https://www.fine-tools.com/halbstabfraeser.html
# OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst)
self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
else:
# Default to standard end mill
self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst))

View File

@@ -0,0 +1,383 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype
import PathScripts.PathUtil as PathUtil
import PySide
import Sketcher
import json
import math
import os
import zipfile
__title__ = "Tool bits."
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Class to deal with and represent a tool bit."
#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
#PathLog.trackModule()
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
ParameterTypeConstraint = {
'Angle': 'App::PropertyAngle',
'Distance': 'App::PropertyLength',
'DistanceX': 'App::PropertyLength',
'DistanceY': 'App::PropertyLength',
'Radius': 'App::PropertyLength'
}
def _findTool(path, typ, dbg=False):
if os.path.exists(path):
if dbg:
PathLog.debug("Found {} at {}".format(typ, path))
return path
def searchFor(pname, fname):
if dbg:
PathLog.debug("Looking for {}".format(pname))
if fname:
for p in PathPreferences.searchPathsTool(typ):
f = os.path.join(p, fname)
if dbg:
PathLog.debug(" Checking {}".format(f))
if os.path.exists(f):
if dbg:
PathLog.debug(" Found {} at {}".format(typ, f))
return f
if pname and os.path.sep != pname:
ppname, pfname = os.path.split(pname)
ffname = os.path.join(pfname, fname) if fname else pfname
return searchFor(ppname, ffname)
return None
return searchFor(path, '')
def findShape(path):
'''findShape(path) ... search for path, full and partially in all known shape directories.'''
return _findTool(path, 'Shape')
def findBit(path):
if path.endswith('.fctb'):
return _findTool(path, 'Bit')
return _findTool("{}.fctb".format(path), 'Bit')
def findLibrary(path, dbg=False):
if path.endswith('.fctl'):
return _findTool(path, 'Library', dbg)
return _findTool("{}.fctl".format(path), 'Library', dbg)
def _findRelativePath(path, typ):
relative = path
for p in PathPreferences.searchPathsTool(typ):
if path.startswith(p):
p = path[len(p):]
if os.path.sep == p[0]:
p = p[1:]
if len(p) < len(relative):
relative = p
return relative
def findRelativePathShape(path):
return _findRelativePath(path, 'Shape')
def findRelativePathTool(path):
return _findRelativePath(path, 'Bit')
def findRelativePathLibrary(path):
return _findRelativePath(path, 'Library')
def updateConstraint(sketch, name, value):
for i, constraint in enumerate(sketch.Constraints):
if constraint.Name.split(';')[0] == name:
constr = None
if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius', 'Angle']:
constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value)
else:
print(constraint.Name, constraint.Type)
if constr is not None:
if not PathGeom.isRoughly(constraint.Value, value.Value):
PathLog.track(name, constraint.Type, 'update', i, "(%.2f -> %.2f)" % (constraint.Value, value.Value))
sketch.delConstraint(i)
sketch.recompute()
n = sketch.addConstraint(constr)
sketch.renameConstraint(n, constraint.Name)
else:
PathLog.track(name, constraint.Type, 'unchanged')
break
PropertyGroupBit = 'Bit'
PropertyGroupAttribute = 'Attribute'
class ToolBit(object):
def __init__(self, obj, shapeFile):
PathLog.track(obj.Label, shapeFile)
self.obj = obj
obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape'))
obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit'))
obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool'))
if shapeFile is None:
obj.BitShape = 'endmill.fcstd'
self._setupBitShape(obj)
self.unloadBitBody(obj)
else:
obj.BitShape = shapeFile
self._setupBitShape(obj)
self.onDocumentRestored(obj)
def __getstate__(self):
return None
def __setstate__(self, state):
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, 'Proxy') and obj.Proxy == self:
self.obj = obj
break
return None
def propertyNamesBit(self, obj):
return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit]
def propertyNamesAttribute(self, obj):
return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute]
def onDocumentRestored(self, obj):
obj.setEditorMode('BitShape', 1)
obj.setEditorMode('BitBody', 2)
obj.setEditorMode('File', 1)
obj.setEditorMode('Shape', 2)
for prop in self.propertyNamesBit(obj):
obj.setEditorMode(prop, 1)
# I currently don't see why these need to be read-only
#for prop in self.propertyNamesAttribute(obj):
# obj.setEditorMode(prop, 1)
def onChanged(self, obj, prop):
PathLog.track(obj.Label, prop)
if prop == 'BitShape' and not 'Restore' in obj.State:
self._setupBitShape(obj)
#elif obj.getGroupOfProperty(prop) == PropertyGroupBit:
# self._updateBitShape(obj, [prop])
def onDelete(self, obj, arg2=None):
PathLog.track(obj.Label)
self.unloadBitBody(obj)
def _updateBitShape(self, obj, properties=None):
if not obj.BitBody is None:
if not properties:
properties = self.propertyNamesBit(obj)
for prop in properties:
for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
PathLog.track(obj.Label, sketch.Label, prop)
updateConstraint(sketch, prop, obj.getPropertyByName(prop))
self._copyBitShape(obj)
def _copyBitShape(self, obj):
obj.Document.recompute()
if obj.BitBody and obj.BitBody.Shape:
obj.Shape = obj.BitBody.Shape
else:
obj.Shape = Part.Shape()
def _loadBitBody(self, obj, path=None):
p = path if path else obj.BitShape
docOpened = False
doc = None
for d in FreeCAD.listDocuments():
if FreeCAD.getDocument(d).FileName == p:
doc = FreeCAD.getDocument(d)
break
if doc is None:
p = findShape(p)
if not path and p != obj.BitShape:
obj.BitShape = p
doc = FreeCAD.open(p)
docOpened = True
return (doc, docOpened)
def _removeBitBody(self, obj):
if obj.BitBody:
obj.BitBody.removeObjectsFromDocument()
obj.Document.removeObject(obj.BitBody.Name)
obj.BitBody = None
def _deleteBitSetup(self, obj):
PathLog.track(obj.Label)
self._removeBitBody(obj)
self._copyBitShape(obj)
for prop in self.propertyNamesBit(obj):
obj.removeProperty(prop)
def loadBitBody(self, obj, force=False):
if force or not obj.BitBody:
activeDoc = FreeCAD.ActiveDocument
if force:
self._removeBitBody(obj)
(doc, opened) = self._loadBitBody(obj)
obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True)
if opened:
FreeCAD.setActiveDocument(activeDoc.Name)
FreeCAD.closeDocument(doc.Name)
self._updateBitShape(obj)
def unloadBitBody(self, obj):
self._removeBitBody(obj)
def _setupBitShape(self, obj, path=None):
activeDoc = FreeCAD.ActiveDocument
(doc, docOpened) = self._loadBitBody(obj, path)
obj.Label = doc.RootObjects[0].Label
self._deleteBitSetup(obj)
obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True)
if docOpened:
FreeCAD.setActiveDocument(activeDoc.Name)
FreeCAD.closeDocument(doc.Name)
if obj.BitBody.ViewObject:
obj.BitBody.ViewObject.Visibility = False
self._copyBitShape(obj)
for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
for constraint in [c for c in sketch.Constraints if c.Name != '']:
typ = ParameterTypeConstraint.get(constraint.Type)
PathLog.track(constraint, typ)
if typ is not None:
parts = [p.strip() for p in constraint.Name.split(';')]
prop = parts[0]
desc = ''
if len(parts) > 1:
desc = parts[1]
obj.addProperty(typ, prop, PropertyGroupBit, desc)
obj.setEditorMode(prop, 1)
value = constraint.Value
if constraint.Type == 'Angle':
value = value * 180 / math.pi
PathUtil.setProperty(obj, prop, value)
def getBitThumbnail(self, obj):
if obj.BitShape:
path = findShape(obj.BitShape)
if path:
with open(path, 'rb') as fd:
zf = zipfile.ZipFile(fd)
pf = zf.open('thumbnails/Thumbnail.png', 'r')
data = pf.read()
pf.close()
return data
return None
def saveToFile(self, obj, path, setFile=True):
try:
with open(path, 'w') as fp:
json.dump(self.shapeAttrs(obj), fp, indent=' ')
if setFile:
obj.File = path
return True
except (OSError, IOError) as e:
PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e))
raise
def shapeAttrs(self, obj):
attrs = {}
attrs['version'] = 2 # Path.Tool is version 1
attrs['name'] = obj.Label
if PathPreferences.toolsStoreAbsolutePaths():
attrs['shape'] = obj.BitShape
else:
attrs['shape'] = findRelativePathShape(obj.BitShape)
params = {}
for name in self.propertyNamesBit(obj):
params[name] = PathUtil.getPropertyValueString(obj, name)
attrs['parameter'] = params
params = {}
for name in self.propertyNamesAttribute(obj):
params[name] = PathUtil.getPropertyValueString(obj, name)
attrs['attribute'] = params
return attrs
def Declaration(path):
with open(path, 'r') as fp:
return json.load(fp)
class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype):
def __init__(self):
PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute')
self.addProperty('App::PropertyEnumeration', 'Material', PropertyGroupAttribute, translate('PathToolBit', 'Tool bit material'))
self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond', 'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon']
self.addProperty('App::PropertyDistance', 'LengthOffset', PropertyGroupAttribute, translate('PathToolBit', 'Length offset in Z direction'))
self.addProperty('App::PropertyInteger', 'Flutes', PropertyGroupAttribute, translate('PathToolBit', 'The number of flutes'))
self.addProperty('App::PropertyDistance', 'ChipLoad', PropertyGroupAttribute, translate('PathToolBit', 'Chipload as per manufacturer'))
class ToolBitFactory(object):
def CreateFromAttrs(self, attrs, name='ToolBit'):
# pylint: disable=protected-access
obj = Factory.Create(name, attrs['shape'])
obj.Label = attrs['name']
params = attrs['parameter']
for prop in params:
PathUtil.setProperty(obj, prop, params[prop])
obj.Proxy._updateBitShape(obj)
obj.Proxy.unloadBitBody(obj)
params = attrs['attribute']
proto = AttributePrototype()
for pname in params:
prop = proto.getProperty(pname)
val = prop.valueFromString(params[pname])
print("prop[%s] = %s (%s)" % (pname, params[pname], type(val)))
prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname]))
return obj
def CreateFrom(self, path, name='ToolBit'):
try:
data = Declaration(path)
bit = Factory.CreateFromAttrs(data, name)
bit.File = path
return bit
except (OSError, IOError) as e:
PathLog.error("%s not a valid tool file (%s)" % (path, e))
raise
def Create(self, name='ToolBit', shapeFile=None):
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name)
obj.Proxy = ToolBit(obj, shapeFile)
return obj
Factory = ToolBitFactory()

View File

@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 FreeCADGui
import PathScripts
import os
from PySide import QtCore
class CommandToolBitCreate:
'''
Command used to create a new Tool.
'''
def __init__(self):
pass
def GetResources(self):
return {'Pixmap': 'Path-ToolBit',
'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Create Tool"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Creates a new ToolBit object")}
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
obj = PathScripts.PathToolBit.Factory.Create()
obj.ViewObject.Proxy.setCreate(obj.ViewObject)
class CommandToolBitSave:
'''
Command used to save an existing Tool to a file.
'''
def __init__(self, saveAs):
self.saveAs = saveAs
def GetResources(self):
if self.saveAs:
menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool as...")
else:
menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool")
return {'Pixmap': 'Path-ToolBit',
'MenuText': menuTxt,
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save an existing ToolBit object to a file")}
def selectedTool(self):
sel = FreeCADGui.Selection.getSelectionEx()
if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit):
return sel[0].Object
return None
def IsActive(self):
tool = self.selectedTool()
if tool:
if tool.File:
return True
return self.saveAs
return False
def Activated(self):
from PySide import QtGui
tool = self.selectedTool()
if tool:
path = None
if not tool.File or self.saveAs:
if tool.File:
fname = tool.File
else:
fname = os.path.join(PathScripts.PathPreferences.lastPathToolBit(), tool.Label + '.fctb')
foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Tool", fname, "*.fctb")
if foo:
path = foo[0]
else:
path = tool.File
if path:
if not path.endswith('.fctb'):
path += '.fctb'
tool.Proxy.saveToFile(tool, path)
PathScripts.PathPreferences.setLastPathToolBit(os.path.dirname(path))
class CommandToolBitLoad:
'''
Command used to load an existing Tool from a file into the current document.
'''
def __init__(self):
pass
def GetResources(self):
return {'Pixmap': 'Path-ToolBit',
'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load Tool"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load an existing ToolBit object from a file")}
def selectedTool(self):
sel = FreeCADGui.Selection.getSelectionEx()
if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit):
return sel[0].Object
return None
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
if PathScripts.PathToolBitGui.LoadTools():
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate())
FreeCADGui.addCommand('Path_ToolBitLoad', CommandToolBitLoad())
FreeCADGui.addCommand('Path_ToolBitSave', CommandToolBitSave(False))
FreeCADGui.addCommand('Path_ToolBitSaveAs', CommandToolBitSave(True))
CommandList = ['Path_ToolBitCreate', 'Path_ToolBitLoad', 'Path_ToolBitSave', 'Path_ToolBitSaveAs']
FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n")

View File

@@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 FreeCADGui
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathSetupSheetGui as PathSetupSheetGui
import PathScripts.PathToolBit as PathToolBit
import os
import re
from PySide import QtCore, QtGui
#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class ToolBitEditor(object):
'''UI and controller for editing a ToolBit.
The controller embeds the UI to the parentWidget which has to have a layout attached to it.
'''
def __init__(self, tool, parentWidget=None):
self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitEditor.ui")
if parentWidget:
self.form.setParent(parentWidget)
parentWidget.layout().addWidget(self.form)
self.tool = tool
if not tool.BitShape:
self.tool.BitShape = 'endmill.fcstd'
self.tool.Proxy.loadBitBody(self.tool)
self.setupTool(self.tool)
self.setupAttributes(self.tool)
def setupTool(self, tool):
layout = self.form.bitParams.layout()
for i in range(layout.rowCount() - 1, -1, -1):
layout.removeRow(i)
editor = {}
ui = FreeCADGui.UiLoader()
for name in tool.PropertiesList:
if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit:
qsb = ui.createWidget('Gui::QuantitySpinBox')
editor[name] = PathGui.QuantitySpinBox(qsb, tool, name)
label = QtGui.QLabel(re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z]+)', r' \1', name)))
#if parameter.get('Desc'):
# qsb.setToolTip(parameter['Desc'])
layout.addRow(label, qsb)
self.bitEditor = editor
img = tool.Proxy.getBitThumbnail(tool)
if img:
self.form.image.setPixmap(QtGui.QPixmap(QtGui.QImage.fromData(img)))
else:
self.form.image.setPixmap(QtGui.QPixmap())
def setupAttributes(self, tool):
self.proto = PathToolBit.AttributePrototype()
self.props = sorted(self.proto.properties)
self.delegate = PathSetupSheetGui.Delegate(self.form)
self.model = QtGui.QStandardItemModel(len(self.props), 3, self.form)
self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value'])
for i, name in enumerate(self.props):
prop = self.proto.getProperty(name)
isset = hasattr(tool, name)
if isset:
prop.setValue(getattr(tool, name))
self.model.setData(self.model.index(i, 0), isset, QtCore.Qt.EditRole)
self.model.setData(self.model.index(i, 1), name, QtCore.Qt.EditRole)
self.model.setData(self.model.index(i, 2), prop, PathSetupSheetGui.Delegate.PropertyRole)
self.model.setData(self.model.index(i, 2), prop.displayString(), QtCore.Qt.DisplayRole)
self.model.item(i, 0).setCheckable(True)
self.model.item(i, 0).setText('')
self.model.item(i, 1).setEditable(False)
self.model.item(i, 1).setToolTip(prop.info)
self.model.item(i, 2).setToolTip(prop.info)
if isset:
self.model.item(i, 0).setCheckState(QtCore.Qt.Checked)
else:
self.model.item(i, 0).setCheckState(QtCore.Qt.Unchecked)
self.model.item(i, 1).setEnabled(False)
self.model.item(i, 2).setEnabled(False)
self.form.attrTable.setModel(self.model)
self.form.attrTable.setItemDelegateForColumn(2, self.delegate)
self.form.attrTable.resizeColumnsToContents()
self.form.attrTable.verticalHeader().hide()
self.model.dataChanged.connect(self.updateData)
def updateData(self, topLeft, bottomRight):
# pylint: disable=unused-argument
if 0 == topLeft.column():
isset = self.model.item(topLeft.row(), 0).checkState() == QtCore.Qt.Checked
self.model.item(topLeft.row(), 1).setEnabled(isset)
self.model.item(topLeft.row(), 2).setEnabled(isset)
def accept(self):
self.refresh()
self.tool.Proxy.unloadBitBody(self.tool)
# get the attributes
for i, name in enumerate(self.props):
prop = self.proto.getProperty(name)
enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked
if enabled and not prop.getValue() is None:
prop.setupProperty(self.tool, name, PathToolBit.PropertyGroupAttribute, prop.getValue())
elif hasattr(self.tool, name):
self.tool.removeProperty(name)
def reject(self):
self.tool.Proxy.unloadBitBody(self.tool)
def updateUI(self):
PathLog.track()
self.form.toolName.setText(self.tool.Label)
self.form.shapePath.setText(self.tool.BitShape)
for editor in self.bitEditor:
self.bitEditor[editor].updateSpinBox()
def updateShape(self):
self.tool.BitShape = str(self.form.shapePath.text())
self.setupTool(self.tool)
self.form.toolName.setText(self.tool.Label)
for editor in self.bitEditor:
self.bitEditor[editor].updateSpinBox()
def updateTool(self):
# pylint: disable=protected-access
PathLog.track()
self.tool.Label = str(self.form.toolName.text())
self.tool.BitShape = str(self.form.shapePath.text())
for editor in self.bitEditor:
self.bitEditor[editor].updateProperty()
self.tool.Proxy._updateBitShape(self.tool)
def refresh(self):
PathLog.track()
self.form.blockSignals(True)
self.updateTool()
self.updateUI()
self.form.blockSignals(False)
def selectShape(self):
path = self.tool.BitShape
if not path:
path = PathPreferences.lastPathToolShape()
foo = QtGui.QFileDialog.getOpenFileName(self.form,
"Path - Tool Shape",
path,
"*.fcstd")
if foo and foo[0]:
PathPreferences.setLastPathToolShape(os.path.dirname(foo[0]))
self.form.shapePath.setText(foo[0])
self.updateShape()
def setupUI(self):
PathLog.track()
self.updateUI()
self.form.toolName.editingFinished.connect(self.refresh)
self.form.shapePath.editingFinished.connect(self.updateShape)
self.form.shapeSet.clicked.connect(self.selectShape)

View File

@@ -0,0 +1,298 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 FreeCADGui
import PathScripts.PathIconViewProvider as PathIconViewProvider
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit
import PathScripts.PathToolBitEdit as PathToolBitEdit
import os
from PySide import QtCore, QtGui
__title__ = "Tool Bit UI"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Task panel editor for a ToolBit"
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
class ViewProvider(object):
'''ViewProvider for a ToolBit.
It's sole job is to provide an icon and invoke the TaskPanel on edit.'''
def __init__(self, vobj, name):
PathLog.track(name, vobj.Object)
self.panel = None
self.icon = name
self.obj = vobj.Object
self.vobj = vobj
vobj.Proxy = self
def attach(self, vobj):
PathLog.track(vobj.Object)
self.vobj = vobj
self.obj = vobj.Object
def getIcon(self):
png = self.obj.Proxy.getBitThumbnail(self.obj)
if png:
pixmap = QtGui.QPixmap()
pixmap.loadFromData(png, 'PNG')
return QtGui.QIcon(pixmap)
return ':/icons/Path-ToolBit.svg'
def __getstate__(self):
return None
def __setstate__(self, state):
# pylint: disable=unused-argument
return None
def onDelete(self, vobj, arg2=None):
PathLog.track(vobj.Object.Label)
vobj.Object.Proxy.onDelete(vobj.Object)
def getDisplayMode(self, mode):
# pylint: disable=unused-argument
return 'Default'
def _openTaskPanel(self, vobj, deleteOnReject):
PathLog.track()
self.panel = TaskPanel(vobj, deleteOnReject)
FreeCADGui.Control.closeDialog()
FreeCADGui.Control.showDialog(self.panel)
self.panel.setupUi()
def setCreate(self, vobj):
PathLog.track()
self._openTaskPanel(vobj, True)
def setEdit(self, vobj, mode=0):
# pylint: disable=unused-argument
self._openTaskPanel(vobj, False)
return True
def unsetEdit(self, vobj, mode):
# pylint: disable=unused-argument
FreeCADGui.Control.closeDialog()
self.panel = None
return
def claimChildren(self):
if self.obj.BitBody:
return [self.obj.BitBody]
return []
def doubleClicked(self, vobj):
self.setEdit(vobj)
class TaskPanel:
'''TaskPanel for the SetupSheet - if it is being edited directly.'''
def __init__(self, vobj, deleteOnReject):
PathLog.track(vobj.Object.Label)
self.vobj = vobj
self.obj = vobj.Object
self.editor = PathToolBitEdit.ToolBitEditor(self.obj)
self.form = self.editor.form
self.deleteOnReject = deleteOnReject
FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Edit ToolBit'))
def reject(self):
FreeCAD.ActiveDocument.abortTransaction()
self.editor.reject()
FreeCADGui.Control.closeDialog()
if self.deleteOnReject:
FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit'))
self.editor.reject()
FreeCAD.ActiveDocument.removeObject(self.obj.Name)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def accept(self):
self.editor.accept()
FreeCAD.ActiveDocument.commitTransaction()
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute()
def updateUI(self):
self.editor.updateUI()
def updateModel(self):
self.editor.updateTool()
FreeCAD.ActiveDocument.recompute()
def setupUi(self):
self.editor.setupUI()
class ToolBitSelector(object):
ToolRole = QtCore.Qt.UserRole + 1
def __init__(self):
self.buttons = None
self.editor = None
self.dialog = None
self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui')
self.setupUI()
def updateTools(self, selected=None):
PathLog.track()
selItem = None
self.form.tools.setUpdatesEnabled(False)
if selected is None and self.form.tools.currentItem():
selected = self.form.tools.currentItem().text()
self.form.tools.clear()
for tool in sorted(self.loadedTools(), key=lambda t: t.Label):
icon = None
if tool.ViewObject and tool.ViewObject.Proxy:
icon = tool.ViewObject.Proxy.getIcon()
if icon and isinstance(icon, QtGui.QIcon):
item = QtGui.QListWidgetItem(icon, tool.Label)
else:
item = QtGui.QListWidgetItem(tool.Label)
item.setData(self.ToolRole, tool)
if selected == tool.Label:
selItem = item
self.form.tools.addItem(item)
if selItem:
self.form.tools.setCurrentItem(selItem)
self.updateSelection()
self.form.tools.setUpdatesEnabled(True)
def getTool(self):
PathLog.track()
self.updateTools()
res = self.form.exec_()
if 1 == res and self.form.tools.currentItem():
return self.form.tools.currentItem().data(self.ToolRole)
return None
def loadedTools(self):
PathLog.track()
if FreeCAD.ActiveDocument:
return [o for o in FreeCAD.ActiveDocument.Objects if hasattr(o, 'Proxy') and isinstance(o.Proxy, PathToolBit.ToolBit)]
return []
def loadTool(self):
PathLog.track()
tool = LoadTool(self.form)
if tool:
self.updateTools(tool.Label)
def createTool(self):
PathLog.track()
tool = PathToolBit.Factory.Create()
def accept():
self.editor.accept()
self.dialog.done(1)
self.updateTools(tool.Label)
def reject():
FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit'))
self.editor.reject()
self.dialog.done(0)
FreeCAD.ActiveDocument.removeObject(tool.Name)
FreeCAD.ActiveDocument.commitTransaction()
self.dialog = QtGui.QDialog(self.form)
layout = QtGui.QVBoxLayout(self.dialog)
self.editor = PathToolBitEdit.ToolBitEditor(tool, self.dialog)
self.editor.setupUI()
self.buttons = QtGui.QDialogButtonBox(
QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel,
QtCore.Qt.Horizontal, self.dialog)
layout.addWidget(self.buttons)
self.buttons.accepted.connect(accept)
self.buttons.rejected.connect(reject)
print(self.dialog.exec_())
def updateSelection(self):
PathLog.track()
if self.form.tools.selectedItems():
self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(True)
else:
self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False)
def setupUI(self):
PathLog.track()
self.form.toolCreate.clicked.connect(self.createTool)
self.form.toolLoad.clicked.connect(self.loadTool)
self.form.tools.itemSelectionChanged.connect(self.updateSelection)
self.form.tools.doubleClicked.connect(self.form.accept)
class ToolBitGuiFactory(PathToolBit.ToolBitFactory):
def Create(self, name='ToolBit', shapeFile=None):
'''Create(name = 'ToolBit') ... creates a new tool bit.
It is assumed the tool will be edited immediately so the internal bit body is still attached.'''
FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit'))
tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile)
PathIconViewProvider.Attach(tool.ViewObject, name)
FreeCAD.ActiveDocument.commitTransaction()
return tool
def GetToolFile(parent = None):
if parent is None:
parent = QtGui.QApplication.activeWindow()
foo = QtGui.QFileDialog.getOpenFileName(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb')
if foo and foo[0]:
PathPreferences.setLastPathToolBit(os.path.dirname(foo[0]))
return foo[0]
return None
def GetToolFiles(parent = None):
if parent is None:
parent = QtGui.QApplication.activeWindow()
foo = QtGui.QFileDialog.getOpenFileNames(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb')
if foo and foo[0]:
PathPreferences.setLastPathToolBit(os.path.dirname(foo[0][0]))
return foo[0]
return []
def LoadTool(parent = None):
'''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.'''
foo = GetToolFile(parent)
return PathToolBit.Factory.CreateFrom(foo) if foo else foo
def LoadTools(parent = None):
'''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.'''
return [PathToolBit.Factory.CreateFrom(foo) for foo in GetToolFiles(parent)]
# Set the factory so all tools are created with UI
PathToolBit.Factory = ToolBitGuiFactory()
PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider)

View File

@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 FreeCADGui
import PySide.QtCore as QtCore
class CommandToolBitLibraryOpen:
'''
Command to ToolBitLibrary editor.
'''
def __init__(self):
pass
def GetResources(self):
return {'Pixmap': 'Path-ToolTable',
'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open ToolBit Library editor"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open an editor to manage ToolBit libraries")}
def IsActive(self):
return True
def Activated(self):
import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui
library = PathToolBitLibraryGui.ToolBitLibrary()
library.open()
class CommandToolBitLibraryLoad:
'''
Command used to load an entire ToolBitLibrary (or part of it) from a file into a job.
'''
def __init__(self):
pass
def GetResources(self):
return {'Pixmap': 'Path-ToolTable',
'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load ToolBit Library"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load an entire ToolBit library or part of it into a job")}
def selectedJob(self):
if FreeCAD.ActiveDocument:
sel = FreeCADGui.Selection.getSelectionEx()
if sel and sel[0].Object.Name[:3] == 'Job':
return sel[0].Object
jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job']
if 1 == len(jobs):
return jobs[0]
return None
def IsActive(self):
return not self.selectedJob() is None
def Activated(self):
job = self.selectedJob()
self.Execute(job)
@classmethod
def Execute(cls, job):
import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui
import PathScripts.PathToolControllerGui as PathToolControllerGui
library = PathToolBitLibraryGui.ToolBitLibrary()
if 1 == library.open(dialog=True) and job:
for nr, tool in library.selectedOrAllTools():
tc = PathToolControllerGui.Create("TC: {}".format(tool.Label), tool, nr)
job.Proxy.addToolController(tc)
FreeCAD.ActiveDocument.recompute()
return True
return False
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Path_ToolBitLibraryOpen', CommandToolBitLibraryOpen())
FreeCADGui.addCommand('Path_ToolBitLibraryLoad', CommandToolBitLibraryLoad())
CommandList = ['Path_ToolBitLibraryOpen', 'Path_ToolBitLibraryLoad']
FreeCAD.Console.PrintLog("Loading PathToolBitLibraryCmd... done\n")

View File

@@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 FreeCADGui
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit
import PathScripts.PathToolBitGui as PathToolBitGui
import PySide
import json
import os
import traceback
import uuid as UUID
#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
_UuidRole = PySide.QtCore.Qt.UserRole + 1
_PathRole = PySide.QtCore.Qt.UserRole + 2
class _TableView(PySide.QtGui.QTableView):
'''Subclass of QTableView to support rearrange and copying of ToolBits'''
def __init__(self, parent):
PySide.QtGui.QTableView.__init__(self, parent)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(PySide.QtGui.QAbstractItemView.InternalMove)
self.setDefaultDropAction(PySide.QtCore.Qt.MoveAction)
self.setSortingEnabled(True)
self.setSelectionBehavior(PySide.QtGui.QAbstractItemView.SelectRows)
self.verticalHeader().hide()
def supportedDropActions(self):
return [PySide.QtCore.Qt.CopyAction, PySide.QtCore.Qt.MoveAction]
def _uuidOfRow(self, row):
model = self.model()
return model.data(model.index(row, 0), _UuidRole)
def _rowWithUuid(self, uuid):
model = self.model()
for row in range(model.rowCount()):
if self._uuidOfRow(row) == uuid:
return row
return None
def _copyTool(self, uuid_, dstRow):
model = self.model()
model.insertRow(dstRow)
srcRow = self._rowWithUuid(uuid_)
for col in range(model.columnCount()):
srcItem = model.item(srcRow, col)
model.setData(model.index(dstRow, col), srcItem.data(PySide.QtCore.Qt.EditRole), PySide.QtCore.Qt.EditRole)
if col == 0:
model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole)
# Even a clone of a tool gets its own uuid so it can be identified when
# rearranging the order or inserting/deleting rows
model.setData(model.index(dstRow, col), UUID.uuid4(), _UuidRole)
else:
model.item(dstRow, col).setEditable(False)
def _copyTools(self, uuids, dst):
for i, uuid in enumerate(uuids):
self._copyTool(uuid, dst + i)
def dropEvent(self, event):
PathLog.track()
mime = event.mimeData()
data = mime.data('application/x-qstandarditemmodeldatalist')
stream = PySide.QtCore.QDataStream(data)
srcRows = []
while not stream.atEnd():
# pylint: disable=unused-variable
row = stream.readInt32()
srcRows.append(row)
col = stream.readInt32()
#PathLog.track(row, col)
cnt = stream.readInt32()
for i in range(cnt):
key = stream.readInt32()
val = stream.readQVariant()
#PathLog.track(' ', i, key, val, type(val))
# I have no idea what these three integers are,
# or if they even are three integers,
# but it seems to work out this way.
i0 = stream.readInt32()
i1 = stream.readInt32()
i2 = stream.readInt32()
#PathLog.track(' ', i0, i1, i2)
# get the uuids of all srcRows
model = self.model()
srcUuids = [self._uuidOfRow(row) for row in set(srcRows)]
destRow = self.rowAt(event.pos().y())
self._copyTools(srcUuids, destRow)
if PySide.QtCore.Qt.DropAction.MoveAction == event.proposedAction():
for uuid in srcUuids:
model.removeRow(self._rowWithUuid(uuid))
class ToolBitLibrary(object):
'''ToolBitLibrary is the controller for displaying/selecting/creating/editing a collection of ToolBits.'''
def __init__(self, path=None):
self.path = path
self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui')
self.toolTableView = _TableView(self.form.toolTableGroup)
self.form.toolTableGroup.layout().replaceWidget(self.form.toolTable, self.toolTableView)
self.form.toolTable.hide()
self.setupUI()
self.title = self.form.windowTitle()
if path:
self.libraryLoad(path)
def _toolAdd(self, nr, tool, path):
toolNr = PySide.QtGui.QStandardItem()
toolNr.setData(nr, PySide.QtCore.Qt.EditRole)
toolNr.setData(path, _PathRole)
toolNr.setData(UUID.uuid4(), _UuidRole)
toolName = PySide.QtGui.QStandardItem()
toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole)
toolName.setEditable(False)
toolShape = PySide.QtGui.QStandardItem()
toolShape.setData(os.path.splitext(os.path.basename(tool['shape']))[0], PySide.QtCore.Qt.EditRole)
toolShape.setEditable(False)
toolDiameter = PySide.QtGui.QStandardItem()
toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole)
toolDiameter.setEditable(False)
self.model.appendRow([toolNr, toolName, toolShape, toolDiameter])
def toolAdd(self):
PathLog.track()
# pylint: disable=broad-except
try:
nr = 0
for row in range(self.model.rowCount()):
itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole))
nr = max(nr, itemNr)
nr += 1
for i, foo in enumerate(PathToolBitGui.GetToolFiles(self.form)):
tool = PathToolBit.Declaration(foo)
self._toolAdd(nr + i, tool, foo)
self.toolTableView.resizeColumnsToContents()
except Exception:
PathLog.error('something happened')
PathLog.error(traceback.print_exc())
def selectedOrAllTools(self):
selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()])
if not selectedRows:
selectedRows = list(range(self.model.rowCount()))
tools = []
for row in selectedRows:
item = self.model.item(row, 0)
toolNr = int(item.data(PySide.QtCore.Qt.EditRole))
toolPath = item.data(_PathRole)
tools.append((toolNr, PathToolBit.Factory.CreateFrom(toolPath)))
return tools
def toolDelete(self):
PathLog.track()
selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()])
for row in sorted(list(selectedRows), key = lambda r: -r):
self.model.removeRows(row, 1)
def toolEnumerate(self):
PathLog.track()
for row in range(self.model.rowCount()):
self.model.setData(self.model.index(row, 0), row + 1, PySide.QtCore.Qt.EditRole)
def toolSelect(self, selected, deselected):
# pylint: disable=unused-argument
self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0)
def open(self, path=None, dialog=False):
'''open(path=None, dialog=False) ... load library stored in path and bring up ui.
Returns 1 if user pressed OK, 0 otherwise.'''
if path:
fullPath = PathToolBit.findLibrary(path)
if fullPath:
self.libraryLoad(fullPath)
else:
self.libraryOpen()
elif dialog:
self.libraryOpen()
return self.form.exec_()
def updateToolbar(self):
if self.path:
self.form.librarySave.setEnabled(True)
else:
self.form.librarySave.setEnabled(False)
def libraryOpen(self):
PathLog.track()
foo = PySide.QtGui.QFileDialog.getOpenFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl')
if foo and foo[0]:
path = foo[0]
PathPreferences.setLastPathToolLibrary(os.path.dirname(path))
self.libraryLoad(path)
def libraryLoad(self, path):
self.toolTableView.setUpdatesEnabled(False)
self.model.clear()
self.model.setHorizontalHeaderLabels(self.columnNames())
if path:
with open(path) as fp:
library = json.load(fp)
for toolBit in library['tools']:
nr = toolBit['nr']
bit = PathToolBit.findBit(toolBit['path'])
if bit:
PathLog.track(bit)
tool = PathToolBit.Declaration(bit)
self._toolAdd(nr, tool, bit)
else:
PathLog.error("Could not find tool #{}: {}".format(nr, library['tools'][nr]))
self.toolTableView.resizeColumnsToContents()
self.toolTableView.setUpdatesEnabled(True)
self.form.setWindowTitle("{} - {}".format(self.title, os.path.basename(path) if path else ''))
self.path = path
self.updateToolbar()
def libraryNew(self):
self.libraryLoad(None)
def librarySave(self):
library = {}
tools = []
library['version'] = 1
library['tools'] = tools
for row in range(self.model.rowCount()):
toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole)
toolPath = self.model.data(self.model.index(row, 0), _PathRole)
if PathPreferences.toolsStoreAbsolutePaths():
tools.append({'nr': toolNr, 'path': toolPath})
else:
tools.append({'nr': toolNr, 'path': PathToolBit.findRelativePathTool(toolPath)})
with open(self.path, 'w') as fp:
json.dump(library, fp, sort_keys=True, indent=2)
def librarySaveAs(self):
foo = PySide.QtGui.QFileDialog.getSaveFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl')
if foo and foo[0]:
path = foo[0] if foo[0].endswith('.fctl') else "{}.fctl".format(foo[0])
PathPreferences.setLastPathToolLibrary(os.path.dirname(path))
self.path = path
self.librarySave()
self.updateToolbar()
def columnNames(self):
return ['Nr', 'Tool', 'Shape', 'Diameter']
def setupUI(self):
PathLog.track('+')
self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.toolTableView)
self.model.setHorizontalHeaderLabels(self.columnNames())
self.toolTableView.setModel(self.model)
self.toolTableView.resizeColumnsToContents()
self.toolTableView.selectionModel().selectionChanged.connect(self.toolSelect)
self.form.toolAdd.clicked.connect(self.toolAdd)
self.form.toolDelete.clicked.connect(self.toolDelete)
self.form.toolEnumerate.clicked.connect(self.toolEnumerate)
self.form.libraryNew.clicked.connect(self.libraryNew)
self.form.libraryOpen.clicked.connect(self.libraryOpen)
self.form.librarySave.clicked.connect(self.librarySave)
self.form.librarySaveAs.clicked.connect(self.librarySaveAs)
self.toolSelect([], [])
self.updateToolbar()
PathLog.track('-')

View File

@@ -26,16 +26,13 @@
import FreeCAD
import Path
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit
from PySide import QtCore
LOGLEVEL = False
if LOGLEVEL:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
#PathLog.trackModule(PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
@@ -61,54 +58,74 @@ class ToolControllerTemplate:
VertRapid = 'vrapid'
class ToolController:
def __init__(self, obj, tool=1):
PathLog.track('tool: {}'.format(tool))
obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The active tool"))
def __init__(self, obj, cTool=False):
PathLog.track('tool: {}'.format(cTool))
obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The active tool"))
obj.ToolNumber = (0, 0, 10000, 1)
obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool used by this controller"))
obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The speed of the cutting spindle in RPM"))
obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "Direction of spindle rotation"))
self.ensureUseLegacyTool(obj, cTool)
obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The speed of the cutting spindle in RPM"))
obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Direction of spindle rotation"))
obj.SpindleDir = ['Forward', 'Reverse']
obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for vertical moves in Z"))
obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for horizontal moves"))
obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for vertical moves in Z"))
obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for horizontal moves"))
obj.Proxy = self
obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for vertical moves in Z"))
obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for horizontal moves"))
obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for vertical moves in Z"))
obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for horizontal moves"))
obj.setEditorMode('Placement', 2)
def onDocumentRestored(self, obj):
obj.setEditorMode('Placement', 2)
def onDelete(self, obj, arg2=None):
# pylint: disable=unused-argument
if not self.usesLegacyTool(obj):
if len(obj.Tool.InList) == 1:
if hasattr(obj.Tool.Proxy, 'onDelete'):
obj.Tool.Proxy.onDelete(obj.Tool)
obj.Document.removeObject(obj.Tool.Name)
def setFromTemplate(self, obj, template):
'''setFromTemplate(obj, xmlItem) ... extract properties from xmlItem and assign to receiver.'''
PathLog.track(obj.Name, template)
if template.get(ToolControllerTemplate.Version) and 1 == int(template.get(ToolControllerTemplate.Version)):
if template.get(ToolControllerTemplate.Label):
obj.Label = template.get(ToolControllerTemplate.Label)
if template.get(ToolControllerTemplate.VertFeed):
obj.VertFeed = template.get(ToolControllerTemplate.VertFeed)
if template.get(ToolControllerTemplate.HorizFeed):
obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed)
if template.get(ToolControllerTemplate.VertRapid):
obj.VertRapid = template.get(ToolControllerTemplate.VertRapid)
if template.get(ToolControllerTemplate.HorizRapid):
obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid)
if template.get(ToolControllerTemplate.SpindleSpeed):
obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed))
if template.get(ToolControllerTemplate.SpindleDir):
obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir)
if template.get(ToolControllerTemplate.ToolNumber):
obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber))
if template.get(ToolControllerTemplate.Tool):
obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool))
if template.get(ToolControllerTemplate.Expressions):
for exprDef in template.get(ToolControllerTemplate.Expressions):
if exprDef[ToolControllerTemplate.ExprExpr]:
obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr])
version = 0
if template.get(ToolControllerTemplate.Version):
version = int(template.get(ToolControllerTemplate.Version))
if version == 1 or version == 2:
if template.get(ToolControllerTemplate.Label):
obj.Label = template.get(ToolControllerTemplate.Label)
if template.get(ToolControllerTemplate.VertFeed):
obj.VertFeed = template.get(ToolControllerTemplate.VertFeed)
if template.get(ToolControllerTemplate.HorizFeed):
obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed)
if template.get(ToolControllerTemplate.VertRapid):
obj.VertRapid = template.get(ToolControllerTemplate.VertRapid)
if template.get(ToolControllerTemplate.HorizRapid):
obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid)
if template.get(ToolControllerTemplate.SpindleSpeed):
obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed))
if template.get(ToolControllerTemplate.SpindleDir):
obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir)
if template.get(ToolControllerTemplate.ToolNumber):
obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber))
if template.get(ToolControllerTemplate.Tool):
toolVersion = template.get(ToolControllerTemplate.Tool).get(ToolControllerTemplate.Version)
if toolVersion == 1:
self.ensureUseLegacyTool(obj, True)
obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool))
else:
self.ensureUseLegacyTool(obj, False)
obj.Tool = PathToolBit.Factory.CreateFromAttrs(template.get(ToolControllerTemplate.Tool))
if obj.Tool and obj.Tool.ViewObject and obj.Tool.ViewObject.Visibility:
obj.Tool.ViewObject.Visibility = False
if template.get(ToolControllerTemplate.Expressions):
for exprDef in template.get(ToolControllerTemplate.Expressions):
if exprDef[ToolControllerTemplate.ExprExpr]:
obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr])
else:
PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version))
else:
PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version))
PathLog.error(translate('PathToolController', 'PathToolController template has no version - corrupted template file?'))
def templateAttrs(self, obj):
'''templateAttrs(obj) ... answer a dictionary with all properties that should be stored for a template.'''
@@ -123,7 +140,10 @@ class ToolController:
attrs[ToolControllerTemplate.HorizRapid] = ("%s" % (obj.HorizRapid))
attrs[ToolControllerTemplate.SpindleSpeed] = obj.SpindleSpeed
attrs[ToolControllerTemplate.SpindleDir] = obj.SpindleDir
attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs()
if self.usesLegacyTool(obj):
attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs()
else:
attrs[ToolControllerTemplate.Tool] = obj.Tool.Proxy.templateAttrs(obj.Tool)
expressions = []
for expr in obj.ExpressionEngine:
PathLog.debug('%s: %s' % (expr[0], expr[1]))
@@ -157,39 +177,61 @@ class ToolController:
PathLog.track()
return obj.Tool
def usesLegacyTool(self, obj):
'''returns True if the tool being controlled is a legacy tool'''
return isinstance(obj.Tool, Path.Tool)
def ensureUseLegacyTool(self, obj, legacy):
if not hasattr(obj, 'Tool') or (legacy != self.usesLegacyTool(obj)):
if legacy and hasattr(obj, 'Tool') and len(obj.Tool.InList) == 1:
if hasattr(obj.Tool.Proxy, 'onDelete'):
obj.Tool.Proxy.onDelete(obj.Tool)
obj.Document.removeObject(obj.Tool.Name)
if hasattr(obj, 'Tool'):
obj.removeProperty('Tool')
if legacy:
obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller"))
else:
obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller"))
def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True):
PathLog.track(tool, toolNumber)
legacyTool = PathPreferences.toolsReallyUseLegacyTools() if tool is None else isinstance(tool, Path.Tool)
PathLog.track(tool, toolNumber, legacyTool)
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Label = name
obj.Proxy = ToolController(obj, legacyTool)
ToolController(obj)
if FreeCAD.GuiUp and assignViewProvider:
ViewProvider(obj.ViewObject)
if tool is None:
tool = Path.Tool()
tool.Diameter = 5.0
tool.Name = "Default Tool"
tool.CuttingEdgeHeight = 15.0
tool.ToolType = "EndMill"
tool.Material = "HighSpeedSteel"
if legacyTool:
tool = Path.Tool()
tool.Diameter = 5.0
tool.Name = "Default Tool"
tool.CuttingEdgeHeight = 15.0
tool.ToolType = "EndMill"
tool.Material = "HighSpeedSteel"
else:
tool = PathToolBit.Factory.Create()
if tool.ViewObject:
tool.ViewObject.Visibility = False
obj.Tool = tool
obj.ToolNumber = toolNumber
return obj
def FromTemplate(template, assignViewProvider=True):
# pylint: disable=unused-argument
PathLog.track()
name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label)
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
tc = ToolController(obj)
if FreeCAD.GuiUp and assignViewProvider:
ViewProvider(obj.ViewObject)
tc.setFromTemplate(obj, template)
obj = Create(name, assignViewProvider=True)
obj.Proxy.setFromTemplate(obj, template)
return obj

View File

@@ -28,6 +28,7 @@ import Part
import PathScripts
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
import PathScripts.PathToolBitGui as PathToolBitGui
import PathScripts.PathToolEdit as PathToolEdit
import PathScripts.PathUtil as PathUtil
@@ -78,6 +79,7 @@ class ViewProvider:
def onDelete(self, vobj, args=None):
# pylint: disable=unused-argument
PathUtil.clearExpressionEngine(vobj.Object)
self.vobj.Object.Proxy.onDelete(vobj.Object, args)
return True
def updateData(self, vobj, prop):
@@ -113,11 +115,21 @@ class ViewProvider:
action.triggered.connect(self.setEdit)
menu.addAction(action)
def claimChildren(self):
obj = self.vobj.Object
if obj and obj.Proxy and not obj.Proxy.usesLegacyTool(obj):
return [obj.Tool]
return []
def Create(name = 'Default Tool', tool=None, toolNumber=1):
PathLog.track(tool, toolNumber)
obj = PathScripts.PathToolController.Create(name, tool, toolNumber)
ViewProvider(obj.ViewObject)
if not obj.Proxy.usesLegacyTool(obj):
# ToolBits are visible by default, which is typically not what the user wants
if tool and tool.ViewObject and tool.ViewObject.Visibility:
tool.ViewObject.Visibility = False
return obj
@@ -129,16 +141,35 @@ class CommandPathToolController(object):
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller to the Job"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller")}
def selectedJob(self):
if FreeCAD.ActiveDocument:
sel = FreeCADGui.Selection.getSelectionEx()
if sel and sel[0].Object.Name[:3] == 'Job':
return sel[0].Object
jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job']
if 1 == len(jobs):
return jobs[0]
return None
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
return self.selectedJob() is not None
def Activated(self):
PathLog.track()
Create()
job = self.selectedJob()
if job:
tool = PathToolBitGui.ToolBitSelector().getTool()
if tool:
toolNr = None
for tc in job.ToolController:
if tc.Tool == tool:
toolNr = tc.ToolNumber
break
if not toolNr:
toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1
tc = Create("TC: {}".format(tool.Label), tool, toolNr)
job.Proxy.addToolController(tc)
FreeCAD.ActiveDocument.recompute()
class ToolControllerEditor(object):
@@ -153,7 +184,12 @@ class ToolControllerEditor(object):
self.vertRapid = PathGui.QuantitySpinBox(self.form.vertRapid, obj, 'VertRapid')
self.horizRapid = PathGui.QuantitySpinBox(self.form.horizRapid, obj, 'HorizRapid')
self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor)
if obj.Proxy.usesLegacyTool(obj):
self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor)
else:
self.editor = None
self.form.toolBox.widget(1).hide()
self.form.toolBox.removeItem(1)
def updateUi(self):
tc = self.obj
@@ -168,7 +204,8 @@ class ToolControllerEditor(object):
if index >= 0:
self.form.spindleDirection.setCurrentIndex(index)
self.editor.updateUI()
if self.editor:
self.editor.updateUI()
def updateToolController(self):
tc = self.obj
@@ -182,8 +219,9 @@ class ToolControllerEditor(object):
tc.SpindleSpeed = self.form.spindleSpeed.value()
tc.SpindleDir = self.form.spindleDirection.currentText()
self.editor.updateTool()
tc.Tool = self.editor.tool
if self.editor:
self.editor.updateTool()
tc.Tool = self.editor.tool
except Exception as e: # pylint: disable=broad-except
PathLog.error(translate("PathToolController", "Error updating TC: %s") % e)
@@ -196,7 +234,8 @@ class ToolControllerEditor(object):
self.form.blockSignals(False)
def setupUi(self):
self.editor.setupUI()
if self.editor:
self.editor.setupUI()
self.form.tcName.editingFinished.connect(self.refresh)
self.form.horizFeed.editingFinished.connect(self.refresh)
@@ -219,13 +258,13 @@ class TaskPanel:
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
if self.toolrep is not None:
if self.toolrep:
FreeCAD.ActiveDocument.removeObject(self.toolrep.Name)
FreeCAD.ActiveDocument.recompute()
def reject(self):
FreeCADGui.Control.closeDialog()
if self.toolrep is not None:
if self.toolrep:
FreeCAD.ActiveDocument.removeObject(self.toolrep.Name)
FreeCAD.ActiveDocument.recompute()
@@ -236,11 +275,12 @@ class TaskPanel:
def setFields(self):
self.editor.updateUi()
tool = self.obj.Tool
radius = tool.Diameter / 2
length = tool.CuttingEdgeHeight
t = Part.makeCylinder(radius, length)
self.toolrep.Shape = t
if self.toolrep:
tool = self.obj.Tool
radius = float(tool.Diameter) / 2
length = tool.CuttingEdgeHeight
t = Part.makeCylinder(radius, length)
self.toolrep.Shape = t
def edit(self, item, column):
# pylint: disable=unused-argument
@@ -253,9 +293,10 @@ class TaskPanel:
FreeCAD.ActiveDocument.recompute()
def setupUi(self):
t = Part.makeCylinder(1, 1)
self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool")
self.toolrep.Shape = t
if self.editor.editor:
t = Part.makeCylinder(1, 1)
self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool")
self.toolrep.Shape = t
self.setFields()
self.editor.setupUi()

View File

@@ -29,6 +29,8 @@ import FreeCADGui
import Path
import PathScripts
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBitLibraryCmd as PathToolBitLibraryCmd
import PathScripts.PathToolEdit as PathToolEdit
import PathScripts.PathUtils as PathUtils
import PathScripts.PathToolLibraryManager as ToolLibraryManager
@@ -439,12 +441,14 @@ class CommandToolLibraryEdit():
pass
def edit(self, job=None, cb=None):
editor = EditorPanel(job, cb)
editor.setupUi()
r = editor.form.exec_()
if r:
pass
if PathPreferences.toolsReallyUseLegacyTools():
editor = EditorPanel(job, cb)
editor.setupUi()
editor.form.exec_()
else:
if PathToolBitLibraryCmd.CommandToolBitLibraryLoad.Execute(job):
if cb:
cb()
def GetResources(self):
return {'Pixmap' : 'Path-ToolTable',
@@ -456,7 +460,6 @@ class CommandToolLibraryEdit():
return not FreeCAD.ActiveDocument is None
def Activated(self):
self.edit()
if FreeCAD.GuiUp:

View File

@@ -34,14 +34,47 @@ other than PathLog, then it probably doesn't belong here.
import six
import PathScripts.PathLog as PathLog
import PySide
LOGLEVEL = False
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
if LOGLEVEL:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
def _getProperty(obj, prop):
o = obj
attr = obj
name = None
for name in prop.split('.'):
o = attr
if not hasattr(o, name):
break
attr = getattr(o, name)
if o == attr:
PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name))
return (None, None, None)
#PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr))
return(o, attr, name)
def getProperty(obj, prop):
'''getProperty(obj, prop) ... answer obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
return attr
def getPropertyValueString(obj, prop):
'''getPropertyValueString(obj, prop) ... answer a string represntation of an object's property's value.'''
attr = getProperty(obj, prop)
if hasattr(attr, 'UserString'):
return attr.UserString
return str(attr)
def setProperty(obj, prop, value):
'''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
if o and name:
setattr(o, name, value)
# NotValidBaseTypeIds = ['Sketcher::SketchObject']
NotValidBaseTypeIds = []
@@ -53,6 +86,9 @@ def isValidBaseObject(obj):
# Can't link to anything inside a geo feature group anymore
PathLog.debug("%s is inside a geo feature group" % obj.Label)
return False
if hasattr(obj, 'BitBody') and hasattr(obj, 'BitShape'):
# ToolBit's are not valid base objects
return False
if obj.TypeId in NotValidBaseTypeIds:
PathLog.debug("%s is blacklisted (%s)" % (obj.Label, obj.TypeId))
return False

View File

@@ -703,14 +703,14 @@ def guessDepths(objshape, subs=None):
def drillTipLength(tool):
"""returns the length of the drillbit tip."""
if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or tool.Diameter == 0.0:
if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or float(tool.Diameter) == 0.0:
return 0.0
else:
if tool.CuttingEdgeAngle <= 0 or tool.CuttingEdgeAngle >= 180:
PathLog.error(translate("Path", "Invalid Cutting Edge Angle %.2f, must be >0° and <=180°") % tool.CuttingEdgeAngle)
return 0.0
theta = math.radians(tool.CuttingEdgeAngle)
length = (tool.Diameter / 2) / math.tan(theta / 2)
length = (float(tool.Diameter) / 2) / math.tan(theta / 2)
if length < 0:
PathLog.error(translate("Path", "Cutting Edge Angle (%.2f) results in negative tool tip length") % tool.CuttingEdgeAngle)
return 0.0

View File

@@ -34,8 +34,10 @@ PathLog.trackModule(PathLog.thisModule())
class TestPathHelix(PathTestUtils.PathTestBase):
RotateBy = 45
def setUp(self):
self.clone = None
self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd')
self.job = PathJob.Create('Job', [self.doc.Body])
@@ -68,7 +70,7 @@ class TestPathHelix(PathTestUtils.PathTestBase):
proxy = op.Proxy
model = self.job.Model.Group[0]
for deg in range(5, 360, 5):
for deg in range(self.RotateBy, 360, self.RotateBy):
model.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0)
for base in op.Base:
model = base[0]
@@ -81,7 +83,7 @@ class TestPathHelix(PathTestUtils.PathTestBase):
def test03(self):
'''Verify Helix generates proper holes for rotated base model'''
for deg in range(5, 360, 5):
for deg in range(self.RotateBy, 360, self.RotateBy):
self.tearDown()
self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd')
self.doc.Body.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0)
@@ -102,7 +104,7 @@ class TestPathHelix(PathTestUtils.PathTestBase):
def test04(self):
'''Verify Helix generates proper holes for rotated clone base model'''
for deg in range(5, 360, 5):
for deg in range(self.RotateBy, 360, self.RotateBy):
self.tearDown()
self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd')
self.clone = Draft.clone(self.doc.Body)

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 PathScripts.PathPreferences as PathPreferences
import PathTests.PathTestUtils as PathTestUtils
class TestPathPreferences(PathTestUtils.PathTestBase):
def test00(self):
'''There is at least one search path.'''
paths = PathPreferences.searchPaths()
self.assertGreater(len(paths), 0)
def test01(self):
'''PathScripts is part of the posts search path.'''
paths = PathPreferences.searchPathsPost()
self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/')]), 1)
def test02(self):
'''PathScripts/post is part of the posts search path.'''
paths = PathPreferences.searchPathsPost()
self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/post/')]), 1)
def test03(self):
'''Available post processors include linuxcnc, grbl and opensbp.'''
posts = PathPreferences.allAvailablePostProcessors()
self.assertTrue('linuxcnc' in posts)
self.assertTrue('grbl' in posts)
self.assertTrue('opensbp' in posts)
def test10(self):
'''Default paths for tools are resolved correctly'''
self.assertTrue(PathPreferences.pathDefaultToolsPath().endswith('/Path/Tools/'))
self.assertTrue(PathPreferences.pathDefaultToolsPath('Bit').endswith('/Path/Tools/Bit'))
self.assertTrue(PathPreferences.pathDefaultToolsPath('Library').endswith('/Path/Tools/Library'))
self.assertTrue(PathPreferences.pathDefaultToolsPath('Template').endswith('/Path/Tools/Template'))

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2019 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 PathScripts.PathToolBit as PathToolBit
import PathTests.PathTestUtils as PathTestUtils
class TestPathToolBit(PathTestUtils.PathTestBase):
def test00(self):
'''Find a tool shapee from file name'''
path = PathToolBit.findShape('endmill.fcstd')
self.assertIsNot(path, None)
self.assertNotEqual(path, 'endmill.fcstd')
def test01(self):
'''Find a tool shapee from an invalid absolute path.'''
path = PathToolBit.findShape('/this/is/unlikely/a/valid/path/v-bit.fcstd')
self.assertIsNot(path, None)
self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd')
def test10(self):
'''find the relative path of a tool bit'''
shape = 'endmill.fcstd'
path = PathToolBit.findShape(shape)
self.assertIsNot(path, None)
self.assertGreater(len(path), len(shape))
rel = PathToolBit.findRelativePathShape(path)
self.assertEqual(rel, shape)
def test11(self):
'''store full path if relative path isn't found'''
path = '/this/is/unlikely/a/valid/path/v-bit.fcstd'
rel = PathToolBit.findRelativePathShape(path)
self.assertEqual(rel, path)

View File

@@ -25,6 +25,7 @@
import TestApp
from PathTests.TestPathLog import TestPathLog
from PathTests.TestPathPreferences import TestPathPreferences
from PathTests.TestPathCore import TestPathCore
#from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathGeom import TestPathGeom
@@ -35,6 +36,7 @@ from PathTests.TestPathDressupHoldingTags import TestHoldingTags
from PathTests.TestPathDressupDogbone import TestDressupDogbone
from PathTests.TestPathStock import TestPathStock
from PathTests.TestPathTool import TestPathTool
from PathTests.TestPathToolBit import TestPathToolBit
from PathTests.TestPathTooltable import TestPathTooltable
from PathTests.TestPathToolController import TestPathToolController
from PathTests.TestPathSetupSheet import TestPathSetupSheet
@@ -58,4 +60,6 @@ False if TestPathToolController.__name__ else True
False if TestPathSetupSheet.__name__ else True
False if TestPathDeburr.__name__ else True
False if TestPathHelix.__name__ else True
False if TestPathPreferences.__name__ else True
False if TestPathToolBit.__name__ else True

2
src/Mod/Path/Tools/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.fcstd1
*.FCStd1

View File

@@ -0,0 +1,12 @@
{
"version": 2,
"name": "T1",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "1.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"name": "T2",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "2.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 2,
"name": "T3",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "3.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"name": "T4",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "4.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"name": "T5",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "5.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"name": "T6",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "6.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"name": "T7",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "7.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"name": "T8",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "8.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"name": "T9",
"shape": "endmill.fcstd",
"attribute": {},
"parameter": {
"CuttingEdgeHeight": "30.000 mm",
"Diameter": "9.000 mm",
"Length": "50.000 mm",
"ShankDiameter": "3.000 mm"
}
}

View File

@@ -0,0 +1,41 @@
{
"tools": [
{
"nr": 1,
"path": "t1.fctb"
},
{
"nr": 2,
"path": "t2.fctb"
},
{
"nr": 3,
"path": "t3.fctb"
},
{
"nr": 4,
"path": "t4.fctb"
},
{
"nr": 5,
"path": "t5.fctb"
},
{
"nr": 6,
"path": "t6.fctb"
},
{
"nr": 7,
"path": "t7.fctb"
},
{
"nr": 8,
"path": "t8.fctb"
},
{
"nr": 9,
"path": "t9.fctb"
}
],
"version": 1
}

View File

@@ -0,0 +1,87 @@
# Tools
Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template.
It also includes all additional parameters and their values.
Storing a tool as a JSON file sounds great but eliminates the option of an accurate thumbnail. On the other hand,
storing each tool as a `*.fcstd` file requires more space and does not allow for generating tools. If one has an
extensive tool aresenal they might want to script the generation of tools which is easily done for a `*.json` file but
practically impossible for `*.fcstd` files.
When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according
to the values from the JSON file. All additional parameters are created as properties on the object. This provides the
the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms (and
potentially simulation).
# Tool Libraries
Due to each tool being stored in its own file and the storage/organization of those files being quite flexible the
importance of a tool library for organisational purposes is quite diminished. The user is free to organise their tools
in whichever directory hierarchy they see fit and can also name them as best fits their use and organisation. A
_tool library_ is nevertheless a great representation for a physical grouping of tools, such as in an automatic tool
changer.
A tool library is a (JSON) file with a mapping of tool id to the path of the tool file. As a consequence each tool
can be in multiple libraries and doesn't have an `id` of it's own. The `id` is a property of the library.
If a tool from a tool library (or an entire tool library) is added to a job it retains its `id` from the library as a
property. Adding a tool bit directly rsults in the tool getting the next free id assigned.
# Tool Controllers
They largely stay the same as they are today. As an additional feature it should be possible to _copy_ a TC, which
allows for easy feed/speed changes for the same tool.
Above requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC.
There are two requirements that are currently mapped to a single `id`. There needs to be an identification of which
TC is being used by a certain op, and which tool number to use for a `M6` command.
# Paths and Extensibility
The following directory structure is used for supplied (shipped with FreeCAD) tools:
```
Tools
+ Bit
+ Library
+ Shape
```
Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file
dialog will open the corresponding directory (depending on context), or whichever directory the user opened last.
Above directory structure with the most common default tools shipped with FreeCAD should be installed analogous to
TechDraw's templates.
## How to create a new tool
1. Set the tool's Label, this will show up in the object tree
1. Select a tool shape from the existing templates. If your tool doesn't exist, you'll have to create a new template,
see below for details.
1. Each template has its own set of parameters, fill them with the tool's values.
1. Select additional parameters
1. Save the tool under path/file that makes sense to you
## How to create a new tool bit Shape
A tool bit template represents the physical shape of a tool. It does not completely desribe the bit - for that some
additional parameters are needed which will be added when an actual bit is parametrized from the template.
1. Create a new FreeCAD document
1. Open the `PartDesign` workbench, create a body and give the body a label you want to show up in the bit selection.
1. Create a sketch in the XZ plane and draw half the profile of the bit.
* Put the top center of the bit on the origin (0,0)
1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint
* The name is the label of the input field
* Names are split at CamelCase boundaries into words in the edit dialog
* Use a `;` in the name to add help text which will show up as the entry fields tool tip
* If the tool is used by legacy ops it should at least have one constraint called `Diameter`
* Use construction lines for constraints that are not directly accessible, like `Diameter` and `Angle`
1. Any unnamed constraint will not be editable for a specific tool
1. Once the sketch is fully constrained, close the sketch
1. Rotate the sketch around the z-axis
1. Save the document as a new file in the Shape directory
* Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in
FreeCAD's preferences.
* Also make sure to switch to _Front View_ and _Fit content to screen_
* Whatever you see when saving the document will end up being the visual representation of the template

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -56,6 +56,7 @@ EXTERNAL_MODULES+=' Path'
EXTERNAL_MODULES+=' PySide'
EXTERNAL_MODULES+=' PySide.QtCore'
EXTERNAL_MODULES+=' PySide.QtGui'
EXTERNAL_MODULES+=' Sketcher'
EXTERNAL_MODULES+=' TechDraw'
EXTERNAL_MODULES+=' TestSketcherApp'
EXTERNAL_MODULES+=' area'