CAM: Replace complete tool management (PR 21425)
This commit is contained in:
48
src/Mod/CAM/Path/Tool/shape/__init__.py
Normal file
48
src/Mod/CAM/Path/Tool/shape/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This package aggregates tool bit shape classes.
|
||||
|
||||
# Import the base class and all concrete shape classes
|
||||
from .models.base import ToolBitShape
|
||||
from .models.ballend import ToolBitShapeBallend
|
||||
from .models.chamfer import ToolBitShapeChamfer
|
||||
from .models.dovetail import ToolBitShapeDovetail
|
||||
from .models.drill import ToolBitShapeDrill
|
||||
from .models.endmill import ToolBitShapeEndmill
|
||||
from .models.fillet import ToolBitShapeFillet
|
||||
from .models.probe import ToolBitShapeProbe
|
||||
from .models.reamer import ToolBitShapeReamer
|
||||
from .models.slittingsaw import ToolBitShapeSlittingSaw
|
||||
from .models.tap import ToolBitShapeTap
|
||||
from .models.threadmill import ToolBitShapeThreadMill
|
||||
from .models.bullnose import ToolBitShapeBullnose
|
||||
from .models.vbit import ToolBitShapeVBit
|
||||
from .models.icon import (
|
||||
ToolBitShapeIcon,
|
||||
ToolBitShapePngIcon,
|
||||
ToolBitShapeSvgIcon,
|
||||
)
|
||||
|
||||
# A list of the name of each ToolBitShape
|
||||
TOOL_BIT_SHAPE_NAMES = sorted([cls.name for cls in ToolBitShape.__subclasses__()])
|
||||
|
||||
# Define __all__ for explicit public interface
|
||||
__all__ = [
|
||||
"ToolBitShape",
|
||||
"ToolBitShapeBallend",
|
||||
"ToolBitShapeChamfer",
|
||||
"ToolBitShapeDovetail",
|
||||
"ToolBitShapeDrill",
|
||||
"ToolBitShapeEndmill",
|
||||
"ToolBitShapeFillet",
|
||||
"ToolBitShapeProbe",
|
||||
"ToolBitShapeReamer",
|
||||
"ToolBitShapeSlittingSaw",
|
||||
"ToolBitShapeTap",
|
||||
"ToolBitShapeThreadMill",
|
||||
"ToolBitShapeBullnose",
|
||||
"ToolBitShapeVBit",
|
||||
"TOOL_BIT_SHAPE_NAMES",
|
||||
"ToolBitShapeIcon",
|
||||
"ToolBitShapeSvgIcon",
|
||||
"ToolBitShapePngIcon",
|
||||
]
|
||||
189
src/Mod/CAM/Path/Tool/shape/doc.py
Normal file
189
src/Mod/CAM/Path/Tool/shape/doc.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 Path.Base.Util as PathUtil
|
||||
from typing import Dict, List, Any, Optional
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
def find_shape_object(doc: "FreeCAD.Document") -> Optional["FreeCAD.DocumentObject"]:
|
||||
"""
|
||||
Find the primary object representing the shape in a document.
|
||||
|
||||
Looks for PartDesign::Body, then Part::Feature. Falls back to the first
|
||||
object if no better candidate is found.
|
||||
|
||||
Args:
|
||||
doc (FreeCAD.Document): The document to search within.
|
||||
|
||||
Returns:
|
||||
Optional[FreeCAD.DocumentObject]: The found object or None.
|
||||
"""
|
||||
obj = None
|
||||
# Prioritize Body
|
||||
for o in doc.Objects:
|
||||
if o.isDerivedFrom("PartDesign::Body"):
|
||||
return o
|
||||
# Keep track of the first Part::Feature found as a fallback
|
||||
if obj is None and o.isDerivedFrom("Part::Feature"):
|
||||
obj = o
|
||||
if obj:
|
||||
return obj
|
||||
# Fallback to the very first object if nothing else suitable found
|
||||
return doc.Objects[0] if doc.Objects else None
|
||||
|
||||
|
||||
def get_object_properties(
|
||||
obj: "FreeCAD.DocumentObject", expected_params: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract properties matching expected_params from a FreeCAD PropertyBag.
|
||||
|
||||
Issues warnings for missing parameters but does not raise an error.
|
||||
|
||||
Args:
|
||||
obj: The PropertyBag to extract properties from.
|
||||
expected_params (List[str]): A list of property names to look for.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary mapping property names to their values.
|
||||
Values are FreeCAD native types.
|
||||
"""
|
||||
properties = {}
|
||||
for name in expected_params:
|
||||
if hasattr(obj, name):
|
||||
properties[name] = getattr(obj, name)
|
||||
else:
|
||||
# Log a warning if a parameter expected by the shape class is missing
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Parameter '{name}' not found on object '{obj.Label}' "
|
||||
f"({obj.Name}). Default value will be used by the shape class.\n"
|
||||
)
|
||||
properties[name] = None # Indicate missing value
|
||||
return properties
|
||||
|
||||
|
||||
def update_shape_object_properties(
|
||||
obj: "FreeCAD.DocumentObject", parameters: Dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Update properties of a FreeCAD PropertyBag based on a dictionary of parameters.
|
||||
|
||||
Args:
|
||||
obj (FreeCAD.DocumentObject): The PropertyBag to update properties on.
|
||||
parameters (Dict[str, Any]): A dictionary of property names and values.
|
||||
"""
|
||||
for name, value in parameters.items():
|
||||
if hasattr(obj, name):
|
||||
try:
|
||||
PathUtil.setProperty(obj, name, value)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Failed to set property '{name}' on object '{obj.Label}'"
|
||||
f" ({obj.Name}) with value '{value}': {e}\n"
|
||||
)
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Property '{name}' not found on object '{obj.Label}'" f" ({obj.Name}). Skipping.\n"
|
||||
)
|
||||
|
||||
|
||||
def get_doc_state() -> Any:
|
||||
"""
|
||||
Used to make a "snapshot" of the current state of FreeCAD, to allow
|
||||
for restoring the ActiveDocument and selection state later.
|
||||
"""
|
||||
doc_name = FreeCAD.ActiveDocument.Name if FreeCAD.ActiveDocument else None
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
|
||||
selection = FreeCADGui.Selection.getSelection()
|
||||
else:
|
||||
selection = []
|
||||
return doc_name, selection
|
||||
|
||||
|
||||
def restore_doc_state(state):
|
||||
doc_name, selection = state
|
||||
if doc_name:
|
||||
FreeCAD.setActiveDocument(doc_name)
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
|
||||
for sel in selection:
|
||||
FreeCADGui.Selection.addSelection(doc_name, sel.Name)
|
||||
|
||||
|
||||
class ShapeDocFromBytes:
|
||||
"""
|
||||
Context manager to create and manage a temporary FreeCAD document,
|
||||
loading content from a byte string.
|
||||
"""
|
||||
|
||||
def __init__(self, content: bytes):
|
||||
self._content = content
|
||||
self._doc = None
|
||||
self._temp_file = None
|
||||
self._old_state = None
|
||||
|
||||
def __enter__(self) -> "FreeCAD.Document":
|
||||
"""Creates a new temporary FreeCAD document or loads cache if provided."""
|
||||
# Create a temporary file and write the cache content to it
|
||||
with tempfile.NamedTemporaryFile(suffix=".FCStd", delete=False) as tmp_file:
|
||||
tmp_file.write(self._content)
|
||||
self._temp_file = tmp_file.name
|
||||
|
||||
# When we open a new document, FreeCAD loses the state, of the active
|
||||
# document (i.e. current selection), even if the newly opened document
|
||||
# is a hidden one.
|
||||
# So we need to restore the active document state at the end.
|
||||
self._old_state = get_doc_state()
|
||||
|
||||
# Open the document from the temporary file
|
||||
# Use a specific name to avoid clashes if multiple docs are open
|
||||
# Open the document from the temporary file
|
||||
self._doc = FreeCAD.openDocument(self._temp_file, hidden=True)
|
||||
if not self._doc:
|
||||
raise RuntimeError(f"Failed to open document from {self._temp_file}")
|
||||
return self._doc
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||
"""Closes the temporary FreeCAD document and cleans up the temp file."""
|
||||
if self._doc:
|
||||
# Note that .closeDocument() is extremely slow; it takes
|
||||
# almost 400ms per document - much longer than opening!
|
||||
FreeCAD.closeDocument(self._doc.Name)
|
||||
self._doc = None
|
||||
|
||||
# Restore the original active document
|
||||
restore_doc_state(self._old_state)
|
||||
|
||||
# Clean up the temporary file if it was created
|
||||
if self._temp_file and os.path.exists(self._temp_file):
|
||||
try:
|
||||
os.remove(self._temp_file)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Failed to remove temporary file {self._temp_file}: {e}\n"
|
||||
)
|
||||
1
src/Mod/CAM/Path/Tool/shape/models/__init__.py
Normal file
1
src/Mod/CAM/Path/Tool/shape/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
59
src/Mod/CAM/Path/Tool/shape/models/ballend.py
Normal file
59
src/Mod/CAM/Path/Tool/shape/models/ballend.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeBallend(ToolBitShape):
|
||||
name: str = "Ballend"
|
||||
aliases = ("ballend",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
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("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Ballend")
|
||||
630
src/Mod/CAM/Path/Tool/shape/models/base.py
Normal file
630
src/Mod/CAM/Path/Tool/shape/models/base.py
Normal file
@@ -0,0 +1,630 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 pathlib
|
||||
import FreeCAD
|
||||
import Path
|
||||
import os
|
||||
from typing import Dict, List, Any, Mapping, Optional, Tuple, Type, cast
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
import io
|
||||
import tempfile
|
||||
from ...assets import Asset, AssetUri, AssetSerializer, DummyAssetSerializer
|
||||
from ...camassets import cam_assets
|
||||
from ..doc import (
|
||||
find_shape_object,
|
||||
get_object_properties,
|
||||
update_shape_object_properties,
|
||||
ShapeDocFromBytes,
|
||||
)
|
||||
from .icon import ToolBitShapeIcon
|
||||
|
||||
|
||||
class ToolBitShape(Asset):
|
||||
"""Abstract base class for tool bit shapes."""
|
||||
|
||||
asset_type: str = "toolbitshape"
|
||||
|
||||
# The name is used...
|
||||
# 1. as a base for the default filename. E.g. if the name is
|
||||
# "Endmill", then by default the file is "endmill.fcstd".
|
||||
# 2. to identify the shape class from a shape.fcstd file.
|
||||
# Upon loading a shape, the name of the body in the shape
|
||||
# file is read. It MUST match one of the names.
|
||||
name: str
|
||||
|
||||
# Aliases exist for backward compatibility. If an existing .fctb file
|
||||
# references a shape such as "v-bit.fctb", and that shape file cannot
|
||||
# be found, then we can attempt to find a shape class from the string
|
||||
# "v-bit", "vbit", etc.
|
||||
aliases: Tuple[str, ...] = tuple()
|
||||
|
||||
def __init__(self, id: str, **kwargs: Any):
|
||||
"""
|
||||
Initialize the shape.
|
||||
|
||||
Args:
|
||||
id (str): The unique identifier for the shape.
|
||||
**kwargs: Keyword arguments for shape parameters (e.g., Diameter).
|
||||
Values should be FreeCAD.Units.Quantity where applicable.
|
||||
"""
|
||||
# _params will be populated with default values after loading
|
||||
self._params: Dict[str, Any] = {}
|
||||
|
||||
# Stores default parameter values loaded from the FCStd file
|
||||
self._defaults: Dict[str, Any] = {}
|
||||
|
||||
# Keeps the loaded FreeCAD document content for this instance
|
||||
self._data: Optional[bytes] = None
|
||||
|
||||
self.id: str = id
|
||||
|
||||
self.is_builtin: bool = True
|
||||
|
||||
self.icon: Optional[ToolBitShapeIcon] = None
|
||||
|
||||
# Assign parameters
|
||||
for param, value in kwargs.items():
|
||||
self.set_parameter(param, value)
|
||||
|
||||
def __str__(self):
|
||||
params_str = ", ".join(f"{name}={val}" for name, val in self._params.items())
|
||||
return f"{self.name}({params_str})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def get_id(self) -> str:
|
||||
"""
|
||||
Get the ID of the shape.
|
||||
|
||||
Returns:
|
||||
str: The ID of the shape.
|
||||
"""
|
||||
return self.id
|
||||
|
||||
@classmethod
|
||||
def _get_shape_class_from_doc(cls, doc: "FreeCAD.Document") -> Type["ToolBitShape"]:
|
||||
# Find the Body object to identify the shape type
|
||||
body_obj = find_shape_object(doc)
|
||||
if not body_obj:
|
||||
raise ValueError(f"No 'PartDesign::Body' object found in {doc}")
|
||||
|
||||
# Find the correct subclass based on the body label
|
||||
shape_classes = {c.name: c for c in ToolBitShape.__subclasses__()}
|
||||
shape_class = shape_classes.get(body_obj.Label)
|
||||
if not shape_class:
|
||||
raise ValueError(
|
||||
f"No ToolBitShape subclass found matching Body label '{body_obj.Label}' in {doc}"
|
||||
)
|
||||
return shape_class
|
||||
|
||||
@classmethod
|
||||
def get_shape_class_from_bytes(cls, data: bytes) -> Type["ToolBitShape"]:
|
||||
"""
|
||||
Identifies the ToolBitShape subclass from the raw bytes of an FCStd file
|
||||
by parsing the XML content to find the Body label.
|
||||
|
||||
Args:
|
||||
data (bytes): The raw bytes of the .FCStd file.
|
||||
|
||||
Returns:
|
||||
Type[ToolBitShape]: The appropriate ToolBitShape subclass.
|
||||
|
||||
Raises:
|
||||
ValueError: If the data is not a valid FCStd file, Document.xml is
|
||||
missing, no Body object is found, or the Body label
|
||||
does not match a known shape name.
|
||||
"""
|
||||
try:
|
||||
# FCStd files are zip archives
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as zf:
|
||||
# Read Document.xml from the archive
|
||||
with zf.open("Document.xml") as doc_xml_file:
|
||||
tree = ET.parse(doc_xml_file)
|
||||
root = tree.getroot()
|
||||
|
||||
# Extract name of the main Body from XML tree using xpath.
|
||||
# The body should be a PartDesign::Body, and its label is
|
||||
# stored in an Property element with a matching name.
|
||||
body_label = None
|
||||
xpath = './/Object[@name="Body"]//Property[@name="Label"]/String'
|
||||
body_label_elem = root.find(xpath)
|
||||
if body_label_elem is not None:
|
||||
body_label = body_label_elem.get("value")
|
||||
|
||||
if not body_label:
|
||||
raise ValueError(
|
||||
"No 'Label' property found for 'PartDesign::Body' object using XPath"
|
||||
)
|
||||
|
||||
# Find the correct subclass based on the body label
|
||||
shape_class = cls.get_subclass_by_name(body_label)
|
||||
if not shape_class:
|
||||
raise ValueError(
|
||||
f"No ToolBitShape subclass found matching Body label '{body_label}'"
|
||||
)
|
||||
return shape_class
|
||||
|
||||
except zipfile.BadZipFile:
|
||||
raise ValueError("Invalid FCStd file data (not a valid zip archive)")
|
||||
except KeyError:
|
||||
raise ValueError("Invalid FCStd file data (Document.xml not found)")
|
||||
except ET.ParseError:
|
||||
raise ValueError("Error parsing Document.xml")
|
||||
except Exception as e:
|
||||
# Catch any other unexpected errors during parsing
|
||||
raise ValueError(f"Error processing FCStd data: {e}")
|
||||
|
||||
@classmethod
|
||||
def _find_property_object(cls, doc: "FreeCAD.Document") -> Optional["FreeCAD.DocumentObject"]:
|
||||
"""
|
||||
Find the PropertyBag object named "Attributes" in a document.
|
||||
|
||||
Args:
|
||||
doc (FreeCAD.Document): The document to search within.
|
||||
|
||||
Returns:
|
||||
Optional[FreeCAD.DocumentObject]: The found object or None.
|
||||
"""
|
||||
for o in doc.Objects:
|
||||
# Check if the object has a Label property and if its value is "Attributes"
|
||||
# This seems to be the convention in the shape files.
|
||||
if hasattr(o, "Label") and o.Label == "Attributes":
|
||||
# We assume this object holds the parameters.
|
||||
# Further type checking (e.g., for App::FeaturePython or PropertyBag)
|
||||
# could be added if needed, but Label check might be sufficient.
|
||||
return o
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def extract_dependencies(cls, data: bytes, serializer: Type[AssetSerializer]) -> List[AssetUri]:
|
||||
"""
|
||||
Extracts URIs of dependencies from the raw bytes of an FCStd file.
|
||||
For ToolBitShape, this is the associated ToolBitShapeIcon, identified
|
||||
by the same ID as the shape asset.
|
||||
"""
|
||||
Path.Log.debug(f"ToolBitShape.extract_dependencies called for {cls.__name__}")
|
||||
assert (
|
||||
serializer == DummyAssetSerializer
|
||||
), f"ToolBitShape supports only native import, not {serializer}"
|
||||
|
||||
# A ToolBitShape asset depends on a ToolBitShapeIcon asset with the same ID.
|
||||
# We need to extract the shape ID from the FCStd data.
|
||||
try:
|
||||
# Open the shape data temporarily to get the Body label, which can
|
||||
# be used to derive the ID if needed, or assume the ID is available
|
||||
# in the data somehow (e.g., in a property).
|
||||
# For now, let's assume the ID is implicitly the asset name derived
|
||||
# from the Body label.
|
||||
shape_class = cls.get_shape_class_from_bytes(data)
|
||||
shape_id = shape_class.name.lower() # Assuming ID is lowercase name
|
||||
|
||||
# Construct the URI for the corresponding icon asset
|
||||
svg_uri = AssetUri.build(
|
||||
asset_type="toolbitshapesvg",
|
||||
asset_id=shape_id + ".svg",
|
||||
)
|
||||
png_uri = AssetUri.build(
|
||||
asset_type="toolbitshapepng",
|
||||
asset_id=shape_id + ".png",
|
||||
)
|
||||
return [svg_uri, png_uri]
|
||||
|
||||
except Exception as e:
|
||||
# If we can't extract the shape ID or something goes wrong,
|
||||
# assume no dependencies for now.
|
||||
Path.Log.error(f"Failed to extract dependencies from shape data: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls,
|
||||
data: bytes,
|
||||
id: str,
|
||||
dependencies: Optional[Mapping[AssetUri, Asset]],
|
||||
serializer: Type[AssetSerializer],
|
||||
) -> "ToolBitShape":
|
||||
"""
|
||||
Create a ToolBitShape instance from the raw bytes of an FCStd file.
|
||||
|
||||
Identifies the correct subclass based on the Body label in the file,
|
||||
loads parameters, and caches the document content.
|
||||
|
||||
Args:
|
||||
data (bytes): The raw bytes of the .FCStd file.
|
||||
id (str): The unique identifier for the shape.
|
||||
dependencies (Optional[Mapping[AssetUri, Any]]): A mapping of
|
||||
resolved dependencies. If None, shallow load was attempted.
|
||||
|
||||
Returns:
|
||||
ToolBitShape: An instance of the appropriate ToolBitShape subclass.
|
||||
|
||||
Raises:
|
||||
ValueError: If the data cannot be opened, no Body or PropertyBag
|
||||
is found, or the Body label does not match a known
|
||||
shape name.
|
||||
Exception: For other potential FreeCAD errors during loading.
|
||||
"""
|
||||
assert serializer == DummyAssetSerializer, "ToolBitShape supports only native import"
|
||||
|
||||
# Open the shape data temporarily to get the Body label and parameters
|
||||
with ShapeDocFromBytes(data) as temp_doc:
|
||||
if not temp_doc:
|
||||
# This case might be covered by ShapeDocFromBytes exceptions,
|
||||
# but keeping for clarity.
|
||||
raise ValueError("Failed to open shape document from bytes")
|
||||
|
||||
# Determine the specific subclass of ToolBitShape using the new method
|
||||
shape_class = ToolBitShape.get_shape_class_from_bytes(data)
|
||||
|
||||
# Load properties from the temporary document
|
||||
props_obj = ToolBitShape._find_property_object(temp_doc)
|
||||
if not props_obj:
|
||||
raise ValueError("No 'Attributes' PropertyBag object found in document bytes")
|
||||
|
||||
# Get properties from the properties object
|
||||
expected_params = shape_class.get_expected_shape_parameters()
|
||||
loaded_params = get_object_properties(props_obj, expected_params)
|
||||
|
||||
missing_params = [
|
||||
name
|
||||
for name in expected_params
|
||||
if name not in loaded_params or loaded_params[name] is None
|
||||
]
|
||||
|
||||
if missing_params:
|
||||
raise ValueError(
|
||||
f"Validation error: Object '{props_obj.Label}' in document bytes "
|
||||
+ f"is missing parameters for {shape_class.__name__}: {', '.join(missing_params)}"
|
||||
)
|
||||
|
||||
# Instantiate the specific subclass with the provided ID
|
||||
instance = shape_class(id=id)
|
||||
instance._data = data # Cache the byte content
|
||||
instance._defaults = loaded_params
|
||||
|
||||
if dependencies: # dependencies is None = shallow load
|
||||
# Assign resolved dependencies (like the icon) to the instance
|
||||
# The icon has the same ID as the shape, with .png or .svg appended.
|
||||
icon_uri = AssetUri.build(
|
||||
asset_type="toolbitshapesvg",
|
||||
asset_id=id + ".svg",
|
||||
)
|
||||
instance.icon = cast(ToolBitShapeIcon, dependencies.get(icon_uri))
|
||||
if not instance.icon:
|
||||
icon_uri = AssetUri.build(
|
||||
asset_type="toolbitshapepng",
|
||||
asset_id=id + ".png",
|
||||
)
|
||||
instance.icon = cast(ToolBitShapeIcon, dependencies.get(icon_uri))
|
||||
|
||||
# Update instance parameters, prioritizing loaded defaults but not
|
||||
# overwriting parameters that may already be set during __init__
|
||||
instance._params = instance._defaults | instance._params
|
||||
|
||||
return instance
|
||||
|
||||
def to_bytes(self, serializer: Type[AssetSerializer]) -> bytes:
|
||||
"""
|
||||
Serializes a ToolBitShape object to bytes (e.g., an fcstd file).
|
||||
This is required by the Asset interface.
|
||||
"""
|
||||
assert serializer == DummyAssetSerializer, "ToolBitShape supports only native export"
|
||||
doc = None
|
||||
try:
|
||||
# Create a new temporary document
|
||||
doc = FreeCAD.newDocument("TemporaryShapeDoc", hidden=True)
|
||||
|
||||
# Add the shape's body to the temporary document
|
||||
self.make_body(doc)
|
||||
|
||||
# Recompute the document to ensure the body is created
|
||||
doc.recompute()
|
||||
|
||||
# Save the temporary document to a temporary file
|
||||
# We cannot use NamedTemporaryFile on Windows, because there
|
||||
# doc.saveAs() may not have permission to access the tempfile
|
||||
# while the NamedTemporaryFile is open.
|
||||
# So we use TemporaryDirectory instead, to ensure cleanup while
|
||||
# still having a the temporary file inside it.
|
||||
with tempfile.TemporaryDirectory() as thedir:
|
||||
temp_file_path = pathlib.Path(thedir, "temp.FCStd")
|
||||
doc.saveAs(str(temp_file_path))
|
||||
return temp_file_path.read_bytes()
|
||||
|
||||
finally:
|
||||
# Clean up the temporary document
|
||||
if doc:
|
||||
FreeCAD.closeDocument(doc.Name)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filepath: pathlib.Path, **kwargs: Any) -> "ToolBitShape":
|
||||
"""
|
||||
Create a ToolBitShape instance from an FCStd file.
|
||||
|
||||
Reads the file bytes and delegates to from_bytes().
|
||||
|
||||
Args:
|
||||
filepath (pathlib.Path): Path to the .FCStd file.
|
||||
**kwargs: Keyword arguments for shape parameters to override defaults.
|
||||
|
||||
Returns:
|
||||
ToolBitShape: An instance of the appropriate ToolBitShape subclass.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file does not exist.
|
||||
ValueError: If the file cannot be opened, no Body or PropertyBag
|
||||
is found, or the Body label does not match a known
|
||||
shape name.
|
||||
Exception: For other potential FreeCAD errors during loading.
|
||||
"""
|
||||
if not filepath.exists():
|
||||
raise FileNotFoundError(f"Shape file not found: {filepath}")
|
||||
|
||||
try:
|
||||
data = filepath.read_bytes()
|
||||
# Extract the ID from the filename (without extension)
|
||||
shape_id = filepath.stem
|
||||
# Pass an empty dictionary for dependencies when loading from a single file
|
||||
# TODO: pass ToolBitShapeIcon as a dependency
|
||||
instance = cls.from_bytes(data, shape_id, {}, DummyAssetSerializer)
|
||||
# Apply kwargs parameters after loading from bytes
|
||||
if kwargs:
|
||||
instance.set_parameters(**kwargs)
|
||||
return instance
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to create shape from {filepath}: {e}")
|
||||
|
||||
@classmethod
|
||||
def get_subclass_by_name(
|
||||
cls, name: str, default: Type["ToolBitShape"] | None = None
|
||||
) -> Optional[Type["ToolBitShape"]]:
|
||||
"""
|
||||
Retrieves a ToolBitShape class by its name or alias.
|
||||
"""
|
||||
name = name.lower()
|
||||
for thecls in cls.__subclasses__():
|
||||
if (
|
||||
thecls.name.lower() == name
|
||||
or thecls.__name__.lower() == name
|
||||
or name in thecls.aliases
|
||||
):
|
||||
return thecls
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def resolve_name(cls, identifier: str) -> AssetUri:
|
||||
"""
|
||||
Resolves an identifier (alias, name, filename, or URI) to a Uri object.
|
||||
"""
|
||||
# 1. If the input is a url string, return the AssetUri for it.
|
||||
if AssetUri.is_uri(identifier):
|
||||
return AssetUri(identifier)
|
||||
|
||||
# 2. If the input is a filename (with extension), assume the asset
|
||||
# name is the base name.
|
||||
asset_name = identifier
|
||||
if identifier.endswith(".fcstd"):
|
||||
asset_name = os.path.splitext(os.path.basename(identifier))[0]
|
||||
|
||||
# 3. Use get_subclass_by_name to try to resolve alias to a class.
|
||||
# if one is found, use the class.name.
|
||||
shape_class = cls.get_subclass_by_name(asset_name.lower())
|
||||
if shape_class:
|
||||
asset_name = shape_class.name.lower()
|
||||
|
||||
# 4. Construct the Uri using AssetUri.build() and return it
|
||||
return AssetUri.build(
|
||||
asset_type="toolbitshape",
|
||||
asset_id=asset_name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
"""
|
||||
Subclasses must define the dictionary mapping parameter names to
|
||||
translations and FreeCAD property type strings (e.g.,
|
||||
'App::PropertyLength').
|
||||
|
||||
The schema defines any parameters that MUST be in the shape file.
|
||||
Any attempt to load a shape file that does not match the schema
|
||||
will cause an error.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
"""Return a user friendly, translatable display name."""
|
||||
raise NotImplementedError
|
||||
|
||||
def reset_parameters(self):
|
||||
"""Reset parameters to their default values."""
|
||||
self._params.update(self._defaults)
|
||||
|
||||
def get_parameter_label(self, param_name: str) -> str:
|
||||
"""
|
||||
Get the user-facing label for a given parameter name.
|
||||
"""
|
||||
str_param_name = str(param_name)
|
||||
entry = self.schema().get(param_name)
|
||||
return entry[0] if entry else str_param_name
|
||||
|
||||
def get_parameter_property_type(self, param_name: str) -> str:
|
||||
"""
|
||||
Get the FreeCAD property type string for a given parameter name.
|
||||
"""
|
||||
return self.schema()[param_name][1]
|
||||
|
||||
def get_parameters(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the dictionary of current parameters and their values.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping parameter names to their values.
|
||||
"""
|
||||
return self._params
|
||||
|
||||
def get_parameter(self, name: str) -> Any:
|
||||
"""
|
||||
Get the value of a specific parameter.
|
||||
|
||||
Args:
|
||||
name (str): The name of the parameter.
|
||||
|
||||
Returns:
|
||||
The value of the parameter (often a FreeCAD.Units.Quantity).
|
||||
|
||||
Raises:
|
||||
KeyError: If the parameter name is not valid for this shape.
|
||||
"""
|
||||
if name not in self.schema():
|
||||
raise KeyError(f"Shape '{self.name}' has no parameter '{name}'")
|
||||
return self._params[name]
|
||||
|
||||
def set_parameter(self, name: str, value: Any):
|
||||
"""
|
||||
Set the value of a specific parameter.
|
||||
|
||||
Args:
|
||||
name (str): The name of the parameter.
|
||||
value: The new value for the parameter. Should be compatible
|
||||
with the expected type (e.g., FreeCAD.Units.Quantity).
|
||||
|
||||
Raises:
|
||||
KeyError: If the parameter name is not valid for this shape.
|
||||
"""
|
||||
if name not in self.schema().keys():
|
||||
Path.Log.debug(
|
||||
f"Shape '{self.name}' was given an invalid parameter '{name}'. Has {self._params}\n"
|
||||
)
|
||||
# Log to confirm this path is taken when an invalid parameter is given
|
||||
Path.Log.debug(
|
||||
f"Invalid parameter '{name}' for shape "
|
||||
f"'{self.name}', returning without raising KeyError."
|
||||
)
|
||||
return
|
||||
|
||||
self._params[name] = value
|
||||
|
||||
def set_parameters(self, **kwargs):
|
||||
"""
|
||||
Set multiple parameters using keyword arguments.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments where keys are parameter names.
|
||||
"""
|
||||
for name, value in kwargs.items():
|
||||
try:
|
||||
self.set_parameter(name, value)
|
||||
except KeyError:
|
||||
Path.Log.debug(f"Ignoring unknown parameter '{name}' for shape '{self.name}'.\n")
|
||||
|
||||
@classmethod
|
||||
def get_expected_shape_parameters(cls) -> List[str]:
|
||||
"""
|
||||
Get a list of parameter names expected by this shape class based on
|
||||
its schema.
|
||||
|
||||
Returns:
|
||||
list[str]: List of parameter names.
|
||||
"""
|
||||
return list(cls.schema().keys())
|
||||
|
||||
def make_body(self, doc: "FreeCAD.Document"):
|
||||
"""
|
||||
Generates the body of the ToolBitShape and copies it to the provided
|
||||
document.
|
||||
"""
|
||||
assert self._data is not None
|
||||
with ShapeDocFromBytes(self._data) as tmp_doc:
|
||||
shape = find_shape_object(tmp_doc)
|
||||
if not shape:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"No suitable shape object found in document. " "Cannot create solid shape.\n"
|
||||
)
|
||||
return None
|
||||
|
||||
props = self._find_property_object(tmp_doc)
|
||||
if not props:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"No suitable shape object found in document. " "Cannot create solid shape.\n"
|
||||
)
|
||||
return None
|
||||
|
||||
update_shape_object_properties(props, self.get_parameters())
|
||||
|
||||
# Recompute the document to apply property changes
|
||||
tmp_doc.recompute()
|
||||
|
||||
# Copy the body to the given document without immediate compute.
|
||||
return doc.copyObject(shape, True)
|
||||
|
||||
"""
|
||||
Retrieves the thumbnail data for the tool bit shape in PNG format.
|
||||
"""
|
||||
|
||||
def get_icon(self) -> Optional[ToolBitShapeIcon]:
|
||||
"""
|
||||
Get the associated ToolBitShapeIcon instance. Tries to load one from
|
||||
the asset manager if none was assigned.
|
||||
|
||||
Returns:
|
||||
Optional[ToolBitShapeIcon]: The icon instance, or None if none found.
|
||||
"""
|
||||
if self.icon:
|
||||
return self.icon
|
||||
|
||||
# Try to get a matching SVG from the asset manager.
|
||||
self.icon = cam_assets.get_or_none(f"toolbitshapesvg://{self.id}.svg")
|
||||
if self.icon:
|
||||
return self.icon
|
||||
self.icon = cam_assets.get_or_none(f"toolbitshapesvg://{self.name.lower()}.svg")
|
||||
if self.icon:
|
||||
return self.icon
|
||||
|
||||
# Try to get a matching PNG from the asset manager.
|
||||
self.icon = cam_assets.get_or_none(f"toolbitshapepng://{self.id}.png")
|
||||
if self.icon:
|
||||
return self.icon
|
||||
self.icon = cam_assets.get_or_none(f"toolbitshapepng://{self.name.lower()}.png")
|
||||
if self.icon:
|
||||
return self.icon
|
||||
return None
|
||||
|
||||
def get_thumbnail(self) -> Optional[bytes]:
|
||||
"""
|
||||
Retrieves the thumbnail data for the tool bit shape in PNG format,
|
||||
as embedded in the shape file.
|
||||
"""
|
||||
if not self._data:
|
||||
return None
|
||||
with zipfile.ZipFile(io.BytesIO(self._data)) as zf:
|
||||
try:
|
||||
with zf.open("thumbnails/Thumbnail.png", "r") as tn:
|
||||
return tn.read()
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
63
src/Mod/CAM/Path/Tool/shape/models/bullnose.py
Normal file
63
src/Mod/CAM/Path/Tool/shape/models/bullnose.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeBullnose(ToolBitShape):
|
||||
name = "Bullnose"
|
||||
aliases = "bullnose", "torus"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
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("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"FlatRadius": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Torus radius"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Torus")
|
||||
63
src/Mod/CAM/Path/Tool/shape/models/chamfer.py
Normal file
63
src/Mod/CAM/Path/Tool/shape/models/chamfer.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeChamfer(ToolBitShape):
|
||||
name = "Chamfer"
|
||||
aliases = ("chamfer",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"CuttingEdgeAngle": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting edge angle"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
"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("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Chamfer")
|
||||
75
src/Mod/CAM/Path/Tool/shape/models/dovetail.py
Normal file
75
src/Mod/CAM/Path/Tool/shape/models/dovetail.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeDovetail(ToolBitShape):
|
||||
name = "Dovetail"
|
||||
aliases = ("dovetail",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"TipDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Crest height"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"CuttingEdgeAngle": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting angle"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
"CuttingEdgeHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Dovetail height"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Major diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Flutes": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Flutes"),
|
||||
"App::PropertyInteger",
|
||||
),
|
||||
"Length": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Overall tool length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"NeckDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Neck diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"NeckHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Neck length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"ShankDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Dovetail")
|
||||
55
src/Mod/CAM/Path/Tool/shape/models/drill.py
Normal file
55
src/Mod/CAM/Path/Tool/shape/models/drill.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeDrill(ToolBitShape):
|
||||
name = "Drill"
|
||||
aliases = ("drill",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"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",
|
||||
),
|
||||
"TipAngle": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Tip angle"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("CAM", "Drill")
|
||||
59
src/Mod/CAM/Path/Tool/shape/models/endmill.py
Normal file
59
src/Mod/CAM/Path/Tool/shape/models/endmill.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeEndmill(ToolBitShape):
|
||||
name = "Endmill"
|
||||
aliases = ("endmill",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
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:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Endmill")
|
||||
63
src/Mod/CAM/Path/Tool/shape/models/fillet.py
Normal file
63
src/Mod/CAM/Path/Tool/shape/models/fillet.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeFillet(ToolBitShape):
|
||||
name = "Fillet"
|
||||
aliases = ("fillet",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"CrownHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Crown height"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"FilletRadius": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Fillet radius"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Flutes": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Flutes"),
|
||||
"App::PropertyInteger",
|
||||
),
|
||||
"Length": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Overall tool length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"ShankDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Filleted Chamfer")
|
||||
296
src/Mod/CAM/Path/Tool/shape/models/icon.py
Normal file
296
src/Mod/CAM/Path/Tool/shape/models/icon.py
Normal file
@@ -0,0 +1,296 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 pathlib
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Mapping, Optional
|
||||
from functools import cached_property
|
||||
from ...assets import Asset, AssetUri, AssetSerializer, DummyAssetSerializer
|
||||
import Path.Tool.shape.util as util
|
||||
from PySide import QtCore, QtGui, QtSvg
|
||||
|
||||
_svg_ns = {"s": "http://www.w3.org/2000/svg"}
|
||||
|
||||
|
||||
class ToolBitShapeIcon(Asset):
|
||||
"""Abstract base class for tool bit shape icons."""
|
||||
|
||||
def __init__(self, id: str, data: bytes):
|
||||
"""
|
||||
Initialize the icon.
|
||||
|
||||
Args:
|
||||
id (str): The unique identifier for the icon, including extension.
|
||||
data (bytes): The raw icon data (e.g., SVG or PNG bytes).
|
||||
"""
|
||||
self.id: str = id
|
||||
self.data: bytes = data
|
||||
|
||||
def get_id(self) -> str:
|
||||
"""
|
||||
Get the ID of the icon.
|
||||
|
||||
Returns:
|
||||
str: The ID of the icon.
|
||||
"""
|
||||
return self.id
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls,
|
||||
data: bytes,
|
||||
id: str,
|
||||
dependencies: Optional[Mapping[AssetUri, Asset]],
|
||||
serializer: AssetSerializer,
|
||||
) -> "ToolBitShapeIcon":
|
||||
"""
|
||||
Create a ToolBitShapeIcon instance from raw bytes.
|
||||
|
||||
Args:
|
||||
data (bytes): The raw bytes of the icon file.
|
||||
id (str): The ID of the asset, including extension.
|
||||
dependencies (Optional[Mapping[AssetUri, Asset]]): A mapping of resolved dependencies (not used for icons).
|
||||
|
||||
Returns:
|
||||
ToolBitShapeIcon: An instance of ToolBitShapeIcon.
|
||||
"""
|
||||
assert serializer == DummyAssetSerializer, "ToolBitShapeIcon supports only native import"
|
||||
return cls(id=id, data=data)
|
||||
|
||||
def to_bytes(self, serializer: AssetSerializer) -> bytes:
|
||||
"""
|
||||
Serializes a ToolBitShapeIcon object to bytes.
|
||||
"""
|
||||
assert serializer == DummyAssetSerializer, "ToolBitShapeIcon supports only native export"
|
||||
return self.data
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filepath: pathlib.Path, id: str) -> "ToolBitShapeIcon":
|
||||
"""
|
||||
Create a ToolBitShapeIcon instance from a file.
|
||||
|
||||
Args:
|
||||
filepath (pathlib.Path): Path to the icon file (.svg or .png).
|
||||
shape_id_base (str): The base ID of the associated shape.
|
||||
|
||||
Returns:
|
||||
ToolBitShapeIcon: An instance of ToolBitShapeIcon.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file does not exist.
|
||||
"""
|
||||
if not filepath.exists():
|
||||
raise FileNotFoundError(f"Icon file not found: {filepath}")
|
||||
|
||||
data = filepath.read_bytes()
|
||||
if filepath.suffix.lower() == ".png":
|
||||
return ToolBitShapePngIcon(id, data)
|
||||
elif filepath.suffix.lower() == ".svg":
|
||||
return ToolBitShapeSvgIcon(id, data)
|
||||
else:
|
||||
raise NotImplementedError(f"unsupported icon file: {filepath}")
|
||||
|
||||
@classmethod
|
||||
def from_shape_data(cls, shape_data: bytes, id: str) -> Optional["ToolBitShapeIcon"]:
|
||||
"""
|
||||
Create a thumbnail icon from shape data bytes.
|
||||
|
||||
Args:
|
||||
shape_data (bytes): The raw bytes of the shape file (.FCStd).
|
||||
shape_id_base (str): The base ID of the associated shape.
|
||||
|
||||
Returns:
|
||||
Optional[ToolBitShapeIcon]: An instance of ToolBitShapeIcon (PNG), or None.
|
||||
"""
|
||||
image_bytes = util.create_thumbnail_from_data(shape_data)
|
||||
if not image_bytes:
|
||||
return None
|
||||
|
||||
# Assuming create_thumbnail_from_data returns PNG data
|
||||
return ToolBitShapePngIcon(id=id, data=image_bytes)
|
||||
|
||||
def get_size_in_bytes(self) -> int:
|
||||
"""
|
||||
Get the size of the icon data in bytes.
|
||||
"""
|
||||
return len(self.data)
|
||||
|
||||
@cached_property
|
||||
def abbreviations(self) -> Mapping[str, str]:
|
||||
"""
|
||||
Returns a cached mapping of parameter abbreviations from the icon data.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_abbr(self, param_name: str) -> Optional[str]:
|
||||
"""
|
||||
Retrieves the abbreviation for a given parameter name.
|
||||
|
||||
Args:
|
||||
param_name: The name of the parameter.
|
||||
|
||||
Returns:
|
||||
The abbreviation string, or None if not found.
|
||||
"""
|
||||
normalized_param_name = param_name.lower().replace(" ", "_")
|
||||
return self.abbreviations.get(normalized_param_name)
|
||||
|
||||
def get_png(self, icon_size: Optional[QtCore.QSize] = None) -> bytes:
|
||||
"""
|
||||
Returns the icon data as PNG bytes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_qpixmap(self, icon_size: Optional[QtCore.QSize] = None) -> QtGui.QPixmap:
|
||||
"""
|
||||
Returns the icon data as a QPixmap.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ToolBitShapeSvgIcon(ToolBitShapeIcon):
|
||||
asset_type: str = "toolbitshapesvg"
|
||||
|
||||
def get_png(self, icon_size: Optional[QtCore.QSize] = None) -> bytes:
|
||||
"""
|
||||
Converts SVG icon data to PNG and returns it using QtSvg.
|
||||
"""
|
||||
if icon_size is None:
|
||||
icon_size = QtCore.QSize(48, 48)
|
||||
image = QtGui.QImage(icon_size, QtGui.QImage.Format_ARGB32)
|
||||
image.fill(QtGui.Qt.transparent)
|
||||
painter = QtGui.QPainter(image)
|
||||
|
||||
buffer = QtCore.QBuffer(QtCore.QByteArray(self.data))
|
||||
buffer.open(QtCore.QIODevice.ReadOnly)
|
||||
svg_renderer = QtSvg.QSvgRenderer(buffer)
|
||||
svg_renderer.setAspectRatioMode(QtCore.Qt.KeepAspectRatio)
|
||||
svg_renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
byte_array = QtCore.QByteArray()
|
||||
buffer = QtCore.QBuffer(byte_array)
|
||||
buffer.open(QtCore.QIODevice.WriteOnly)
|
||||
image.save(buffer, "PNG")
|
||||
|
||||
return bytes(byte_array)
|
||||
|
||||
def get_qpixmap(self, icon_size: Optional[QtCore.QSize] = None) -> QtGui.QPixmap:
|
||||
"""
|
||||
Returns the SVG icon data as a QPixmap using QtSvg.
|
||||
"""
|
||||
if icon_size is None:
|
||||
icon_size = QtCore.QSize(48, 48)
|
||||
icon_ba = QtCore.QByteArray(self.data)
|
||||
image = QtGui.QImage(icon_size, QtGui.QImage.Format_ARGB32)
|
||||
image.fill(QtGui.Qt.transparent)
|
||||
painter = QtGui.QPainter(image)
|
||||
|
||||
buffer = QtCore.QBuffer(icon_ba) # PySide6
|
||||
buffer.open(QtCore.QIODevice.ReadOnly)
|
||||
data = QtCore.QXmlStreamReader(buffer)
|
||||
renderer = QtSvg.QSvgRenderer(data)
|
||||
renderer.setAspectRatioMode(QtCore.Qt.KeepAspectRatio)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
return QtGui.QPixmap.fromImage(image)
|
||||
|
||||
@cached_property
|
||||
def abbreviations(self) -> Mapping[str, str]:
|
||||
"""
|
||||
Returns a cached mapping of parameter abbreviations from the icon data.
|
||||
|
||||
Only applicable for SVG icons.
|
||||
"""
|
||||
if self.data:
|
||||
return self.get_abbreviations_from_svg(self.data)
|
||||
return {}
|
||||
|
||||
def get_abbr(self, param_name: str) -> Optional[str]:
|
||||
"""
|
||||
Retrieves the abbreviation for a given parameter name.
|
||||
|
||||
Args:
|
||||
param_name: The name of the parameter.
|
||||
|
||||
Returns:
|
||||
The abbreviation string, or None if not found.
|
||||
"""
|
||||
normalized_param_name = param_name.lower().replace(" ", "_")
|
||||
return self.abbreviations.get(normalized_param_name)
|
||||
|
||||
@staticmethod
|
||||
def get_abbreviations_from_svg(svg: bytes) -> Mapping[str, str]:
|
||||
"""
|
||||
Extract abbreviations from SVG text elements.
|
||||
"""
|
||||
try:
|
||||
tree = ET.fromstring(svg)
|
||||
except ET.ParseError:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
for text_elem in tree.findall(".//s:text", _svg_ns):
|
||||
id = text_elem.attrib.get("id", _svg_ns)
|
||||
if id is None or not isinstance(id, str):
|
||||
continue
|
||||
|
||||
abbr = text_elem.text
|
||||
if abbr is not None:
|
||||
result[id.lower()] = abbr
|
||||
|
||||
span_elem = text_elem.find(".//s:tspan", _svg_ns)
|
||||
if span_elem is None:
|
||||
continue
|
||||
abbr = span_elem.text
|
||||
result[id.lower()] = abbr
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ToolBitShapePngIcon(ToolBitShapeIcon):
|
||||
asset_type: str = "toolbitshapepng"
|
||||
|
||||
def get_png(self, icon_size: Optional[QtCore.QSize] = None) -> bytes:
|
||||
"""
|
||||
Returns the PNG icon data.
|
||||
"""
|
||||
# For PNG, resizing might be needed if icon_size is different
|
||||
# from the original size. Simple return for now.
|
||||
return self.data
|
||||
|
||||
def get_qpixmap(self, icon_size: Optional[QtCore.QSize] = None) -> QtGui.QPixmap:
|
||||
"""
|
||||
Returns the PNG icon data as a QPixmap.
|
||||
"""
|
||||
if icon_size is None:
|
||||
icon_size = QtCore.QSize(48, 48)
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.loadFromData(self.data, "PNG")
|
||||
# Scale the pixmap if the requested size is different
|
||||
if pixmap.size() != icon_size:
|
||||
pixmap = pixmap.scaled(
|
||||
icon_size,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation,
|
||||
)
|
||||
return pixmap
|
||||
51
src/Mod/CAM/Path/Tool/shape/models/probe.py
Normal file
51
src/Mod/CAM/Path/Tool/shape/models/probe.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeProbe(ToolBitShape):
|
||||
name = "Probe"
|
||||
aliases = ("probe",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Ball diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Length": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Length of probe"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"ShaftDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Shaft diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Probe")
|
||||
55
src/Mod/CAM/Path/Tool/shape/models/reamer.py
Normal file
55
src/Mod/CAM/Path/Tool/shape/models/reamer.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeReamer(ToolBitShape):
|
||||
name = "Reamer"
|
||||
aliases = ("reamer",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"CuttingEdgeHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting edge height"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Length": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Overall tool length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"ShankDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Reamer")
|
||||
67
src/Mod/CAM/Path/Tool/shape/models/slittingsaw.py
Normal file
67
src/Mod/CAM/Path/Tool/shape/models/slittingsaw.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeSlittingSaw(ToolBitShape):
|
||||
name = "SlittingSaw"
|
||||
aliases = "slittingsaw", "slitting-saw"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"BladeThickness": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Blade thickness"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"CapDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cap diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"CapHeight": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cap 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("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Slitting Saw")
|
||||
63
src/Mod/CAM/Path/Tool/shape/models/tap.py
Normal file
63
src/Mod/CAM/Path/Tool/shape/models/tap.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeTap(ToolBitShape):
|
||||
name = "Tap"
|
||||
aliases = ("Tap",)
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"CuttingEdgeLength": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting edge length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Tap diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Flutes": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Flutes"),
|
||||
"App::PropertyInteger",
|
||||
),
|
||||
"Length": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Overall length of tap"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"ShankDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"TipAngle": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Tip angle"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Tap")
|
||||
71
src/Mod/CAM/Path/Tool/shape/models/threadmill.py
Normal file
71
src/Mod/CAM/Path/Tool/shape/models/threadmill.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeThreadMill(ToolBitShape):
|
||||
name = "ThreadMill"
|
||||
aliases = "threadmill", "thread-mill"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"Crest": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Crest height"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Diameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Major diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Flutes": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Flutes"),
|
||||
"App::PropertyInteger",
|
||||
),
|
||||
"Length": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Overall tool length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"NeckDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Neck diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"NeckLength": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Neck length"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"ShankDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"cuttingAngle": ( # TODO rename to CuttingAngle
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting angle"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "Thread Mill")
|
||||
67
src/Mod/CAM/Path/Tool/shape/models/vbit.py
Normal file
67
src/Mod/CAM/Path/Tool/shape/models/vbit.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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
|
||||
from typing import Tuple, Mapping
|
||||
from .base import ToolBitShape
|
||||
|
||||
|
||||
class ToolBitShapeVBit(ToolBitShape):
|
||||
name = "VBit"
|
||||
aliases = "vbit", "v-bit"
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"CuttingEdgeAngle": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Cutting edge angle"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
"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("ToolBitShape", "Shank diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"TipDiameter": (
|
||||
FreeCAD.Qt.translate("ToolBitShape", "Tip diameter"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return FreeCAD.Qt.translate("ToolBitShape", "V-Bit")
|
||||
0
src/Mod/CAM/Path/Tool/shape/ui/__init__.py
Normal file
0
src/Mod/CAM/Path/Tool/shape/ui/__init__.py
Normal file
216
src/Mod/CAM/Path/Tool/shape/ui/flowlayout.py
Normal file
216
src/Mod/CAM/Path/Tool/shape/ui/flowlayout.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
from PySide.QtCore import *
|
||||
from PySide.QtGui import *
|
||||
|
||||
|
||||
class FlowLayout(QLayout):
|
||||
widthChanged = Signal(int)
|
||||
|
||||
def __init__(self, parent=None, margin=0, spacing=-1, orientation=Qt.Horizontal):
|
||||
super(FlowLayout, self).__init__(parent)
|
||||
|
||||
if parent is not None:
|
||||
self.setContentsMargins(margin, margin, margin, margin)
|
||||
|
||||
self.setSpacing(spacing)
|
||||
self.itemList = []
|
||||
self.orientation = orientation
|
||||
|
||||
def __del__(self):
|
||||
item = self.takeAt(0)
|
||||
while item:
|
||||
item = self.takeAt(0)
|
||||
|
||||
def addItem(self, item):
|
||||
self.itemList.append(item)
|
||||
|
||||
def count(self):
|
||||
return len(self.itemList)
|
||||
|
||||
def itemAt(self, index):
|
||||
if index >= 0 and index < len(self.itemList):
|
||||
return self.itemList[index]
|
||||
|
||||
return None
|
||||
|
||||
def takeAt(self, index):
|
||||
if index >= 0 and index < len(self.itemList):
|
||||
return self.itemList.pop(index)
|
||||
|
||||
return None
|
||||
|
||||
def expandingDirections(self):
|
||||
return Qt.Orientations(Qt.Orientation(0))
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
if self.orientation == Qt.Horizontal:
|
||||
return self.doLayoutHorizontal(QRect(0, 0, width, 0), True)
|
||||
elif self.orientation == Qt.Vertical:
|
||||
return self.doLayoutVertical(QRect(0, 0, width, 0), True)
|
||||
|
||||
def setGeometry(self, rect):
|
||||
super(FlowLayout, self).setGeometry(rect)
|
||||
if self.orientation == Qt.Horizontal:
|
||||
self.doLayoutHorizontal(rect, False)
|
||||
elif self.orientation == Qt.Vertical:
|
||||
self.doLayoutVertical(rect, False)
|
||||
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
|
||||
def minimumSize(self):
|
||||
size = QSize()
|
||||
|
||||
for item in self.itemList:
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
|
||||
margin, _, _, _ = self.getContentsMargins()
|
||||
|
||||
size += QSize(2 * margin, 2 * margin)
|
||||
return size
|
||||
|
||||
def doLayoutHorizontal(self, rect, testOnly):
|
||||
# Get initial coordinates of the drawing region (should be 0, 0)
|
||||
x = rect.x()
|
||||
y = rect.y()
|
||||
lineHeight = 0
|
||||
i = 0
|
||||
for item in self.itemList:
|
||||
wid = item.widget()
|
||||
# Space X and Y is item spacing horizontally and vertically
|
||||
spaceX = self.spacing() + wid.style().layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
spaceY = self.spacing() + wid.style().layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
# Determine the coordinate we want to place the item at
|
||||
# It should be placed at : initial coordinate of the rect + width of the item + spacing
|
||||
nextX = x + item.sizeHint().width() + spaceX
|
||||
# If the calculated nextX is greater than the outer bound...
|
||||
if nextX - spaceX > rect.right() and lineHeight > 0:
|
||||
x = rect.x() # Reset X coordinate to origin of drawing region
|
||||
y = y + lineHeight + spaceY # Move Y coordinate to the next line
|
||||
nextX = (
|
||||
x + item.sizeHint().width() + spaceX
|
||||
) # Recalculate nextX based on the new X coordinate
|
||||
lineHeight = 0
|
||||
|
||||
if not testOnly:
|
||||
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
|
||||
|
||||
x = nextX # Store the next starting X coordinate for next item
|
||||
lineHeight = max(lineHeight, item.sizeHint().height())
|
||||
i = i + 1
|
||||
|
||||
return y + lineHeight - rect.y()
|
||||
|
||||
def doLayoutVertical(self, rect, testOnly):
|
||||
# Get initial coordinates of the drawing region (should be 0, 0)
|
||||
x = rect.x()
|
||||
y = rect.y()
|
||||
# Initialize column width and line height
|
||||
columnWidth = 0
|
||||
lineHeight = 0
|
||||
|
||||
# Space between items
|
||||
spaceX = 0
|
||||
spaceY = 0
|
||||
|
||||
# Variables that will represent the position of the widgets in a 2D Array
|
||||
i = 0
|
||||
j = 0
|
||||
for item in self.itemList:
|
||||
wid = item.widget()
|
||||
# Space X and Y is item spacing horizontally and vertically
|
||||
spaceX = self.spacing() + wid.style().layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
spaceY = self.spacing() + wid.style().layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
# Determine the coordinate we want to place the item at
|
||||
# It should be placed at : initial coordinate of the rect + width of the item + spacing
|
||||
nextY = y + item.sizeHint().height() + spaceY
|
||||
# If the calculated nextY is greater than the outer bound, move to the next column
|
||||
if nextY - spaceY > rect.bottom() and columnWidth > 0:
|
||||
y = rect.y() # Reset y coordinate to origin of drawing region
|
||||
x = x + columnWidth + spaceX # Move X coordinate to the next column
|
||||
nextY = (
|
||||
y + item.sizeHint().height() + spaceY
|
||||
) # Recalculate nextX based on the new X coordinate
|
||||
# Reset the column width
|
||||
columnWidth = 0
|
||||
|
||||
# Set indexes of the item for the 2D array
|
||||
j += 1
|
||||
i = 0
|
||||
|
||||
# Assign 2D array indexes
|
||||
item.x_index = i
|
||||
item.y_index = j
|
||||
|
||||
# Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
|
||||
# For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
|
||||
if not testOnly:
|
||||
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
|
||||
|
||||
y = nextY # Store the next starting Y coordinate for next item
|
||||
columnWidth = max(
|
||||
columnWidth, item.sizeHint().width()
|
||||
) # Update the width of the column
|
||||
lineHeight = max(lineHeight, item.sizeHint().height()) # Update the height of the line
|
||||
|
||||
i += 1 # Increment i
|
||||
|
||||
# Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
|
||||
# For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
|
||||
if not testOnly:
|
||||
self.calculateMaxWidth(i)
|
||||
self.widthChanged.emit(self.totalMaxWidth + spaceX * self.itemsOnWidestRow)
|
||||
return lineHeight
|
||||
|
||||
# Method to calculate the maximum width among each "row" of the flow layout
|
||||
# This will be useful to let the UI know the total width of the flow layout
|
||||
def calculateMaxWidth(self, numberOfRows):
|
||||
# Init variables
|
||||
self.totalMaxWidth = 0
|
||||
self.itemsOnWidestRow = 0
|
||||
|
||||
# For each "row", calculate the total width by adding the width of each item
|
||||
# and then update the totalMaxWidth if the calculated width is greater than the current value
|
||||
# Also update the number of items on the widest row
|
||||
for i in range(numberOfRows):
|
||||
rowWidth = 0
|
||||
itemsOnWidestRow = 0
|
||||
for item in self.itemList:
|
||||
# Only compare items from the same row
|
||||
if item.x_index == i:
|
||||
rowWidth += item.sizeHint().width()
|
||||
itemsOnWidestRow += 1
|
||||
if rowWidth > self.totalMaxWidth:
|
||||
self.totalMaxWidth = rowWidth
|
||||
self.itemsOnWidestRow = itemsOnWidestRow
|
||||
47
src/Mod/CAM/Path/Tool/shape/ui/shapebutton.py
Normal file
47
src/Mod/CAM/Path/Tool/shape/ui/shapebutton.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
from PySide import QtGui, QtCore
|
||||
|
||||
|
||||
class ShapeButton(QtGui.QToolButton):
|
||||
def __init__(self, shape, parent=None):
|
||||
super(ShapeButton, self).__init__(parent)
|
||||
self.shape = shape
|
||||
|
||||
self.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
|
||||
self.setText(shape.label)
|
||||
|
||||
self.setFixedSize(128, 128)
|
||||
self.setBaseSize(128, 128)
|
||||
self.icon_size = QtCore.QSize(71, 100)
|
||||
self.setIconSize(self.icon_size)
|
||||
|
||||
self._update_icon()
|
||||
|
||||
def set_text(self, text):
|
||||
self.label.setText(text)
|
||||
|
||||
def _update_icon(self):
|
||||
icon = self.shape.get_icon()
|
||||
if icon:
|
||||
pixmap = icon.get_qpixmap(self.icon_size)
|
||||
self.setIcon(QtGui.QIcon(pixmap))
|
||||
80
src/Mod/CAM/Path/Tool/shape/ui/shapeselector.py
Normal file
80
src/Mod/CAM/Path/Tool/shape/ui/shapeselector.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
from typing import Optional, cast
|
||||
import FreeCADGui
|
||||
from functools import partial
|
||||
from PySide import QtGui
|
||||
from ...camassets import cam_assets
|
||||
from .. import ToolBitShape
|
||||
from .flowlayout import FlowLayout
|
||||
from .shapebutton import ShapeButton
|
||||
|
||||
|
||||
class ShapeSelector:
|
||||
def __init__(self):
|
||||
self.shape = None
|
||||
self.form = FreeCADGui.PySideUic.loadUi(":/panels/ShapeSelector.ui")
|
||||
|
||||
self.form.buttonBox.clicked.connect(self.form.close)
|
||||
|
||||
self.flows = {}
|
||||
|
||||
self.update_shapes()
|
||||
self.form.toolBox.setCurrentIndex(0)
|
||||
|
||||
def _add_shape_group(self, toolbox):
|
||||
if toolbox in self.flows:
|
||||
return self.flows[toolbox]
|
||||
flow = FlowLayout(toolbox, orientation=QtGui.Qt.Horizontal)
|
||||
flow.widthChanged.connect(lambda x: toolbox.setMinimumWidth(x))
|
||||
self.flows[toolbox] = flow
|
||||
return flow
|
||||
|
||||
def _add_shapes(self, toolbox, shapes):
|
||||
flow = self._add_shape_group(toolbox)
|
||||
|
||||
# Remove all shapes first.
|
||||
for i in reversed(range(flow.count())):
|
||||
flow.itemAt(i).widget().setParent(None)
|
||||
|
||||
# Add all shapes.
|
||||
for shape in sorted(shapes, key=lambda x: x.label):
|
||||
button = ShapeButton(shape)
|
||||
flow.addWidget(button)
|
||||
cb = partial(self.on_shape_button_clicked, shape)
|
||||
button.clicked.connect(cb)
|
||||
|
||||
def update_shapes(self):
|
||||
# Retrieve each shape asset
|
||||
shapes = set(cam_assets.fetch(asset_type="toolbitshape"))
|
||||
|
||||
builtin = set(s for s in shapes if cast(ToolBitShape, s).is_builtin)
|
||||
self._add_shapes(self.form.standardTools, builtin)
|
||||
self._add_shapes(self.form.customTools, shapes - builtin)
|
||||
|
||||
def on_shape_button_clicked(self, shape):
|
||||
self.shape = shape
|
||||
self.form.close()
|
||||
|
||||
def show(self) -> Optional[ToolBitShape]:
|
||||
self.form.exec()
|
||||
return self.shape
|
||||
43
src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py
Normal file
43
src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
from PySide import QtGui, QtCore
|
||||
|
||||
|
||||
class ShapeWidget(QtGui.QWidget):
|
||||
def __init__(self, shape, parent=None):
|
||||
super(ShapeWidget, self).__init__(parent)
|
||||
self.layout = QtGui.QVBoxLayout(self)
|
||||
self.layout.setAlignment(QtCore.Qt.AlignHCenter)
|
||||
|
||||
self.shape = shape
|
||||
ratio = self.devicePixelRatioF()
|
||||
self.icon_size = QtCore.QSize(200 * ratio, 235 * ratio)
|
||||
self.icon_widget = QtGui.QLabel()
|
||||
self.layout.addWidget(self.icon_widget)
|
||||
|
||||
self._update_icon()
|
||||
|
||||
def _update_icon(self):
|
||||
icon = self.shape.get_icon()
|
||||
if icon:
|
||||
pixmap = icon.get_qpixmap(self.icon_size)
|
||||
self.icon_widget.setPixmap(pixmap)
|
||||
114
src/Mod/CAM/Path/Tool/shape/util.py
Normal file
114
src/Mod/CAM/Path/Tool/shape/util.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Samuel Abels <knipknap@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 pathlib
|
||||
from typing import Optional
|
||||
import FreeCAD
|
||||
import tempfile
|
||||
import os
|
||||
from .doc import ShapeDocFromBytes
|
||||
|
||||
|
||||
_svg_ns = {"s": "http://www.w3.org/2000/svg"}
|
||||
|
||||
|
||||
def file_is_newer(reference: pathlib.Path, file: pathlib.Path):
|
||||
return reference.stat().st_mtime > file.stat().st_mtime
|
||||
|
||||
|
||||
def create_thumbnail(filepath: pathlib.Path, w: int = 200, h: int = 200) -> Optional[pathlib.Path]:
|
||||
if not FreeCAD.GuiUp:
|
||||
return None
|
||||
|
||||
try:
|
||||
import FreeCADGui
|
||||
except ImportError:
|
||||
raise RuntimeError("Error: Could not load UI - is it up?")
|
||||
|
||||
doc = FreeCAD.openDocument(str(filepath))
|
||||
view = FreeCADGui.activeDocument().ActiveView
|
||||
out_filepath = filepath.with_suffix(".png")
|
||||
if not view:
|
||||
print("No view active, cannot make thumbnail for {}".format(filepath))
|
||||
return
|
||||
|
||||
view.viewFront()
|
||||
view.fitAll()
|
||||
view.setAxisCross(False)
|
||||
view.saveImage(str(out_filepath), w, h, "Transparent")
|
||||
|
||||
FreeCAD.closeDocument(doc.Name)
|
||||
return out_filepath
|
||||
|
||||
|
||||
def create_thumbnail_from_data(shape_data: bytes, w: int = 200, h: int = 200) -> Optional[bytes]:
|
||||
"""
|
||||
Create a thumbnail icon from shape data bytes using a temporary document.
|
||||
|
||||
Args:
|
||||
shape_data (bytes): The raw bytes of the shape file (.FCStd).
|
||||
w (int): Width of the thumbnail.
|
||||
h (int): Height of the thumbnail.
|
||||
|
||||
Returns:
|
||||
Optional[bytes]: PNG image bytes, or None if generation fails.
|
||||
"""
|
||||
if not FreeCAD.GuiUp:
|
||||
return None
|
||||
|
||||
try:
|
||||
import FreeCADGui
|
||||
except ImportError:
|
||||
raise RuntimeError("Error: Could not load UI - is it up?")
|
||||
|
||||
temp_png_path = None
|
||||
try:
|
||||
with ShapeDocFromBytes(shape_data) as doc:
|
||||
view = FreeCADGui.activeDocument().ActiveView
|
||||
|
||||
if not view:
|
||||
print("No view active, cannot make thumbnail from data")
|
||||
return None
|
||||
|
||||
view.viewFront()
|
||||
view.fitAll()
|
||||
view.setAxisCross(False)
|
||||
|
||||
# Create a temporary file path for the output PNG
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
|
||||
temp_png_path = pathlib.Path(temp_file.name)
|
||||
|
||||
view.saveImage(str(temp_png_path), w, h, "Transparent")
|
||||
|
||||
# Read the PNG bytes
|
||||
with open(temp_png_path, "rb") as f:
|
||||
png_bytes = f.read()
|
||||
|
||||
return png_bytes
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating thumbnail from data: {e}")
|
||||
return None
|
||||
|
||||
finally:
|
||||
# Clean up temporary PNG file
|
||||
if temp_png_path and temp_png_path.exists():
|
||||
os.remove(temp_png_path)
|
||||
Reference in New Issue
Block a user