From 2c07938e0d2177b91c7b54341a642653098fdfbe Mon Sep 17 00:00:00 2001 From: lorenz Date: Mon, 5 Jul 2021 00:01:02 +0200 Subject: [PATCH 1/4] Remove Attachment deprecation warning for version >=0.19 --- freecad/gears/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freecad/gears/features.py b/freecad/gears/features.py index 627b157..5bb5c99 100644 --- a/freecad/gears/features.py +++ b/freecad/gears/features.py @@ -83,7 +83,7 @@ class BaseGear(object): # Needed to make this object "attachable", # aka able to attach parameterically to other objects # cf. https://wiki.freecadweb.org/Scripted_objects_with_attachment - if int(App.Version()[1]) >= 20: + if int(App.Version()[1]) >= 19: obj.addExtension('Part::AttachExtensionPython') else: obj.addExtension('Part::AttachExtensionPython', obj) From bd6c2107eed6374c99e10b2e54cbd31c5225fdab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Tue, 6 Jul 2021 00:47:24 +0200 Subject: [PATCH 2/4] First proof of concept of "additive gears" in PartDesign bodies In this PoC only the involute gears work, and there is still a lot of cleanup pending. What does work, however, is that those gears now play nicely with PartDesign's concept of stacking features onto each other, i.e. that the result of a feature is the fusion of all previous ones. Special Thanks goes to DeepSOIC for his tutorial in the forum at [1] as well as this Part-o-Matic which showed me how this works in real live [2] [1]: https://forum.freecadweb.org/viewtopic.php?f=22&t=21097#p163340 [2]: https://github.com/DeepSOIC/Part-o-magic/blob/master/PartOMagic/Features/PartDesign/PDShapeFeature.py --- freecad/gears/commands.py | 2 +- freecad/gears/features.py | 50 ++++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/freecad/gears/commands.py b/freecad/gears/commands.py index a191549..295ae73 100644 --- a/freecad/gears/commands.py +++ b/freecad/gears/commands.py @@ -63,7 +63,7 @@ class BaseCommand(object): cls.GEAR_FUNCTION(obj) if body: - body.Group += [obj] + body.addObject(obj) elif part: part.Group += [obj] else: diff --git a/freecad/gears/features.py b/freecad/gears/features.py index 5bb5c99..6c02ce3 100644 --- a/freecad/gears/features.py +++ b/freecad/gears/features.py @@ -87,6 +87,8 @@ class BaseGear(object): obj.addExtension('Part::AttachExtensionPython') else: obj.addExtension('Part::AttachExtensionPython', obj) + # unveil the "Placement" property, which seems hidden by default in PartDesign + obj.setEditorMode('Placement', 0) #non-readonly non-hidden def execute(self, fp): # checksbackwardcompatibility: @@ -94,7 +96,22 @@ class BaseGear(object): self.make_attachable(fp) fp.positionBySupport() -class InvoluteGear(BaseGear): +class BaseGearNew(BaseGear): + def execute(self, fp): + super(BaseGearNew, self).execute(fp) + self.update_gear_properties(fp) + gear_shape = self.generate_gear_shape(fp) + if hasattr(fp, "BaseFeature") and fp.BaseFeature != None: + # we're inside a PartDesign Body, thus need to fuse with the base feature + gear_shape.Placement = fp.Placement # ensure the gear is placed correctly before fusing + result_shape = fp.BaseFeature.Shape.fuse(gear_shape) + result_shape.transformShape(fp.Placement.inverse().toMatrix(), True) # account for setting fp.Shape below moves the shape to fp.Placement, ignoring its previous placement + fp.Shape = result_shape + else: + fp.Shape = gear_shape + App.Console.PrintLog(f"execute done\n") + +class InvoluteGear(BaseGearNew): """FreeCAD gear""" @@ -165,8 +182,7 @@ class InvoluteGear(BaseGear): obj.addProperty("App::PropertyLength", "df", "computed", "root diameter", 1) - def execute(self, fp): - super(InvoluteGear, self).execute(fp) + def update_gear_properties(self, fp): fp.gear.double_helix = fp.double_helix fp.gear.m_n = fp.module.Value fp.gear.z = fp.teeth @@ -182,6 +198,17 @@ class InvoluteGear(BaseGear): if "properties_from_tool" in fp.PropertiesList: fp.gear.properties_from_tool = fp.properties_from_tool fp.gear._update() + + # computed properties + fp.dw = "{}mm".format(fp.gear.dw) + fp.transverse_pitch = "{}mm".format(fp.gear.pitch) + # checksbackwardcompatibility: + if not "da" in fp.PropertiesList: + self.add_limiting_diameter_properties(fp) + fp.da = "{}mm".format(fp.gear.da) + fp.df = "{}mm".format(fp.gear.df) + + def generate_gear_shape(self, fp): pts = fp.gear.points(num=fp.numpoints) rotated_pts = pts rot = rotation(-fp.gear.phipart) @@ -198,25 +225,16 @@ class InvoluteGear(BaseGear): wi.append(out.toShape()) wi = Wire(wi) if fp.height.Value == 0: - fp.Shape = wi + return wi elif fp.beta.Value == 0: sh = Face(wi) - fp.Shape = sh.extrude(App.Vector(0, 0, fp.height.Value)) + return sh.extrude(App.Vector(0, 0, fp.height.Value)) else: - fp.Shape = helicalextrusion( + return helicalextrusion( wi, fp.height.Value, fp.height.Value * np.tan(fp.gear.beta) * 2 / fp.gear.d, fp.double_helix) else: rw = fp.gear.dw / 2 - fp.Shape = Part.makeCylinder(rw, fp.height.Value) - - # computed properties - fp.dw = "{}mm".format(fp.gear.dw) - fp.transverse_pitch = "{}mm".format(fp.gear.pitch) - # checksbackwardcompatibility: - if not "da" in fp.PropertiesList: - self.add_limiting_diameter_properties(fp) - fp.da = "{}mm".format(fp.gear.da) - fp.df = "{}mm".format(fp.gear.df) + return Part.makeCylinder(rw, fp.height.Value) def __getstate__(self): return None From 9983f5ee61646e8d67c7d6c6d2fef94b7c6525e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Wed, 7 Jul 2021 01:09:16 +0200 Subject: [PATCH 3/4] Extend the "additiveness" to all gears when used in PD:Bodies There are still some issues when the generated Shape is not a solid, e.g. in the preview mode of the Crown Gear. --- freecad/gears/features.py | 86 ++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/freecad/gears/features.py b/freecad/gears/features.py index 6c02ce3..07883bd 100644 --- a/freecad/gears/features.py +++ b/freecad/gears/features.py @@ -95,11 +95,6 @@ class BaseGear(object): if not hasattr(fp, "positionBySupport"): self.make_attachable(fp) fp.positionBySupport() - -class BaseGearNew(BaseGear): - def execute(self, fp): - super(BaseGearNew, self).execute(fp) - self.update_gear_properties(fp) gear_shape = self.generate_gear_shape(fp) if hasattr(fp, "BaseFeature") and fp.BaseFeature != None: # we're inside a PartDesign Body, thus need to fuse with the base feature @@ -109,9 +104,14 @@ class BaseGearNew(BaseGear): fp.Shape = result_shape else: fp.Shape = gear_shape - App.Console.PrintLog(f"execute done\n") -class InvoluteGear(BaseGearNew): + def generate_gear_shape(self, fp): + """ + This method has to return the TopoShape of the gear. + """ + raise "generate_gear_shape not implemented" + +class InvoluteGear(BaseGear): """FreeCAD gear""" @@ -182,7 +182,7 @@ class InvoluteGear(BaseGearNew): obj.addProperty("App::PropertyLength", "df", "computed", "root diameter", 1) - def update_gear_properties(self, fp): + def generate_gear_shape(self, fp): fp.gear.double_helix = fp.double_helix fp.gear.m_n = fp.module.Value fp.gear.z = fp.teeth @@ -208,7 +208,6 @@ class InvoluteGear(BaseGearNew): fp.da = "{}mm".format(fp.gear.da) fp.df = "{}mm".format(fp.gear.df) - def generate_gear_shape(self, fp): pts = fp.gear.points(num=fp.numpoints) rotated_pts = pts rot = rotation(-fp.gear.phipart) @@ -294,8 +293,7 @@ class InvoluteGearRack(BaseGear): self.obj = obj obj.Proxy = self - def execute(self, fp): - super(InvoluteGearRack, self).execute(fp) + def generate_gear_shape(self, fp): fp.rack.m = fp.module.Value fp.rack.z = fp.teeth fp.rack.pressure_angle = fp.pressure_angle.Value * np.pi / 180. @@ -312,13 +310,18 @@ class InvoluteGearRack(BaseGear): if "simplified" in fp.PropertiesList: fp.rack.simplified = fp.simplified fp.rack._update() + + # computed properties + if "transverse_pitch" in fp.PropertiesList: + fp.transverse_pitch = "{} mm".format(fp.rack.compute_properties()[2]) + pts = fp.rack.points() pol = Wire(makePolygon(list(map(fcvec, pts)))) if fp.height.Value == 0: - fp.Shape = pol + return pol elif fp.beta.Value == 0: face = Face(Wire(pol)) - fp.Shape = face.extrude(fcvec([0., 0., fp.height.Value])) + return face.extrude(fcvec([0., 0., fp.height.Value])) elif fp.double_helix: beta = fp.beta.Value * np.pi / 180. pol2 = Part.Wire(pol) @@ -326,16 +329,13 @@ class InvoluteGearRack(BaseGear): fcvec([0., np.tan(beta) * fp.height.Value / 2, fp.height.Value / 2])) pol3 = Part.Wire(pol) pol3.translate(fcvec([0., 0., fp.height.Value])) - fp.Shape = makeLoft([pol, pol2, pol3], True, True) + return makeLoft([pol, pol2, pol3], True, True) else: beta = fp.beta.Value * np.pi / 180. pol2 = Part.Wire(pol) pol2.translate( fcvec([0., np.tan(beta) * fp.height.Value, fp.height.Value])) - fp.Shape = makeLoft([pol, pol2], True) - # computed properties - if "transverse_pitch" in fp.PropertiesList: - fp.transverse_pitch = "{} mm".format(fp.rack.compute_properties()[2]) + return makeLoft([pol, pol2], True) def __getstate__(self): return None @@ -414,8 +414,7 @@ class CrownGear(BaseGear): pts.append(pts[0]) return pts - def execute(self, fp): - super(CrownGear, self).execute(fp) + def generate_gear_shape(self, fp): inner_diameter = fp.module.Value * fp.teeth outer_diameter = inner_diameter + fp.height.Value * 2 inner_circle = Part.Wire(Part.makeCircle(inner_diameter / 2.)) @@ -449,12 +448,12 @@ class CrownGear(BaseGear): for _ in range(t): loft = loft.transformGeometry(rot) cut_shapes.append(loft) - fp.Shape = Part.Compound(cut_shapes) + return Part.Compound(cut_shapes) else: for i in range(t): loft = loft.transformGeometry(rot) solid = solid.cut(loft) - fp.Shape = solid + return solid def __getstate__(self): pass @@ -503,8 +502,7 @@ class CycloidGear(BaseGear): obj.double_helix = False obj.Proxy = self - def execute(self, fp): - super(CycloidGear, self).execute(fp) + def generate_gear_shape(self, fp): fp.gear.m = fp.module.Value fp.gear.z = fp.teeth fp.gear.z1 = fp.inner_diameter.Value @@ -512,6 +510,7 @@ class CycloidGear(BaseGear): fp.gear.clearance = fp.clearance fp.gear.backlash = fp.backlash.Value fp.gear._update() + pts = fp.gear.points(num=fp.numpoints) rotated_pts = pts rot = rotation(-fp.gear.phipart) @@ -527,12 +526,12 @@ class CycloidGear(BaseGear): wi.append(out.toShape()) wi = Wire(wi) if fp.height.Value == 0: - fp.Shape = wi + return wi elif fp.beta.Value == 0: sh = Face(wi) - fp.Shape = sh.extrude(App.Vector(0, 0, fp.height.Value)) + return sh.extrude(App.Vector(0, 0, fp.height.Value)) else: - fp.Shape = helicalextrusion( + return helicalextrusion( wi, fp.height.Value, fp.height.Value * np.tan(fp.beta.Value * np.pi / 180) * 2 / fp.gear.d, fp.double_helix) def __getstate__(self): @@ -587,8 +586,7 @@ class BevelGear(BaseGear): self.obj = obj obj.Proxy = self - def execute(self, fp): - super(BevelGear, self).execute(fp) + def generate_gear_shape(self, fp): fp.gear.z = fp.teeth fp.gear.module = fp.module.Value fp.gear.pressure_angle = (90 - fp.pressure_angle.Value) * np.pi / 180. @@ -636,8 +634,8 @@ class BevelGear(BaseGear): mat.A33 = -1 mat.move(fcvec([0, 0, scale_1])) shape = shape.transformGeometry(mat) - fp.Shape = shape - # fp.Shape = self.create_teeth(pts, pos1, fp.teeth) + return shape + # return self.create_teeth(pts, pos1, fp.teeth) def create_tooth(self): w = [] @@ -710,8 +708,7 @@ class WormGear(BaseGear): self.obj = obj obj.Proxy = self - def execute(self, fp): - super(WormGear, self).execute(fp) + def generate_gear_shape(self, fp): m = fp.module.Value d = fp.diameter.Value t = fp.teeth @@ -779,12 +776,12 @@ class WormGear(BaseGear): full_wire = Part.Wire(Part.Wire(w_all)) if h == 0: - fp.Shape = full_wire + return full_wire else: shape = helicalextrusion(full_wire, h, h * np.tan(beta) * 2 / d) - fp.Shape = shape + return shape def __getstate__(self): return None @@ -841,8 +838,7 @@ class TimingGear(BaseGear): self.obj = obj obj.Proxy = self - def execute(self, fp): - super(TimingGear, self).execute(fp) + def generate_gear_shape(self, fp): # m ... center of arc/circle # r ... radius of arc/circle # x ... end-point of arc @@ -920,9 +916,9 @@ class TimingGear(BaseGear): wi = Part.Wire(wires) if fp.height.Value == 0: - fp.Shape = wi + return wi else: - fp.Shape = Part.Face(wi).extrude(App.Vector(0, 0, fp.height)) + return Part.Face(wi).extrude(App.Vector(0, 0, fp.height)) def __getstate__(self): pass @@ -957,8 +953,7 @@ class LanternGear(BaseGear): self.obj = obj obj.Proxy = self - def execute(self, fp): - super(LanternGear, self).execute(fp) + def generate_gear_shape(self, fp): m = fp.module.Value teeth = fp.teeth r_r = fp.bolt_radius.Value @@ -1013,9 +1008,9 @@ class LanternGear(BaseGear): wi = Part.Wire(wires) if fp.height.Value == 0: - fp.Shape = wi + return wi else: - fp.Shape = Part.Face(wi).extrude(App.Vector(0, 0, fp.height)) + return Part.Face(wi).extrude(App.Vector(0, 0, fp.height)) def __getstate__(self): pass @@ -1108,8 +1103,7 @@ class HypoCycloidGear(BaseGear): x, y = self.to_rect(r, a) return x, y - def execute(self,fp): - super(HypoCycloidGear, self).execute(fp) + def generate_gear_shape(self, fp): b = fp.pin_circle_radius d = fp.roller_diameter e = fp.eccentricity @@ -1216,7 +1210,7 @@ class HypoCycloidGear(BaseGear): to_be_fused.append(pins); if to_be_fused: - fp.Shape = Part.makeCompound(to_be_fused) + return Part.makeCompound(to_be_fused) def __getstate__(self): pass From 37b99b119d6d868488d83cb8575acd90c571dc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Thu, 8 Jul 2021 23:23:36 +0200 Subject: [PATCH 4/4] Fix crown gear preview mode in PartDesign Bodies Previously, the `preview_mode` of the crown gear returned a compound of the base and the cut-outs. This caused problems in PD::Bodies where a single solid is requried. The solution in this commit changes the preview_mode to only output the base, not generating the cutout shapes at all. This is consistent with the involute gears having "simple=true" and saves again 0.5 Seconds processsing time on my system using defaults (15 teeth, 4 loft profiles). In addition, "preview = false" is also speed up by first collecting all cut-outs, and then passing them all at once to a single cut operation. This reduced the cutting time from 3.0 Seconds to 2.2 Seconds here. So preview now generats the shape immediately (0.0008s vs 0.5s) and the actual crown is generated in 2.7s instead of 3.5s (again, using the defaut parameters, measued via Python's time.perf_counter). --- freecad/gears/features.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/freecad/gears/features.py b/freecad/gears/features.py index 07883bd..ce7d495 100644 --- a/freecad/gears/features.py +++ b/freecad/gears/features.py @@ -422,6 +422,8 @@ class CrownGear(BaseGear): inner_circle.reverse() face = Part.Face([outer_circle, inner_circle]) solid = face.extrude(App.Vector([0., 0., -fp.thickness.Value])) + if fp.preview_mode: + return solid # cutting obj alpha_w = np.deg2rad(fp.pressure_angle.Value) @@ -443,17 +445,11 @@ class CrownGear(BaseGear): loft = makeLoft(polies, True) rot = App.Matrix() rot.rotateZ(2 * np.pi / t) - if fp.preview_mode: - cut_shapes = [solid] - for _ in range(t): - loft = loft.transformGeometry(rot) - cut_shapes.append(loft) - return Part.Compound(cut_shapes) - else: - for i in range(t): - loft = loft.transformGeometry(rot) - solid = solid.cut(loft) - return solid + cut_shapes = [] + for _ in range(t): + loft = loft.transformGeometry(rot) + cut_shapes.append(loft) + return solid.cut(cut_shapes) def __getstate__(self): pass