# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. """Sketch manipulation.""" from copy import deepcopy import FreeCAD import Draft import Part import Sketcher import numpy as np from .auxiliary import * vec = FreeCAD.Vector def findSegments(sketch): """Return the line segments in a sketch as a numpy array. Parameters ---------- sketch : Returns ------- A `np.ndarray` of line segments from `sketch`. Notes ----- In FC0.17 sketches contain wires by default. """ lineSegments = [] for wire in sketch.Shape.Wires: for edge in wire.Edges: lineSegments.append( [tuple(edge.Vertexes[0].Point), tuple(edge.Vertexes[1].Point)] ) return np.array(lineSegments) def nextSegment(lineSegments, segIndex, tol=1e-8, fixOrder=True): """Return the next line segment index in a collection of tuples defining several cycles. WARNING: this will by default fixOrder, i.e. side effects on the caller. Parameters ---------- lineSegments : ndarray with [lineSegmentIndex,start/end point,coordinate] segIndex : the index to consider tol : repair tolerance for matching (Default value = 1e-8) fixOrder : whether the order lineSegments should be repaired on the fly (Default value = True) Returns ------- Line segment. """ # initial end point - all other segment starts diffList0 = np.sum( np.abs(lineSegments[segIndex, 1, :] - lineSegments[:, 0, :]), axis=1 ) # initial end point - all other segment ends diffList1 = np.sum( np.abs(lineSegments[segIndex, 1, :] - lineSegments[:, 1, :]), axis=1 ) diffList0[segIndex] = 1000.0 diffList1[segIndex] = 1000.0 nextList0 = np.where(diffList0 <= tol)[0] nextList1 = np.where(diffList1 <= tol)[0] if len(nextList0) + len(nextList1) > 1: raise ValueError( "Multiple possible paths found while parsing cycles in sketch." ) elif len(nextList0) + len(nextList1) < 1: raise ValueError("No paths found while parsing cycles in sketch.") elif len(nextList0) == 1: return nextList0[0] else: if fixOrder: # the points were out of order, so they need to be switched nextPoint0 = deepcopy(lineSegments[nextList1[0], 0, :]) nextPoint1 = deepcopy(lineSegments[nextList1[0], 1, :]) lineSegments[nextList1[0], 0, :] = nextPoint1 lineSegments[nextList1[0], 1, :] = nextPoint0 return nextList1[0] def findCycle(lineSegments, startingIndex, availSegIDs): """Find a cycle in a collection of line segments given a starting index. Parameters ---------- lineSegments : startingIndex : availSegIDs : Returns ------- """ currentIndex = startingIndex segList = [startingIndex] for i in availSegIDs: currentIndex = nextSegment( lineSegments, currentIndex ) # throws eventually if not in cycle if currentIndex in segList: break else: segList += [currentIndex] return segList # ~ def findCycle2(sketch, lineSegments, idx): # ~ '''Find a cycle in a collection of line segments given a starting index. # ~ Return the list of indices in the cycle. # ~ ''' # ~ # Find wire to which the indexed segment belongs # ~ # return lineSegment indices of all edges in this wire # ~ for wire in sketch.Shape.Wires: # ~ for edge in wire.Edges: # ~ if idx in wire def addCycleSketch(name, wire): """Add a sketch of a cycle (closed wire) to a FC document. Parameters ---------- name : wire : Returns ------- """ assert wire.isClosed() doc = FreeCAD.ActiveDocument if doc.getObject(name) is not None: raise ValueError(f"Sketch with name '{name}' already exists.") # makeSketch() could handle constraints itself and does recompute() well, # but sometimes we may have invalid wires, which it handles badly (fixsometime) # ~ return Draft.makeSketch([wire], name=name, autoconstraints=True) sketch = doc.addObject("Sketcher::SketchObject", name) for i, edge in enumerate(wire.Edges): v0 = vec(tuple(edge.Vertexes[0].Point)) v1 = vec(tuple(edge.Vertexes[1].Point)) if i > 0: if (v0 - old_v1).Length > 1e-5: # fix invalid wire segments v1 = vec(tuple(edge.Vertexes[0].Point)) v0 = vec(tuple(edge.Vertexes[1].Point)) old_v1 = v1 sketch.addGeometry(Part.LineSegment(v0, v1)) if i > 0: sketch.addConstraint(Sketcher.Constraint("Coincident", i - 1, 2, i, 1)) sketch.addConstraint(Sketcher.Constraint("Coincident", i, 2, 0, 1)) doc.recompute() return sketch def addPolyLineSketch(name, doc, segmentOrder, lineSegments): """Add a sketch given segment order and line segments. Parameters ---------- name : doc : segmentOrder : lineSegments : Returns ------- """ if doc.getObject(name) is not None: raise ValueError(f"Sketch with name '{name}' already exists.") obj = doc.addObject("Sketcher::SketchObject", name) for segIndex, segment in enumerate(lineSegments): startPoint = segment[0, :] endPoint = segment[1, :] obj.addGeometry(Part.LineSegment(vec(tuple(startPoint)), vec(tuple(endPoint)))) for i in range(len(lineSegments)): connectIndex = segmentOrder[i] if connectIndex < len(lineSegments): obj.addConstraint(Sketcher.Constraint("Coincident", i, 2, connectIndex, 1)) doc.recompute() return obj def findEdgeCycles(sketch): # TODO: port objectConstruction crossection stuff """Find the list of edges in a sketch and separate them into cycles. Parameters ---------- sketch : Returns ------- """ lineSegments = findSegments(sketch) # Next, detect cycles: availSegIDs = range(lineSegments.shape[0]) cycles = [] for _ in range(lineSegments.shape[0]): if len(availSegIDs) <= 0: break startingIndex = availSegIDs[0] newCycle = findCycle(lineSegments, startingIndex, availSegIDs) cycles += [newCycle] availSegIDs = [item for item in availSegIDs if item not in newCycle] return lineSegments, cycles def findEdgeCycles2(sketch): """Find the list of edges in a sketch and separate them into cycles. Parameters ---------- sketch : Returns ------- """ return sketch.Shape.Wires def splitSketch(sketch): """Splits a sketch into several, returning a list of names of the new sketches. Parameters ---------- sketch : Returns ------- """ if not sketch.Shape.Wires: raise ValueError("No wires in sketch.") return [ addCycleSketch(f"{sketch.Name}_{i}", wire) for i, wire in enumerate(sketch.Shape.Wires) ] def extendSketch(sketch, d): """For a disconnected polyline, extends the last points of the sketch by a distance d. Parameters ---------- sketch : d : Returns ------- """ doc = FreeCAD.ActiveDocument segments = findSegments(sketch) connections = [] for i in range(len(segments)): try: connecting = nextSegment(segments, i) except: connecting = len(segments) connections += [connecting] # Find the first and last segments: seg0Index = [i for i in range(len(segments)) if i not in connections][0] seg1Index = connections.index(len(segments)) segIndices = [seg0Index, seg1Index] # Since we automatically reorder these, we know the orientation. seg0 = segments[seg0Index] x0, y0, z0 = seg0[0] x1, y1, z1 = seg0[1] dx = x1 - x0 dy = y1 - y0 alpha = np.abs(np.arctan(dy / dx)) if x0 < x1: x0p = x0 - np.cos(alpha) * d else: x0p = x0 + np.cos(alpha) * d if y0 < y1: y0p = y0 - np.sin(alpha) * d else: y0p = y0 + np.sin(alpha) * d segments[seg0Index][0][0] = x0p segments[seg0Index][0][1] = y0p seg1 = segments[seg1Index] x0, y0, z0 = seg1[0] x1, y1, z1 = seg1[1] dx = x1 - x0 dy = y1 - y0 alpha = np.abs(np.arctan(dy / dx)) if x1 < x0: x1p = x1 - np.cos(alpha) * d else: x1p = x1 + np.cos(alpha) * d if y1 < y0: y1p = y1 - np.sin(alpha) * d else: y1p = y1 + np.sin(alpha) * d segments[seg1Index][1][0] = x1p segments[seg1Index][1][1] = y1p myNewLine = addPolyLineSketch( sketch.Name + "_extension", doc, connections, segments ) return myNewLine def makeIntoSketch(inputObj, sketchName=None): """Turn a 2D generic object like a polyline into a sketch. Parameters ---------- inputObj : sketchName : (Default value = None) Returns ------- """ if sketchName is None: sketchName = inputObj.Name + "_sketch" returnSketch = Draft.makeSketch(inputObj, autoconstraints=True, name=sketchName) deepRemove(obj=inputObj) FreeCAD.ActiveDocument.recompute() return returnSketch