CAM: Remove hardcoded style for Tool Number, Fix TestPathToolBitSerializer
Fix issue with toolshapes Renamed fillet to radius Added Tool Type Filter to library Fix units so that they honor user preference Remove the QToolBox widget from the Shape Selector page and combine into a single page. Fix issue with PropertyBag so that CustomPropertyGroups as a string is converted to enum and enums are handled correctly. Update TestPathPropertyBag test for enum changes. Update TestPathToolBitListWidget Update TestPathToolLibrarySerializer to match new LinuxCNC output Fix LinuxCNC export too handle ALL tool types, use user preferences for units, and include all lcnc fields
This commit is contained in:
@@ -25,6 +25,20 @@ import Path.Base.PropertyBag as PathPropertyBag
|
||||
import CAMTests.PathTestUtils as PathTestUtils
|
||||
|
||||
|
||||
def as_group_list(groups):
|
||||
"""Normalize CustomPropertyGroups to a list of strings."""
|
||||
if groups is None:
|
||||
return []
|
||||
if isinstance(groups, (list, tuple)):
|
||||
return list(groups)
|
||||
if isinstance(groups, str):
|
||||
return [groups]
|
||||
try:
|
||||
return list(groups)
|
||||
except Exception:
|
||||
return [str(groups)]
|
||||
|
||||
|
||||
class TestPathPropertyBag(PathTestUtils.PathTestBase):
|
||||
def setUp(self):
|
||||
self.doc = FreeCAD.newDocument("test-property-bag")
|
||||
@@ -37,7 +51,7 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase):
|
||||
bag = PathPropertyBag.Create()
|
||||
self.assertTrue(hasattr(bag, "Proxy"))
|
||||
self.assertEqual(bag.Proxy.getCustomProperties(), [])
|
||||
self.assertEqual(bag.CustomPropertyGroups, [])
|
||||
self.assertEqual(as_group_list(bag.CustomPropertyGroups), [])
|
||||
|
||||
def test01(self):
|
||||
"""adding properties to a PropertyBag is tracked properly"""
|
||||
@@ -48,7 +62,7 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase):
|
||||
bag.Title = "Madame"
|
||||
self.assertEqual(bag.Title, "Madame")
|
||||
self.assertEqual(bag.Proxy.getCustomProperties(), ["Title"])
|
||||
self.assertEqual(bag.CustomPropertyGroups, ["Address"])
|
||||
self.assertEqual(as_group_list(bag.CustomPropertyGroups), ["Address"])
|
||||
|
||||
def test02(self):
|
||||
"""refreshCustomPropertyGroups deletes empty groups"""
|
||||
@@ -59,7 +73,7 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase):
|
||||
bag.removeProperty("Title")
|
||||
proxy.refreshCustomPropertyGroups()
|
||||
self.assertEqual(bag.Proxy.getCustomProperties(), [])
|
||||
self.assertEqual(bag.CustomPropertyGroups, [])
|
||||
self.assertEqual(as_group_list(bag.CustomPropertyGroups), [])
|
||||
|
||||
def test03(self):
|
||||
"""refreshCustomPropertyGroups does not delete non-empty groups"""
|
||||
@@ -72,4 +86,4 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase):
|
||||
bag.removeProperty("Gender")
|
||||
proxy.refreshCustomPropertyGroups()
|
||||
self.assertEqual(bag.Proxy.getCustomProperties(), ["Title"])
|
||||
self.assertEqual(bag.CustomPropertyGroups, ["Address"])
|
||||
self.assertEqual(as_group_list(bag.CustomPropertyGroups), ["Address"])
|
||||
|
||||
@@ -55,7 +55,7 @@ class TestToolBitListWidget(PathTestWithAssets):
|
||||
self.assertEqual(cell_widget.tool_no, str(tool_no))
|
||||
self.assertEqual(cell_widget.upper_text, toolbit.label)
|
||||
# Assuming the 5mm_Endmill asset has a shape named 'Endmill'
|
||||
self.assertEqual(cell_widget.lower_text, "5 mm 4-flute endmill, 30 mm cutting edge")
|
||||
self.assertEqual(cell_widget.lower_text, "5.00 mm 4-flute endmill, 30.00 mm cutting edge")
|
||||
|
||||
# Verify URI is stored in item data
|
||||
stored_uri = item.data(ToolBitUriRole)
|
||||
|
||||
@@ -149,7 +149,7 @@ class TestYamlToolBitSerializer(_BaseToolBitSerializerTestCase):
|
||||
self.assertEqual(data.get("shape"), "endmill.fcstd")
|
||||
self.assertEqual(data.get("shape-type"), "Endmill")
|
||||
self.assertEqual(data.get("parameter", {}).get("Diameter"), "4.12 mm")
|
||||
self.assertEqual(data.get("parameter", {}).get("Length"), "15.0 mm")
|
||||
self.assertEqual(data.get("parameter", {}).get("Length"), "15.00 mm")
|
||||
|
||||
def test_extract_dependencies(self):
|
||||
"""Test dependency extraction for YAML."""
|
||||
|
||||
@@ -144,13 +144,13 @@ class TestLinuxCNCLibrarySerializer(TestPathToolLibrarySerializerBase):
|
||||
# Verify the content format (basic check)
|
||||
lines = serialized_data.decode("ascii", "ignore").strip().split("\n")
|
||||
self.assertEqual(len(lines), 3)
|
||||
self.assertEqual(lines[0], "T1 P0 D6.000 ;Endmill 6mm")
|
||||
self.assertEqual(lines[1], "T2 P0 D3.000 ;Endmill 3mm")
|
||||
self.assertEqual(lines[2], "T3 P0 D5.000 ;Ballend 5mm")
|
||||
self.assertEqual(lines[0], "T1 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D6.00 I0 J0 Q0 ;Endmill 6mm")
|
||||
self.assertEqual(lines[1], "T2 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D3.00 I0 J0 Q0 ;Endmill 3mm")
|
||||
self.assertEqual(lines[2], "T3 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D5.00 I0 J0 Q0 ;Ballend 5mm")
|
||||
|
||||
def test_linuxcnc_deserialize_not_implemented(self):
|
||||
serializer = LinuxCNCSerializer
|
||||
dummy_data = b"T1 D6.0 ;Endmill 6mm\n"
|
||||
dummy_data = b"T1 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D6.00 I0 J0 Q0 ;Endmill 6mm\n"
|
||||
with self.assertRaises(NotImplementedError):
|
||||
serializer.deserialize(dummy_data, "dummy_id", {})
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ SET(PathPythonToolsToolBitModels_SRCS
|
||||
Path/Tool/toolbit/models/dovetail.py
|
||||
Path/Tool/toolbit/models/drill.py
|
||||
Path/Tool/toolbit/models/endmill.py
|
||||
Path/Tool/toolbit/models/fillet.py
|
||||
Path/Tool/toolbit/models/radius.py
|
||||
Path/Tool/toolbit/models/probe.py
|
||||
Path/Tool/toolbit/models/reamer.py
|
||||
Path/Tool/toolbit/models/slittingsaw.py
|
||||
@@ -264,7 +264,7 @@ SET(PathPythonToolsShapeModels_SRCS
|
||||
Path/Tool/shape/models/dovetail.py
|
||||
Path/Tool/shape/models/drill.py
|
||||
Path/Tool/shape/models/endmill.py
|
||||
Path/Tool/shape/models/fillet.py
|
||||
Path/Tool/shape/models/radius.py
|
||||
Path/Tool/shape/models/icon.py
|
||||
Path/Tool/shape/models/probe.py
|
||||
Path/Tool/shape/models/reamer.py
|
||||
@@ -455,8 +455,8 @@ SET(Tools_Shape_SRCS
|
||||
Tools/Shape/drill.svg
|
||||
Tools/Shape/endmill.fcstd
|
||||
Tools/Shape/endmill.svg
|
||||
Tools/Shape/fillet.fcstd
|
||||
Tools/Shape/fillet.svg
|
||||
Tools/Shape/radius.fcstd
|
||||
Tools/Shape/radius.svg
|
||||
Tools/Shape/probe.fcstd
|
||||
Tools/Shape/probe.svg
|
||||
Tools/Shape/reamer.fcstd
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="gridLayout">
|
||||
<item>
|
||||
<widget class="QToolBox" name="toolBox">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="standardTools">
|
||||
<widget class="QWidget" name="toolsContainer">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
@@ -30,22 +30,6 @@
|
||||
<height>487</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
<string>Standard tools</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
<widget class="QWidget" name="customTools">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>880</width>
|
||||
<height>487</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
<string>My tools</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -150,7 +150,6 @@ class PropertyCreate(object):
|
||||
self.form.propertyEnum.textChanged.connect(self.updateUI)
|
||||
|
||||
def updateUI(self):
|
||||
|
||||
typeSet = True
|
||||
if self.propertyIsEnumeration():
|
||||
self.form.labelEnum.setEnabled(True)
|
||||
@@ -239,7 +238,17 @@ class TaskPanel(object):
|
||||
pass
|
||||
|
||||
def _setupProperty(self, i, name):
|
||||
typ = PathPropertyBag.getPropertyTypeName(self.obj.getTypeIdOfProperty(name))
|
||||
if name not in self.obj.PropertiesList:
|
||||
Path.Log.warning(f"Property '{name}' not found in object {self.obj.Name}")
|
||||
return
|
||||
prop_type_id = self.obj.getTypeIdOfProperty(name)
|
||||
try:
|
||||
typ = PathPropertyBag.getPropertyTypeName(prop_type_id)
|
||||
except IndexError:
|
||||
Path.Log.error(
|
||||
f"Unknown property type id '{prop_type_id}' for property '{name}' in object {self.obj.Name}"
|
||||
)
|
||||
return
|
||||
val = PathUtil.getPropertyValueString(self.obj, name)
|
||||
info = self.obj.getDocumentationOfProperty(name)
|
||||
|
||||
|
||||
@@ -68,12 +68,14 @@ class PropertyBag(object):
|
||||
CustomPropertyGroupDefault = "User"
|
||||
|
||||
def __init__(self, obj):
|
||||
obj.addProperty(
|
||||
"App::PropertyStringList",
|
||||
self.CustomPropertyGroups,
|
||||
"Base",
|
||||
QT_TRANSLATE_NOOP("App::Property", "List of custom property groups"),
|
||||
)
|
||||
# Always add as enumeration
|
||||
if not hasattr(obj, self.CustomPropertyGroups):
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
self.CustomPropertyGroups,
|
||||
"Base",
|
||||
QT_TRANSLATE_NOOP("App::Property", "List of custom property groups"),
|
||||
)
|
||||
self.onDocumentRestored(obj)
|
||||
|
||||
def dumps(self):
|
||||
@@ -96,15 +98,39 @@ class PropertyBag(object):
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
self.obj = obj
|
||||
obj.setEditorMode(self.CustomPropertyGroups, 2) # hide
|
||||
cpg = getattr(obj, self.CustomPropertyGroups, None)
|
||||
# If it's a string list, convert to enum
|
||||
if isinstance(cpg, list):
|
||||
vals = cpg
|
||||
try:
|
||||
obj.removeProperty(self.CustomPropertyGroups)
|
||||
except Exception:
|
||||
pass
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
self.CustomPropertyGroups,
|
||||
"Base",
|
||||
QT_TRANSLATE_NOOP("App::Property", "List of custom property groups"),
|
||||
)
|
||||
if hasattr(obj, "setEnumerationsOfProperty"):
|
||||
obj.setEnumerationsOfProperty(self.CustomPropertyGroups, vals)
|
||||
else:
|
||||
# Fallback: set the property value directly (may not work in all FreeCAD versions)
|
||||
setattr(obj, self.CustomPropertyGroups, vals)
|
||||
if hasattr(obj, "setEditorMode"):
|
||||
obj.setEditorMode(self.CustomPropertyGroups, 2) # hide
|
||||
elif hasattr(obj, "getEnumerationsOfProperty"):
|
||||
if hasattr(obj, "setEditorMode"):
|
||||
obj.setEditorMode(self.CustomPropertyGroups, 2) # hide
|
||||
|
||||
def getCustomProperties(self):
|
||||
"""getCustomProperties() ... Return a list of all custom properties created in this container."""
|
||||
return [
|
||||
p
|
||||
for p in self.obj.PropertiesList
|
||||
if self.obj.getGroupOfProperty(p) in self.obj.CustomPropertyGroups
|
||||
]
|
||||
"""Return a list of all custom properties created in this container."""
|
||||
groups = []
|
||||
if hasattr(self.obj, "getEnumerationsOfProperty"):
|
||||
groups = list(self.obj.getEnumerationsOfProperty(self.CustomPropertyGroups))
|
||||
else:
|
||||
groups = list(getattr(self.obj, self.CustomPropertyGroups, []))
|
||||
return [p for p in self.obj.PropertiesList if self.obj.getGroupOfProperty(p) in groups]
|
||||
|
||||
def addCustomProperty(self, propertyType, name, group=None, desc=None):
|
||||
"""addCustomProperty(propertyType, name, group=None, desc=None) ... adds a custom property and tracks its group."""
|
||||
@@ -112,15 +138,23 @@ class PropertyBag(object):
|
||||
desc = ""
|
||||
if group is None:
|
||||
group = self.CustomPropertyGroupDefault
|
||||
groups = self.obj.CustomPropertyGroups
|
||||
|
||||
# Always use enum for groups
|
||||
if hasattr(self.obj, "getEnumerationsOfProperty"):
|
||||
groups = list(self.obj.getEnumerationsOfProperty(self.CustomPropertyGroups))
|
||||
else:
|
||||
groups = list(getattr(self.obj, self.CustomPropertyGroups, []))
|
||||
|
||||
name = self.__sanitizePropertyName(name)
|
||||
if not re.match("^[A-Za-z0-9_]*$", name):
|
||||
raise ValueError("Property Name can only contain letters and numbers")
|
||||
|
||||
if not group in groups:
|
||||
if group not in groups:
|
||||
groups.append(group)
|
||||
self.obj.CustomPropertyGroups = groups
|
||||
if hasattr(self.obj, "setEnumerationsOfProperty"):
|
||||
self.obj.setEnumerationsOfProperty(self.CustomPropertyGroups, groups)
|
||||
else:
|
||||
setattr(self.obj, self.CustomPropertyGroups, groups)
|
||||
self.obj.addProperty(propertyType, name, group, desc)
|
||||
return name
|
||||
|
||||
@@ -129,9 +163,16 @@ class PropertyBag(object):
|
||||
customGroups = []
|
||||
for p in self.obj.PropertiesList:
|
||||
group = self.obj.getGroupOfProperty(p)
|
||||
if group in self.obj.CustomPropertyGroups and not group in customGroups:
|
||||
if hasattr(self.obj, "getEnumerationsOfProperty"):
|
||||
groups = list(self.obj.getEnumerationsOfProperty(self.CustomPropertyGroups))
|
||||
else:
|
||||
groups = list(getattr(self.obj, self.CustomPropertyGroups, []))
|
||||
if group in groups and group not in customGroups:
|
||||
customGroups.append(group)
|
||||
self.obj.CustomPropertyGroups = customGroups
|
||||
if hasattr(self.obj, "setEnumerationsOfProperty"):
|
||||
self.obj.setEnumerationsOfProperty(self.CustomPropertyGroups, customGroups)
|
||||
else:
|
||||
setattr(self.obj, self.CustomPropertyGroups, customGroups)
|
||||
|
||||
|
||||
def Create(name="PropertyBag"):
|
||||
|
||||
@@ -78,10 +78,10 @@ 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)
|
||||
if attr is not None and isinstance(value, str):
|
||||
if isinstance(attr, int):
|
||||
value = int(value, 0)
|
||||
elif isinstance(attr, bool):
|
||||
if isinstance(attr, bool):
|
||||
value = value.lower() in ["true", "1", "yes", "ok"]
|
||||
elif isinstance(attr, int):
|
||||
value = int(value, 0)
|
||||
if o and name:
|
||||
setattr(o, name, value)
|
||||
|
||||
|
||||
@@ -50,18 +50,29 @@ class LinuxCNCSerializer(AssetSerializer):
|
||||
|
||||
output = io.BytesIO()
|
||||
for bit_no, bit in sorted(asset._bit_nos.items()):
|
||||
assert isinstance(bit, ToolBit)
|
||||
if not isinstance(bit, RotaryToolBitMixin):
|
||||
Path.Log.warning(
|
||||
f"Skipping too {bit.label} (bit.id) because it is not a rotary tool"
|
||||
)
|
||||
continue
|
||||
diameter = bit.get_diameter()
|
||||
# Connor: assert isinstance(bit, ToolBit)
|
||||
# if not isinstance(bit, RotaryToolBitMixin):
|
||||
# Path.Log.warning(
|
||||
# f"Skipping too {bit.label} (bit.id) because it is not a rotary tool"
|
||||
# )
|
||||
# continue
|
||||
# Commenting this out. Why did we skip because it is not a rotary tool?
|
||||
diameter = bit.get_diameter().getUserPreferred()[0]
|
||||
pocket = "P0" # TODO: is there a better way?
|
||||
|
||||
# Format diameter to one decimal place and remove units
|
||||
diameter_value = diameter.Value if hasattr(diameter, "Value") else diameter
|
||||
line = f"T{bit_no} {pocket} D{diameter_value:.3f} ;{bit.label}\n"
|
||||
# TODO: Strip units by splitting at the first space if diameter is a string
|
||||
# This is where we need a machine definition so we can export these out correctly
|
||||
# for a metric or imperial machine
|
||||
# Using user preferred for now
|
||||
if hasattr(diameter, "Value"):
|
||||
diameter_value = diameter.Value
|
||||
elif isinstance(diameter, str):
|
||||
diameter_value = diameter.split(" ")[0]
|
||||
else:
|
||||
diameter_value = diameter
|
||||
line = (
|
||||
f"T{bit_no} {pocket} X0 Y0 Z0 A0 B0 C0 U0 V0 W0 "
|
||||
f"D{diameter_value} I0 J0 Q0 ;{bit.label}\n"
|
||||
)
|
||||
output.write(line.encode("utf-8"))
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
@@ -65,8 +65,17 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
compact=compact,
|
||||
)
|
||||
self.current_library: Optional[Library] = None
|
||||
self._selected_tool_type: Optional[str] = None
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Add tool type filter combo box to the base widget
|
||||
self._tool_type_combo = QtGui.QComboBox()
|
||||
self._tool_type_combo.setSizePolicy(
|
||||
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred
|
||||
)
|
||||
self._top_layout.insertWidget(0, self._tool_type_combo, 1)
|
||||
self._tool_type_combo.currentTextChanged.connect(self._on_tool_type_combo_changed)
|
||||
|
||||
self.restore_last_sort_order()
|
||||
self.load_last_library()
|
||||
|
||||
@@ -177,6 +186,35 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
if library:
|
||||
Path.Preferences.setLastToolLibrary(str(library.get_uri()))
|
||||
|
||||
def _get_available_tool_types(self):
|
||||
"""Get all available tool types from the current assets."""
|
||||
tool_types = set()
|
||||
# Make sure we have assets to work with
|
||||
if not hasattr(self, "_all_assets") or not self._all_assets:
|
||||
return []
|
||||
|
||||
for asset in self._all_assets:
|
||||
# Use get_shape_name() method to get the tool type
|
||||
if hasattr(asset, "get_shape_name"):
|
||||
tool_type = asset.get_shape_name()
|
||||
if tool_type:
|
||||
tool_types.add(tool_type)
|
||||
|
||||
return sorted(tool_types)
|
||||
|
||||
def _get_filtered_assets(self):
|
||||
"""Get assets filtered by tool type if a specific type is selected."""
|
||||
if not self._selected_tool_type or self._selected_tool_type == "All Tool Types":
|
||||
return self._all_assets
|
||||
|
||||
filtered_assets = []
|
||||
for asset in self._all_assets:
|
||||
if hasattr(asset, "get_shape_name"):
|
||||
tool_type = asset.get_shape_name()
|
||||
if tool_type == self._selected_tool_type:
|
||||
filtered_assets.append(asset)
|
||||
return filtered_assets
|
||||
|
||||
def _update_tool_list(self):
|
||||
"""Updates the tool list based on the current library."""
|
||||
if self.current_library:
|
||||
@@ -187,8 +225,34 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
self._all_assets = cast(List[ToolBit], all_toolbits)
|
||||
self._sort_assets()
|
||||
self._tool_list_widget.clear_list()
|
||||
# Update tool type combo after assets are loaded
|
||||
if hasattr(self, "_tool_type_combo"):
|
||||
self._update_tool_type_combo()
|
||||
self._update_list()
|
||||
|
||||
def _update_list(self):
|
||||
"""Updates the list widget with filtered assets."""
|
||||
self._tool_list_widget.clear_list()
|
||||
filtered_assets = self._get_filtered_assets()
|
||||
|
||||
# Apply search filter if there is one
|
||||
search_term = self._search_edit.text().lower()
|
||||
if search_term:
|
||||
search_filtered = []
|
||||
for asset in filtered_assets:
|
||||
if search_term in asset.label.lower():
|
||||
search_filtered.append(asset)
|
||||
continue
|
||||
# Also search in tool type
|
||||
if hasattr(asset, "get_shape_name"):
|
||||
tool_type = asset.get_shape_name()
|
||||
if tool_type and search_term in tool_type.lower():
|
||||
search_filtered.append(asset)
|
||||
filtered_assets = search_filtered
|
||||
|
||||
for asset in filtered_assets:
|
||||
self._tool_list_widget.add_toolbit(asset)
|
||||
|
||||
def _add_shortcuts(self):
|
||||
"""Adds keyboard shortcuts for common actions."""
|
||||
Path.Log.debug("LibraryBrowserWidget._add_shortcuts: Called.")
|
||||
@@ -476,6 +540,32 @@ class LibraryBrowserWidget(ToolBitBrowserWidget):
|
||||
self._asset_manager.add(library)
|
||||
self.refresh()
|
||||
|
||||
def _update_tool_type_combo(self):
|
||||
"""Update the tool type combo box with available types."""
|
||||
current_selection = self._tool_type_combo.currentText()
|
||||
self._tool_type_combo.blockSignals(True)
|
||||
try:
|
||||
self._tool_type_combo.clear()
|
||||
self._tool_type_combo.addItem("All Tool Types")
|
||||
|
||||
for tool_type in self._get_available_tool_types():
|
||||
self._tool_type_combo.addItem(tool_type)
|
||||
|
||||
# Restore selection if it still exists
|
||||
index = self._tool_type_combo.findText(current_selection)
|
||||
if index >= 0:
|
||||
self._tool_type_combo.setCurrentIndex(index)
|
||||
else:
|
||||
self._tool_type_combo.setCurrentIndex(0)
|
||||
self._selected_tool_type = "All Tool Types"
|
||||
finally:
|
||||
self._tool_type_combo.blockSignals(False)
|
||||
|
||||
def _on_tool_type_combo_changed(self, tool_type):
|
||||
"""Handle tool type filter selection change."""
|
||||
self._selected_tool_type = tool_type
|
||||
self._update_list()
|
||||
|
||||
|
||||
class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
"""
|
||||
@@ -502,10 +592,15 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
self._top_layout.removeWidget(self._search_edit)
|
||||
layout.insertWidget(1, self._search_edit, 20)
|
||||
|
||||
# Library selection combo box
|
||||
self._library_combo = QtGui.QComboBox()
|
||||
self._library_combo.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
|
||||
self._top_layout.insertWidget(0, self._library_combo, 1)
|
||||
self._library_combo.currentIndexChanged.connect(self._on_library_combo_changed)
|
||||
|
||||
self._top_layout.removeWidget(self._tool_type_combo)
|
||||
self._top_layout.insertWidget(1, self._tool_type_combo, 1)
|
||||
|
||||
self.current_library_changed.connect(self._on_current_library_changed)
|
||||
|
||||
self._in_refresh = False
|
||||
@@ -554,6 +649,11 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget):
|
||||
if not libraries:
|
||||
return
|
||||
if not self.current_library:
|
||||
first_library = self._library_combo.itemData(0)
|
||||
if first_library:
|
||||
uri = first_library.get_uri()
|
||||
library = self._asset_manager.get(uri, store=self._store_name, depth=1)
|
||||
self.set_current_library(library)
|
||||
self._library_combo.setCurrentIndex(0)
|
||||
return
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ from ...toolbit import ToolBit
|
||||
from .editor import LibraryEditor
|
||||
from .browser import LibraryBrowserWithCombo
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
@@ -66,7 +65,7 @@ class ToolBitLibraryDock(object):
|
||||
self.form_layout.setSpacing(4)
|
||||
|
||||
# Create the browser widget
|
||||
self.browser_widget = LibraryBrowserWidget(asset_manager=cam_assets)
|
||||
self.browser_widget = LibraryBrowserWithCombo(asset_manager=cam_assets)
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
@@ -80,8 +79,6 @@ class ToolBitLibraryDock(object):
|
||||
main_layout.setContentsMargins(4, 4, 4, 4)
|
||||
main_layout.setSpacing(4)
|
||||
|
||||
# Create the browser widget
|
||||
self.browser_widget = LibraryBrowserWithCombo(asset_manager=cam_assets)
|
||||
main_layout.addWidget(self.browser_widget)
|
||||
|
||||
# Create buttons
|
||||
@@ -89,11 +86,19 @@ class ToolBitLibraryDock(object):
|
||||
translate("CAM_ToolBit", "Open Library Editor")
|
||||
)
|
||||
self.addToolControllerButton = QtGui.QPushButton(translate("CAM_ToolBit", "Add to Job"))
|
||||
self.closeButton = QtGui.QPushButton(translate("CAM_ToolBit", "Close"))
|
||||
|
||||
# Add buttons to a horizontal layout
|
||||
button_width = 120
|
||||
self.libraryEditorOpenButton.setMinimumWidth(button_width)
|
||||
self.addToolControllerButton.setMinimumWidth(button_width)
|
||||
self.closeButton.setMinimumWidth(button_width)
|
||||
|
||||
# Add buttons to a horizontal layout, right-align Close
|
||||
button_layout = QtGui.QHBoxLayout()
|
||||
button_layout.addWidget(self.libraryEditorOpenButton)
|
||||
button_layout.addWidget(self.addToolControllerButton)
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self.closeButton)
|
||||
|
||||
# Add the button layout to the main layout
|
||||
main_layout.addLayout(button_layout)
|
||||
@@ -106,6 +111,7 @@ class ToolBitLibraryDock(object):
|
||||
self.browser_widget.itemDoubleClicked.connect(self._on_doubleclick)
|
||||
self.libraryEditorOpenButton.clicked.connect(self._open_editor)
|
||||
self.addToolControllerButton.clicked.connect(self._add_tool_controller_to_doc)
|
||||
self.closeButton.clicked.connect(self.form.reject)
|
||||
|
||||
# Update the initial state of the UI
|
||||
self._update_state()
|
||||
|
||||
@@ -10,7 +10,7 @@ from .models.custom import ToolBitShapeCustom
|
||||
from .models.dovetail import ToolBitShapeDovetail
|
||||
from .models.drill import ToolBitShapeDrill
|
||||
from .models.endmill import ToolBitShapeEndmill
|
||||
from .models.fillet import ToolBitShapeFillet
|
||||
from .models.radius import ToolBitShapeRadius
|
||||
from .models.probe import ToolBitShapeProbe
|
||||
from .models.reamer import ToolBitShapeReamer
|
||||
from .models.slittingsaw import ToolBitShapeSlittingSaw
|
||||
@@ -36,7 +36,7 @@ __all__ = [
|
||||
"ToolBitShapeDovetail",
|
||||
"ToolBitShapeDrill",
|
||||
"ToolBitShapeEndmill",
|
||||
"ToolBitShapeFillet",
|
||||
"ToolBitShapeRadius",
|
||||
"ToolBitShapeProbe",
|
||||
"ToolBitShapeReamer",
|
||||
"ToolBitShapeSlittingSaw",
|
||||
|
||||
@@ -34,9 +34,31 @@ class ToolBitShapeCustom(ToolBitShape):
|
||||
name: str = "Custom"
|
||||
aliases = ("custom",)
|
||||
|
||||
# Connor: We're going to treat custom tools as normal endmills
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {}
|
||||
return {
|
||||
"CuttingEdgeHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting edge height"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Flutes": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Flutes"),
|
||||
"App::PropertyInteger",
|
||||
),
|
||||
"Length": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Overall tool length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"ShankDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitToolBitShapeShapeEndMill", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
|
||||
@@ -25,23 +25,26 @@ from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeFillet(ToolBitShape):
|
||||
name = "Fillet"
|
||||
aliases = ("fillet",)
|
||||
class ToolBitShapeRadius(ToolBitShape):
|
||||
name = "Radius"
|
||||
aliases = (
|
||||
"radius",
|
||||
"fillet",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"CrownHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Crown height"),
|
||||
"CuttingEdgeHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting edge height"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"FilletRadius": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Fillet radius"),
|
||||
"CuttingRadius": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting radius"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Flutes": (
|
||||
@@ -60,4 +63,4 @@ class ToolBitShapeFillet(ToolBitShape):
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Filleted Chamfer")
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Radius Mill")
|
||||
@@ -39,7 +39,6 @@ class ShapeSelector:
|
||||
self.flows = {}
|
||||
|
||||
self.update_shapes()
|
||||
self.form.toolBox.setCurrentIndex(0)
|
||||
|
||||
def _add_shape_group(self, toolbox):
|
||||
if toolbox in self.flows:
|
||||
@@ -70,8 +69,10 @@ class ShapeSelector:
|
||||
custom = cam_assets.fetch(asset_type="toolbitshape", store="local")
|
||||
for shape in custom:
|
||||
builtin.pop(shape.id, None)
|
||||
self._add_shapes(self.form.standardTools, builtin.values())
|
||||
self._add_shapes(self.form.customTools, custom)
|
||||
|
||||
# Combine all shapes into a single list
|
||||
all_shapes = list(builtin.values()) + list(custom)
|
||||
self._add_shapes(self.form.toolsContainer, all_shapes)
|
||||
|
||||
def on_shape_button_clicked(self, shape):
|
||||
self.shape = shape
|
||||
|
||||
@@ -10,7 +10,7 @@ from .models.custom import ToolBitCustom
|
||||
from .models.dovetail import ToolBitDovetail
|
||||
from .models.drill import ToolBitDrill
|
||||
from .models.endmill import ToolBitEndmill
|
||||
from .models.fillet import ToolBitFillet
|
||||
from .models.radius import ToolBitRadius
|
||||
from .models.probe import ToolBitProbe
|
||||
from .models.reamer import ToolBitReamer
|
||||
from .models.slittingsaw import ToolBitSlittingSaw
|
||||
@@ -28,7 +28,7 @@ __all__ = [
|
||||
"ToolBitDovetail",
|
||||
"ToolBitDrill",
|
||||
"ToolBitEndmill",
|
||||
"ToolBitFillet",
|
||||
"ToolBitRadius",
|
||||
"ToolBitProbe",
|
||||
"ToolBitReamer",
|
||||
"ToolBitSlittingSaw",
|
||||
|
||||
@@ -513,6 +513,7 @@ class ToolBit(Asset, ABC):
|
||||
self._create_base_properties()
|
||||
|
||||
# Transfer property values from the detached object to the real object
|
||||
self._suppress_visual_update = True
|
||||
temp_obj.copy_to(self.obj)
|
||||
|
||||
# Ensure label is set
|
||||
@@ -520,6 +521,7 @@ class ToolBit(Asset, ABC):
|
||||
|
||||
# Update the visual representation now that it's attached
|
||||
self._update_tool_properties()
|
||||
self._suppress_visual_update = False
|
||||
self._update_visual_representation()
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
@@ -528,6 +530,9 @@ class ToolBit(Asset, ABC):
|
||||
if "Restore" in obj.State:
|
||||
return
|
||||
|
||||
if getattr(self, "_suppress_visual_update", False):
|
||||
return
|
||||
|
||||
if hasattr(self, "_in_update") and self._in_update:
|
||||
Path.Log.debug(f"Skipping onChanged for {obj.Label} due to active update.")
|
||||
return
|
||||
|
||||
@@ -35,3 +35,32 @@ class ToolBitCustom(ToolBit):
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
return FreeCAD.Qt.translate("CAM", "Unknown custom toolbit type")
|
||||
|
||||
# Connor: Adding in getters and setters for diameter and length
|
||||
def get_diameter(self) -> FreeCAD.Units.Quantity:
|
||||
"""
|
||||
Get the diameter of the rotary tool bit from the shape.
|
||||
"""
|
||||
return self.obj.Diameter
|
||||
|
||||
def set_diameter(self, diameter: FreeCAD.Units.Quantity):
|
||||
"""
|
||||
Set the diameter of the rotary tool bit on the shape.
|
||||
"""
|
||||
if not isinstance(diameter, FreeCAD.Units.Quantity):
|
||||
raise ValueError("Diameter must be a FreeCAD Units.Quantity")
|
||||
self.obj.Diameter = diameter
|
||||
|
||||
def get_length(self) -> FreeCAD.Units.Quantity:
|
||||
"""
|
||||
Get the length of the rotary tool bit from the shape.
|
||||
"""
|
||||
return self.obj.Length
|
||||
|
||||
def set_length(self, length: FreeCAD.Units.Quantity):
|
||||
"""
|
||||
Set the length of the rotary tool bit on the shape.
|
||||
"""
|
||||
if not isinstance(length, FreeCAD.Units.Quantity):
|
||||
raise ValueError("Length must be a FreeCAD Units.Quantity")
|
||||
self.obj.Length = length
|
||||
|
||||
@@ -46,3 +46,32 @@ class ToolBitProbe(ToolBit):
|
||||
|
||||
def can_rotate(self) -> bool:
|
||||
return False
|
||||
|
||||
# Connor: Add getters and setters for Diameter and Length
|
||||
def get_diameter(self) -> FreeCAD.Units.Quantity:
|
||||
"""
|
||||
Get the diameter of the rotary tool bit from the shape.
|
||||
"""
|
||||
return self.obj.Diameter
|
||||
|
||||
def set_diameter(self, diameter: FreeCAD.Units.Quantity):
|
||||
"""
|
||||
Set the diameter of the rotary tool bit on the shape.
|
||||
"""
|
||||
if not isinstance(diameter, FreeCAD.Units.Quantity):
|
||||
raise ValueError("Diameter must be a FreeCAD Units.Quantity")
|
||||
self.obj.Diameter = diameter
|
||||
|
||||
def get_length(self) -> FreeCAD.Units.Quantity:
|
||||
"""
|
||||
Get the length of the rotary tool bit from the shape.
|
||||
"""
|
||||
return self.obj.Length
|
||||
|
||||
def set_length(self, length: FreeCAD.Units.Quantity):
|
||||
"""
|
||||
Set the length of the rotary tool bit on the shape.
|
||||
"""
|
||||
if not isinstance(length, FreeCAD.Units.Quantity):
|
||||
raise ValueError("Length must be a FreeCAD Units.Quantity")
|
||||
self.obj.Length = length
|
||||
|
||||
@@ -21,25 +21,25 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
import Path
|
||||
from ...shape import ToolBitShapeFillet
|
||||
from ...shape import ToolBitShapeRadius
|
||||
from ..mixins import RotaryToolBitMixin, CuttingToolMixin
|
||||
from .base import ToolBit
|
||||
|
||||
|
||||
class ToolBitFillet(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeFillet
|
||||
class ToolBitRadius(ToolBit, CuttingToolMixin, RotaryToolBitMixin):
|
||||
SHAPE_CLASS = ToolBitShapeRadius
|
||||
|
||||
def __init__(self, shape: ToolBitShapeFillet, id: str | None = None):
|
||||
Path.Log.track(f"ToolBitFillet __init__ called with shape: {shape}, id: {id}")
|
||||
def __init__(self, shape: ToolBitShapeRadius, id: str | None = None):
|
||||
Path.Log.track(f"ToolBitRadius __init__ called with shape: {shape}, id: {id}")
|
||||
super().__init__(shape, id=id)
|
||||
CuttingToolMixin.__init__(self, self.obj)
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
radius = self.get_property_str("FilletRadius", "?", precision=3)
|
||||
radius = self.get_property_str("CuttingRadius", "?", precision=3)
|
||||
flutes = self.get_property("Flutes")
|
||||
diameter = self.get_property_str("ShankDiameter", "?", precision=3)
|
||||
|
||||
return FreeCAD.Qt.translate(
|
||||
"CAM", f"R{radius} fillet bit, {diameter} shank, {flutes}-flute"
|
||||
"CAM", f"R{radius} radius mill, {diameter} shank, {flutes}-flute"
|
||||
)
|
||||
@@ -66,24 +66,20 @@ class TwoLineTableCell(QtGui.QWidget):
|
||||
self.vbox.addWidget(self.label_upper)
|
||||
self.vbox.addWidget(self.label_lower)
|
||||
|
||||
style = "color: {}".format(fg_color.name())
|
||||
self.label_left = QtGui.QLabel()
|
||||
self.label_left.setMinimumWidth(40)
|
||||
self.label_left.setTextFormat(QtCore.Qt.RichText)
|
||||
self.label_left.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter)
|
||||
self.label_left.setStyleSheet(style)
|
||||
self.label_left.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
|
||||
|
||||
ratio = self.devicePixelRatioF()
|
||||
self.icon_size = QtCore.QSize(50 * ratio, 60 * ratio)
|
||||
self.icon_widget = QtGui.QLabel()
|
||||
|
||||
style = "color: {}".format(fg_color.name())
|
||||
self.label_right = QtGui.QLabel()
|
||||
self.label_right.setMinimumWidth(40)
|
||||
self.label_right.setTextFormat(QtCore.Qt.RichText)
|
||||
self.label_right.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.label_right.setStyleSheet(style)
|
||||
self.label_right.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
|
||||
|
||||
self.hbox = QtGui.QHBoxLayout()
|
||||
|
||||
@@ -34,9 +34,16 @@ def format_value(value: FreeCAD.Units.Quantity | int | float | None, precision:
|
||||
return None
|
||||
elif isinstance(value, FreeCAD.Units.Quantity):
|
||||
if precision is not None:
|
||||
user_val, _, user_unit = value.getUserPreferred()
|
||||
if user_unit in ("deg", "°", "degree", "degrees"):
|
||||
# Remove the last character (degree symbol) and convert to float
|
||||
try:
|
||||
deg_val = float(str(user_val)[:-1])
|
||||
except Exception:
|
||||
return value.getUserPreferred()[0]
|
||||
formatted_value = f"{deg_val:.1f}".rstrip("0").rstrip(".")
|
||||
return f"{formatted_value}°"
|
||||
# Format the value with the specified number of precision and strip trailing zeros
|
||||
formatted_value = f"{value.Value:.{precision}f}".rstrip("0").rstrip(".")
|
||||
unit = value.getUserPreferred()[2]
|
||||
return f"{formatted_value} {unit}"
|
||||
return value.getUserPreferred()[0]
|
||||
return value.UserString
|
||||
return str(value)
|
||||
|
||||
Binary file not shown.
BIN
src/Mod/CAM/Tools/Shape/radius.fcstd
Normal file
BIN
src/Mod/CAM/Tools/Shape/radius.fcstd
Normal file
Binary file not shown.
@@ -5,9 +5,9 @@
|
||||
viewBox="0 0 210 297"
|
||||
height="297mm"
|
||||
width="210mm"
|
||||
sodipodi:docname="fillet.svg"
|
||||
sodipodi:docname="radius.svg"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
@@ -26,13 +26,13 @@
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.50398562"
|
||||
inkscape:cx="712.32191"
|
||||
inkscape:cy="915.70073"
|
||||
inkscape:window-width="2311"
|
||||
inkscape:window-height="1509"
|
||||
inkscape:window-x="1529"
|
||||
inkscape:window-y="377"
|
||||
inkscape:zoom="0.41628253"
|
||||
inkscape:cx="136.92624"
|
||||
inkscape:cy="599.35256"
|
||||
inkscape:window-width="1512"
|
||||
inkscape:window-height="916"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg8" /><defs
|
||||
id="defs2"><linearGradient
|
||||
@@ -97,7 +97,11 @@
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker4584"
|
||||
style="overflow:visible"><path
|
||||
style="overflow:visible"
|
||||
viewBox="0 0 12.70584107 9.5264135"
|
||||
markerWidth="12.70584106"
|
||||
markerHeight="9.5264135"
|
||||
preserveAspectRatio="xMidYMid"><path
|
||||
id="path4582"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
@@ -128,7 +132,11 @@
|
||||
id="marker3948"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"><path
|
||||
orient="auto"
|
||||
viewBox="0 0 12.70584107 9.5264135"
|
||||
markerWidth="12.70584107"
|
||||
markerHeight="9.5264135"
|
||||
preserveAspectRatio="xMidYMid"><path
|
||||
transform="matrix(1.1,0,0,1.1,1.1,0)"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
@@ -249,7 +257,11 @@
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker4856"
|
||||
style="overflow:visible"><path
|
||||
style="overflow:visible"
|
||||
viewBox="0 0 17.77385393 10.15648796"
|
||||
markerWidth="17.77385393"
|
||||
markerHeight="10.15648796"
|
||||
preserveAspectRatio="xMidYMid"><path
|
||||
id="path4854"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
@@ -329,7 +341,25 @@
|
||||
y1="117.03271"
|
||||
x2="136.77219"
|
||||
y2="117.03271"
|
||||
gradientUnits="userSpaceOnUse" /></defs><metadata
|
||||
gradientUnits="userSpaceOnUse" /><marker
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker7593-1"
|
||||
style="overflow:visible"><path
|
||||
id="path7591-7"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
transform="matrix(1.1,0,0,1.1,1.1,0)" /></marker><marker
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker5072-2"
|
||||
style="overflow:visible"><path
|
||||
id="path5070-3"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
|
||||
transform="matrix(-0.8,0,0,-0.8,-10,0)" /></marker></defs><metadata
|
||||
id="metadata5"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><path
|
||||
@@ -362,7 +392,7 @@
|
||||
style="fill:none;stroke:#000000;stroke-width:0.743595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
|
||||
id="path4542"
|
||||
d="M 77.321236,58.014043 H 133.53382"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.682912;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker4186);marker-end:url(#marker4584)" /><path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.65;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker4186);marker-end:url(#marker4584)" /><path
|
||||
id="path4548"
|
||||
d="M 41.274623,258.30918 H 166.88345"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.721845;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker3948);marker-end:url(#marker4328)" /><text
|
||||
@@ -391,35 +421,46 @@
|
||||
id="path4538-8"
|
||||
d="M 38.74436,270.09124 V 164.05264"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.743595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
|
||||
id="path4538-8-9"
|
||||
d="m 93.265958,250.35549 0,-23.30956"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.743595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc" /><path
|
||||
id="path4538-8-9-3"
|
||||
d="M 114.22859,250.35549 V 227.04593"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.743595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc" /><path
|
||||
id="path4548-6"
|
||||
d="m 175.2226,160.40007 h 18.38097"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.592962;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
|
||||
d="m 118.72725,223.34176 74.87632,0"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.592962;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc" /><path
|
||||
id="path4548-6-8"
|
||||
d="m 132.26064,181.33547 39.1751,40.80318"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.757536;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker4186)"
|
||||
d="m 132.26064,182.39381 39.1751,40.80318"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.65;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker4186)"
|
||||
sodipodi:nodetypes="cc" /><path
|
||||
id="sesr3u8"
|
||||
d="M 184.26391,158.38661 V 136.96645"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.66145833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker7593);marker-end:url(#marker5072)" /><text
|
||||
d="m 184.26391,221.22509 0,-84.25864"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.65;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker7593);marker-end:url(#marker4584)"
|
||||
sodipodi:nodetypes="cc" /><text
|
||||
transform="scale(0.97096033,1.0299082)"
|
||||
id="cutting_edge_height"
|
||||
y="123.2775"
|
||||
x="180.79047"
|
||||
y="180.48601"
|
||||
x="191.77032"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28.2222px;line-height:1.25;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.28362"
|
||||
xml:space="preserve"><tspan
|
||||
y="123.2775"
|
||||
x="180.79047"
|
||||
y="180.48601"
|
||||
x="191.77032"
|
||||
id="tspan7855"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28.2222px;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal">h</tspan></text><path
|
||||
id="path936"
|
||||
d="M 21.764217,82.083367 H 72.731009"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.987384;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
|
||||
id="path938"
|
||||
d="m 19.64521,223.34176 h 83.99998"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.757536;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
|
||||
d="m 21.764217,223.34176 66.827826,0"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.757536;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc" /><path
|
||||
id="path940"
|
||||
d="M 28.028504,219.65219 V 84.84232"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.17078;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker7593);marker-end:url(#marker5072)" /><text
|
||||
style="fill:none;stroke:#000000;stroke-width:0.65;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker7593);marker-end:url(#marker4584)" /><text
|
||||
transform="scale(0.97096033,1.0299082)"
|
||||
id="length"
|
||||
y="153.26979"
|
||||
@@ -435,11 +476,25 @@
|
||||
style="fill:none;stroke:#000000;stroke-width:0.592962;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><text
|
||||
transform="scale(0.97096033,1.0299082)"
|
||||
id="cutting_edge_height-7"
|
||||
y="217.33482"
|
||||
x="144.21545"
|
||||
y="195.87868"
|
||||
x="156.38969"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28.2222px;line-height:1.25;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.28362"
|
||||
xml:space="preserve"><tspan
|
||||
y="217.33482"
|
||||
x="144.21545"
|
||||
y="195.87868"
|
||||
x="156.38969"
|
||||
id="tspan7855-5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28.2222px;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal">r</tspan></text></svg>
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28.2222px;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal">r</tspan></text><text
|
||||
transform="scale(0.97096033,1.0299082)"
|
||||
id="diameter-9"
|
||||
y="244.40749"
|
||||
x="70.96611"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32.286px;line-height:1.25;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.02681"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32.286px;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:3.02681"
|
||||
y="244.40749"
|
||||
x="70.96611"
|
||||
id="tspan5690-9">d</tspan></text><path
|
||||
id="sesr3u8-6"
|
||||
d="m 112.39408,239.97586 -17.090908,0"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.65;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#marker3948);marker-end:url(#marker4584)"
|
||||
sodipodi:nodetypes="cc" /></svg>
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user