Download src/Mod/CAM/Path/Post/scripts/svg_post.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Post/scripts/svg_post.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Post/scripts/svg_post.py
-
curl -L -o svg_post.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Post/scripts/svg_post.py
11.8 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2024 Ondsel <development@ondsel.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 Path.Post.Processor import PostProcessor | |
| import Path | |
| import FreeCAD | |
| import math | |
| Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) | |
| translate = FreeCAD.Qt.translate | |
| debug = False | |
| if debug: | |
| Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) | |
| Path.Log.trackModule(Path.Log.thisModule()) | |
| else: | |
| Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) | |
| # Define colors for the layers | |
| LAYER_COLORS = {"CUT": "red", "ENGRAVE": "blue", "FILL": "green", "DEFAULT": "black"} | |
| class Svg(PostProcessor): | |
| def __init__(self, job): | |
| super().__init__( | |
| job, | |
| tooltip=translate("CAM", "SVG post processor"), | |
| tooltipargs=[], | |
| units="mm", | |
| ) | |
| Path.Log.debug("SVG post processor initialized") | |
| def export(self): | |
| Path.Log.debug("Exporting the job") | |
| use_layers = "--layers" in self._job.PostProcessorArgs | |
| postables = self._buildPostList() | |
| Path.Log.debug(f"postables count: {len(postables)}") | |
| svg_strings = [] | |
| for idx, section in enumerate(postables): | |
| svg_content = self.create_svg_section(section, idx, use_layers) | |
| svg_strings.append((idx, svg_content)) | |
| return svg_strings | |
| def create_svg_section(self, section, idx, use_layers): | |
| Path.Log.track() | |
| partname, sublist = section | |
| # Initialize bounding box | |
| xmin, ymin, xmax, ymax = self.calculate_bounding_box(sublist) | |
| if xmin is None or ymin is None or xmax is None or ymax is None: | |
| Path.Log.debug("No wires found, skipping section") | |
| return "" | |
| width = xmax - xmin | |
| height = ymax - ymin | |
| # Create the SVG header with a normalized viewBox | |
| svg_content = ( | |
| f'<svg xmlns="http://www.w3.org/2000/svg" ' | |
| f'xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" ' | |
| f'width="{width}mm" height="{height}mm" viewBox="0 0 {width} {height}">\n' | |
| ) | |
| # Convert each wire to an SVG path | |
| for obj_idx, obj in enumerate(sublist): | |
| strokestyle = self.get_stroke_style(obj) | |
| color = strokestyle["color"] | |
| width = strokestyle["width"] | |
| pathtype = strokestyle["label"] | |
| Path.Log.debug(pathtype) | |
| wires = Path.Geom.wiresForPath(obj.Path) | |
| if not wires: | |
| continue # Skip objects that do not produce wires | |
| if use_layers: | |
| layer_id = f"layer{idx}_{obj_idx}" | |
| svg_content += ( | |
| f'<g id="{layer_id}" inkscape:label="{obj.Label}" inkscape:groupmode="layer">\n' | |
| ) | |
| for wire in wires: | |
| path_data = self.wire_to_svg_path(wire, width, height, xmin, ymin) | |
| if pathtype == "FILL": | |
| svg_content += f' <path d="{path_data}" stroke="none" fill="{color}" />\n' | |
| else: | |
| svg_content += f' <path d="{path_data}" stroke="{color}" stroke-width="{width}" fill="none" />\n' | |
| if use_layers: | |
| svg_content += "</g>\n" | |
| # Close the SVG tag | |
| svg_content += "</svg>" | |
| return svg_content | |
| def wire_to_svg_path(self, wire, width, height, xmin, ymin): | |
| """Convert FreeCAD wire to SVG path data with y-axis inversion and limited precision""" | |
| path_data = "" | |
| is_first_point = True | |
| vertices_info = [] | |
| def format_coord(value): | |
| return f"{value:.4f}" | |
| for edge in wire.Edges: | |
| start_point = edge.Vertexes[0].Point | |
| end_point = edge.Vertexes[-1].Point | |
| Path.Log.debug(f"Edge Type: {edge.Curve.TypeId}") | |
| Path.Log.debug( | |
| f"Start Point: ({format_coord(start_point.x)}, {format_coord(start_point.y)})" | |
| ) | |
| Path.Log.debug(f"End Point: ({format_coord(end_point.x)}, {format_coord(end_point.y)})") | |
| # Check if the edge is vertical (should be skipped) | |
| if start_point.x == end_point.x and start_point.y == end_point.y: | |
| Path.Log.debug("Skipping vertical edge") | |
| continue | |
| if is_first_point: | |
| path_data += f"M {format_coord(start_point.x - xmin)} {format_coord(height - (start_point.y - ymin))} " | |
| vertices_info.append( | |
| f"M {format_coord(start_point.x - xmin)} {format_coord(height - (start_point.y - ymin))}" | |
| ) | |
| is_first_point = False | |
| if edge.Curve.TypeId in ["Part::GeomLineSegment", "Part::GeomLine"]: | |
| # Handle line segment without discretization | |
| path_data += f"L {format_coord(end_point.x - xmin)} {format_coord(height - (end_point.y - ymin))} " | |
| vertices_info.append( | |
| f"L {format_coord(end_point.x - xmin)} {format_coord(height - (end_point.y - ymin))}" | |
| ) | |
| Path.Log.debug( | |
| f"Line segment from ({format_coord(start_point.x)}, {format_coord(start_point.y)}) to ({format_coord(end_point.x)}, {format_coord(end_point.y)})" | |
| ) | |
| elif edge.Curve.TypeId in ["Part::GeomCircle", "Part::GeomArcOfCircle"]: | |
| # Handle circular arc using 'A' command | |
| radius = edge.Curve.Radius | |
| center = edge.Curve.Center | |
| start_angle = math.atan2(start_point.y - center.y, start_point.x - center.x) | |
| end_angle = math.atan2(end_point.y - center.y, end_point.x - center.x) | |
| # Calculate the angle difference and normalize to [-π, π] | |
| angle_diff = (end_angle - start_angle) % (2 * math.pi) | |
| if angle_diff > math.pi: | |
| angle_diff -= 2 * math.pi | |
| Path.Log.debug(f"Angle difference: {angle_diff:.2f} radians") | |
| # Determine the large_arc_flag and sweep_flag | |
| large_arc_flag = 1 if abs(angle_diff) > (math.pi) else 0 | |
| sweep_flag = 0 if angle_diff > 0 else 1 | |
| start_x = format_coord(start_point.x - xmin) | |
| start_y = format_coord(height - (start_point.y - ymin)) | |
| end_x = format_coord(end_point.x - xmin) | |
| end_y = format_coord(height - (end_point.y - ymin)) | |
| path_data += f"A {format_coord(radius)} {format_coord(radius)} 0 {large_arc_flag} {sweep_flag} {end_x} {end_y} " | |
| vertices_info.append( | |
| f"A {format_coord(radius)} {format_coord(radius)} 0 {large_arc_flag} {sweep_flag} {end_x} {end_y}" | |
| ) | |
| Path.Log.debug( | |
| f"Circular arc with radius {format_coord(radius)} from ({start_x}, {start_y}) to ({end_x}, {end_y}) (large_arc_flag: {large_arc_flag}, sweep_flag: {sweep_flag})" | |
| ) | |
| Path.Log.debug(path_data) | |
| else: | |
| # Discretize other types of edges into 100 segments | |
| vertices = edge.discretize(100) | |
| if len(vertices) < 2: | |
| continue | |
| if all(vertices[0].x == v.x and vertices[0].y == v.y for v in vertices[1:]): | |
| continue | |
| for vertex in vertices: | |
| path_data += f"L {format_coord(vertex.x - xmin)} {format_coord(height - (vertex.y - ymin))} " | |
| vertices_info.append( | |
| f"L {format_coord(vertex.x - xmin)} {format_coord(height - (vertex.y - ymin))}" | |
| ) | |
| Path.Log.debug(f"Discretized edge with {len(vertices)} points") | |
| path_data += "Z " # Close the path for filled edges | |
| vertices_info.append("Z") | |
| Path.Log.debug(f"SVG Path: {path_data.strip()}") | |
| Path.Log.debug(f"Vertices Info: {vertices_info}") | |
| return path_data.strip() | |
| def calculate_bounding_box(self, sublist): | |
| xmin, ymin, xmax, ymax = None, None, None, None | |
| for obj in sublist: | |
| wires = Path.Geom.wiresForPath(obj.Path) | |
| for wire in wires: | |
| for vertex in wire.Vertexes: | |
| x, y = vertex.X, vertex.Y | |
| if xmin is None or x < xmin: | |
| xmin = x | |
| if xmax is None or x > xmax: | |
| xmax = x | |
| if ymin is None or y < ymin: | |
| ymin = y | |
| if ymax is None or y > ymax: | |
| ymax = y | |
| return xmin, ymin, xmax, ymax | |
| def get_stroke_style(self, obj): | |
| strokewidth = 0.1 | |
| color = LAYER_COLORS["DEFAULT"] | |
| label = None | |
| if hasattr(obj, "ToolController"): | |
| tc = obj.ToolController | |
| if hasattr(tc, "Tool") and hasattr(tc.Tool, "Diameter"): | |
| strokewidth = tc.Tool.Diameter | |
| if hasattr(tc, "Label"): | |
| for key in LAYER_COLORS: | |
| if key in tc.Label: | |
| label = key | |
| color = LAYER_COLORS[key] | |
| break | |
| return {"width": strokewidth, "color": color, "label": label} | |
| def tooltip(self): | |
| tooltip = """ | |
| This is the Ondsel SVG CAM post processor. | |
| It will export a CAM job to an SVG file with colors and layers. | |
| Tool Controllers determine how the svg will be created and, thus, how the | |
| laser will behave. They must have a label containing a string from the list at the top of the post file. | |
| (e.g. CUT, FILL, ENGRAVE). SVG Paths will be color coded similarly | |
| Actual laser behavior will depend on how the laser controller is configured | |
| to process the colors / layers. | |
| Step-downs in operations will result in multiple passes. | |
| """ | |
| return tooltip | |
| def tooltipArgs(self): | |
| argtooltip = """ | |
| --layers: Output will be written to different layers. Layer names are taken from the operation label | |
| """ | |
| return argtooltip | |
| def preferredExtension(self): | |
| return "svg" | |