| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from PySide.QtCore import QT_TRANSLATE_NOOP |
| import FreeCAD |
| import Path |
| import Path.Base.Generator.dogboneII as dogboneII |
| import Path.Base.Language as PathLanguage |
| import Path.Dressup.Utils as PathDressup |
| import PathScripts.PathUtils as PathUtils |
| import math |
|
|
| if False: |
| Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| Path.Log.trackModule(Path.Log.thisModule()) |
|
|
| PI = math.pi |
|
|
|
|
| def calc_length_adaptive(kink, angle, nominal_length, custom_length): |
| Path.Log.track(kink, angle, nominal_length, custom_length) |
|
|
| if Path.Geom.isRoughly(abs(kink.deflection()), 0): |
| return 0 |
|
|
| |
| |
| |
| if Path.Geom.isRoughly(abs(kink.deflection()), PI): |
| return nominal_length |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| dist = nominal_length / math.cos(kink.deflection() / 2) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| da = Path.Geom.normalizeAngle(kink.normAngle() - angle) |
| depth = dist * math.cos(da) |
| if depth < 0: |
| Path.Log.debug( |
| f"depth={depth:4f}: kink={kink}, angle={180*angle/PI}, dist={dist:.4f}, da={180*da/PI} -> depth=0.0" |
| ) |
| depth = 0 |
| else: |
| height = dist * abs(math.sin(da)) |
| if height < nominal_length: |
| depth = depth - math.sqrt(nominal_length * nominal_length - height * height) |
| Path.Log.debug( |
| f"{kink}: angle={180*angle/PI}, dist={dist:.4f}, da={180*da/PI}, depth={depth:.4f}" |
| ) |
|
|
| return depth |
|
|
|
|
| def calc_length_nominal(kink, angle, nominal_length, custom_length): |
| return nominal_length |
|
|
|
|
| def calc_length_custom(kink, angle, nominal_length, custom_length): |
| return custom_length |
|
|
|
|
| class Style(object): |
| """Style - enumeration class for the supported bone styles""" |
|
|
| Dogbone = "Dogbone" |
| Tbone_H = "T-bone horizontal" |
| Tbone_V = "T-bone vertical" |
| Tbone_L = "T-bone long edge" |
| Tbone_S = "T-bone short edge" |
| All = [Dogbone, Tbone_H, Tbone_V, Tbone_L, Tbone_S] |
|
|
| Generator = { |
| Dogbone: dogboneII.GeneratorDogbone, |
| Tbone_H: dogboneII.GeneratorTBoneHorizontal, |
| Tbone_V: dogboneII.GeneratorTBoneVertical, |
| Tbone_S: dogboneII.GeneratorTBoneOnShort, |
| Tbone_L: dogboneII.GeneratorTBoneOnLong, |
| } |
|
|
|
|
| class Side(object): |
| """Side - enumeration class for the side of the path to attach bones""" |
|
|
| Left = "Left" |
| Right = "Right" |
| All = [Left, Right] |
|
|
| @classmethod |
| def oppositeOf(cls, side): |
| if side == cls.Left: |
| return cls.Right |
| if side == cls.Right: |
| return cls.Left |
| return None |
|
|
|
|
| class Incision(object): |
| """Incision - enumeration class for the different depths of bone incision""" |
|
|
| Fixed = "fixed" |
| Adaptive = "adaptive" |
| Custom = "custom" |
| All = [Adaptive, Fixed, Custom] |
|
|
| Calc = { |
| Fixed: calc_length_nominal, |
| Adaptive: calc_length_adaptive, |
| Custom: calc_length_custom, |
| } |
|
|
|
|
| def insertBone(obj, kink): |
| """insertBone(kink, side) - return True if a bone should be inserted into the kink""" |
| if not kink.isKink(): |
| Path.Log.debug("not a kink") |
| return False |
|
|
| if obj.Side == Side.Right and kink.goesRight(): |
| return False |
| if obj.Side == Side.Left and kink.goesLeft(): |
| return False |
| return True |
|
|
|
|
| class BoneState(object): |
| def __init__(self, bone, nr, enabled=True): |
| self.bone = bone |
| self.bones = {nr: bone} |
| self.enabled = enabled |
| pos = bone.position() |
| self.pos = FreeCAD.Vector(pos.x, pos.y, 0) |
|
|
| def isEnabled(self): |
| return self.enabled |
|
|
| def addBone(self, bone, nr): |
| self.bones[nr] = bone |
|
|
| def position(self): |
| return self.pos |
|
|
| def boneTip(self): |
| return self.bone.tip() |
|
|
| def boneIDs(self): |
| return sorted(self.bones) |
|
|
| def zLevels(self): |
| return sorted([bone.position().z for bone in self.bones.values()]) |
|
|
| def length(self): |
| return self.bone.length |
|
|
|
|
| class Proxy(object): |
| def __init__(self, obj, base): |
| obj.addProperty( |
| "App::PropertyLink", |
| "Base", |
| "Base", |
| QT_TRANSLATE_NOOP("App::Property", "The base path to dress up"), |
| ) |
| obj.Base = base |
|
|
| obj.addProperty( |
| "App::PropertyEnumeration", |
| "Side", |
| "Dressup", |
| QT_TRANSLATE_NOOP("App::Property", "The side of path to insert bones"), |
| ) |
| obj.Side = Side.All |
| if hasattr(base, "BoneBlacklist"): |
| obj.Side = base.Side |
| else: |
| side = Side.Right |
| if hasattr(obj.Base, "Side") and obj.Base.Side == "Inside": |
| side = Side.Left |
| if hasattr(obj.Base, "Direction") and obj.Base.Direction == "CCW": |
| side = Side.oppositeOf(side) |
| obj.Side = side |
|
|
| obj.addProperty( |
| "App::PropertyEnumeration", |
| "Style", |
| "Dressup", |
| QT_TRANSLATE_NOOP("App::Property", "The style of bones"), |
| ) |
| obj.Style = Style.All |
| obj.Style = Style.Dogbone |
|
|
| obj.addProperty( |
| "App::PropertyEnumeration", |
| "Incision", |
| "Dressup", |
| QT_TRANSLATE_NOOP("App::Property", "The algorithm to determine the bone length"), |
| ) |
| obj.Incision = Incision.All |
| obj.Incision = Incision.Adaptive |
|
|
| obj.addProperty( |
| "App::PropertyLength", |
| "Custom", |
| "Dressup", |
| QT_TRANSLATE_NOOP("App::Property", "Dressup length if incision is set to 'custom'"), |
| ) |
| obj.Custom = 0.0 |
|
|
| obj.addProperty( |
| "App::PropertyIntegerList", |
| "BoneBlacklist", |
| "Dressup", |
| QT_TRANSLATE_NOOP("App::Property", "Bones that aren't dressed up"), |
| ) |
| obj.BoneBlacklist = [] |
|
|
| obj.addProperty( |
| "App::PropertyBool", |
| "OnlyClosedProfiles", |
| "Dressup", |
| QT_TRANSLATE_NOOP( |
| "App::Property", |
| "Create bones only for outer closed profiles\nCan be useful for multi profile operations, e.g. Pocket with ZigZagOffset pattern", |
| ), |
| ) |
| self.onDocumentRestored(obj) |
|
|
| def onDocumentRestored(self, obj): |
| self.obj = obj |
| obj.setEditorMode("BoneBlacklist", 2) |
|
|
| def dumps(self): |
| return None |
|
|
| def loads(self, state): |
| return None |
|
|
| def onChanged(self, obj, prop): |
| if prop == "Path" and obj.ViewObject: |
| obj.ViewObject.signalChangeIcon() |
|
|
| def toolRadius(self, obj): |
| return PathDressup.toolController(obj.Base).Tool.Diameter.Value / 2 |
|
|
| def createBone(self, obj, move0, move1): |
| if move0.isRapid() and move1.isRapid(): |
| return None |
| kink = dogboneII.Kink(move0, move1) |
| Path.Log.debug(f"{obj.Label}.createBone({kink})") |
| if insertBone(obj, kink): |
| generator = Style.Generator[obj.Style] |
| calc_length = Incision.Calc[obj.Incision] |
| nominal = self.toolRadius(obj) |
| custom = obj.Custom.Value |
| return dogboneII.generate(kink, generator, calc_length, nominal, custom) |
| return None |
|
|
| |
| def findStartIndexClosedProfile(self, source, startAreaIndex, endAreaIndex): |
| points = [] |
| points.append(source[endAreaIndex].positionEnd()) |
| points.append(source[endAreaIndex - 1].positionEnd()) |
| for i in range(endAreaIndex - 2, startAreaIndex - 1, -1): |
| point = source[i].positionBegin() |
| for j, p in enumerate(points): |
| |
| if Path.Geom.pointsCoincide(point, p): |
| return i, endAreaIndex - j |
| points.append(point) |
| return None, None |
|
|
| |
| def isEquelBoundboxes(self, bb1, bb2): |
| if not Path.Geom.isRoughly(bb1.XMin, bb2.XMin): |
| return False |
| if not Path.Geom.isRoughly(bb1.XMax, bb2.XMax): |
| return False |
| if not Path.Geom.isRoughly(bb1.YMin, bb2.YMin): |
| return False |
| if not Path.Geom.isRoughly(bb1.YMax, bb2.YMax): |
| return False |
| if not Path.Geom.isRoughly(bb1.ZMin, bb2.ZMin): |
| return False |
| if not Path.Geom.isRoughly(bb1.ZMax, bb2.ZMax): |
| return False |
| return True |
|
|
| |
| def getIndexInnerProfiles(self, source, indexList): |
| boundboxList = [] |
| for i, areaIndexList in enumerate(indexList): |
| minX, minY, maxX, maxY = None, None, None, None |
| for index in areaIndexList: |
| point = source[index].positionEnd() |
| minX = point.x if minX is None or point.x < minX else minX |
| maxX = point.x if maxX is None or point.x > maxX else maxX |
| minY = point.y if minY is None or point.y < minY else minY |
| maxY = point.y if maxY is None or point.y > maxY else maxY |
| boundbox = FreeCAD.BoundBox(minX, minY, 0, maxX, maxY, 0) |
| boundboxList.append(boundbox) |
|
|
| excludeList = [] |
| for i, boundbox in enumerate(boundboxList): |
| for bb in boundboxList: |
| if not self.isEquelBoundboxes(boundbox, bb) and bb.isInside(boundbox): |
| excludeList.append(i) |
| break |
| return excludeList |
|
|
| |
| def isCuttingMove(self, instr): |
| result = instr.isMove() and not instr.isRapid() and not instr.isPlunge() |
| return result |
|
|
| def getIndexOuterClosedProfiles(self, source): |
| closedProfilesIndex = [] |
| startArea = None |
| endArea = None |
| for i, instr in enumerate(source): |
| if ( |
| startArea is None |
| and endArea is None |
| and self.isCuttingMove(source[i]) |
| and (i == 0 or not self.isCuttingMove(source[i - 1])) |
| ): |
| |
| startArea = i |
|
|
| if ( |
| startArea is not None |
| and endArea is None |
| and (i == len(source) - 1 or not self.isCuttingMove(source[i + 1])) |
| ): |
| |
| endArea = i |
|
|
| if startArea and endArea: |
| p1 = source[startArea].positionBegin() |
| p2 = source[endArea].positionEnd() |
| if Path.Geom.pointsCoincide(p1, p2): |
| |
| |
| closedProfilesIndex.append(list(range(startArea, endArea + 1))) |
| else: |
| |
| |
| startIndex, endIndex = self.findStartIndexClosedProfile( |
| source, startArea, endArea |
| ) |
| if startIndex is not None and endIndex is not None: |
| closedProfilesIndex.append(list(range(startIndex, endIndex + 1))) |
|
|
| startArea = None |
| endArea = None |
|
|
| excludeList = self.getIndexInnerProfiles(source, closedProfilesIndex) |
|
|
| outerClosedProfilesIndex = [] |
| for i, area in enumerate(closedProfilesIndex): |
| if i not in excludeList: |
| for j in area: |
| outerClosedProfilesIndex.append(j) |
|
|
| return outerClosedProfilesIndex |
|
|
| def execute(self, obj): |
| Path.Log.track(obj.Label) |
| maneuver = PathLanguage.Maneuver() |
| bones = [] |
| lastMove = None |
| moveAfterPlunge = None |
| dressingUpDogbone = hasattr(obj.Base, "BoneBlacklist") |
|
|
| if obj.Base and obj.Base.Path and obj.Base.Path.Commands: |
| source = PathLanguage.Maneuver.FromPath(PathUtils.getPathWithPlacement(obj.Base)).instr |
|
|
| |
| if hasattr(obj, "OnlyClosedProfiles") and obj.OnlyClosedProfiles: |
| closedProfilesIndex = self.getIndexOuterClosedProfiles(source) |
| else: |
| closedProfilesIndex = None |
|
|
| for index, instr in enumerate(source): |
| |
| if instr.isMove(): |
| thisMove = instr |
| bone = None |
| if thisMove.isPlunge() or ( |
| closedProfilesIndex is not None and index not in closedProfilesIndex |
| ): |
| if lastMove and moveAfterPlunge and lastMove.leadsInto(moveAfterPlunge): |
| bone = self.createBone(obj, lastMove, moveAfterPlunge) |
| lastMove = None |
| moveAfterPlunge = None |
| else: |
| if moveAfterPlunge is None: |
| moveAfterPlunge = thisMove |
| if lastMove: |
| bone = self.createBone(obj, lastMove, thisMove) |
| lastMove = thisMove |
| if bone: |
| enabled = len(bones) not in obj.BoneBlacklist |
| if enabled and not ( |
| dressingUpDogbone and obj.Base.Proxy.includesBoneAt(bone.position()) |
| ): |
| maneuver.addInstructions(bone.instr) |
| else: |
| Path.Log.debug(f"{bone.kink} disabled {enabled}") |
| bones.append(bone) |
| maneuver.addInstruction(thisMove) |
| else: |
| |
| maneuver.addInstruction(instr) |
|
|
| else: |
| Path.Log.info(f"No Path found to dress up in op {obj.Base}") |
| self.maneuver = maneuver |
| self.bones = bones |
| self.boneTips = None |
| obj.Path = maneuver.toPath() |
|
|
| def boneStates(self, obj): |
| state = {} |
| if hasattr(self, "bones"): |
| for nr, bone in enumerate(self.bones): |
| pos = bone.position() |
| loc = f"({pos.x:.4f}, {pos.y:.4f})" |
| if state.get(loc, None): |
| state[loc].addBone(bone, nr) |
| else: |
| state[loc] = BoneState(bone, nr) |
| if nr in obj.BoneBlacklist: |
| state[loc].enabled = False |
| return state.values() |
|
|
| def includesBoneAt(self, pos): |
| if hasattr(self, "bones"): |
| for nr, bone in enumerate(self.bones): |
| if Path.Geom.pointsCoincide(bone.position(), pos): |
| return nr not in self.obj.BoneBlacklist |
| return False |
|
|
|
|
| def Create(base, name="DressupDogbone"): |
| obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) |
| pxy = Proxy(obj, base) |
|
|
| obj.Proxy = pxy |
|
|
| return obj |
|
|