diff --git a/freecad/gears/commands.py b/freecad/gears/commands.py index 977f04c..56396fc 100644 --- a/freecad/gears/commands.py +++ b/freecad/gears/commands.py @@ -57,11 +57,9 @@ class BaseCommand(object): def Activated(self): gui.doCommandGui("import freecad.gears.commands") - app.ActiveDocument.openTransaction("Create gear") gui.doCommandGui( "freecad.gears.commands.{}.create()".format(self.__class__.__name__) ) - app.ActiveDocument.commitTransaction() app.ActiveDocument.recompute() gui.SendMsgToActiveView("ViewFit") @@ -216,23 +214,64 @@ class CreateGearConnector(BaseCommand): if len(selection) != 2: raise ValueError( - app.Qt.translate("Log", "Please select two gear objects.") + app.Qt.translate("Log", "Please select two objects (gear+gear or connector+gear).") ) - for obj in selection: - if not isinstance(obj.Proxy, BaseGear): + # Check if first selection is a GearConnector (for chaining) + if isinstance(selection[0].Proxy, GearConnector): + parent_connector = selection[0] + master_gear = parent_connector.slave_gear + slave_gear = selection[1] + + # Validate that slave is a gear + if not isinstance(slave_gear.Proxy, BaseGear): raise TypeError( - app.Qt.translate("Log", "Selected object is not a gear.") + app.Qt.translate("Log", "Second selection must be a gear object.") ) - obj = app.ActiveDocument.addObject("Part::FeaturePython", self.NAME) - GearConnector(obj, selection[0], selection[1]) - ViewProviderGearConnector(obj.ViewObject) + # Create the chained connector + obj = app.ActiveDocument.addObject("Part::FeaturePython", self.NAME) + GearConnector(obj, master_gear, slave_gear) + ViewProviderGearConnector(obj.ViewObject) + + # Add parent_connector as a link property to create explicit dependency + if not hasattr(obj, 'parent_connector'): + obj.addProperty( + "App::PropertyLink", + "parent_connector", + "gear", + app.Qt.translate("App::Property", "Parent connector for chained gear trains"), + 1, # Read-only + ) + obj.parent_connector = parent_connector + + # Auto-sync angle1 to master gear's actual rotation angle + # This ensures the gear train rotates in sync based on actual gear rotations + obj.setExpression('angle1', f'{master_gear.Name}.Placement.Rotation.Angle * 180 / pi') + + # Inherit stationary state from parent (both default to True now) + if hasattr(parent_connector, 'slave_gear_stationary'): + obj.master_gear_stationary = parent_connector.slave_gear_stationary + if hasattr(parent_connector, 'master_gear_stationary'): + obj.slave_gear_stationary = parent_connector.slave_gear_stationary + + app.Console.PrintMessage( + f"Created chained GearConnector: {parent_connector.Name} -> {obj.Name}\n" + ) + else: + # Original behavior: two gears selected + for obj_sel in selection: + if not isinstance(obj_sel.Proxy, BaseGear): + raise TypeError( + app.Qt.translate("Log", "Selected objects must be gears.") + ) + + obj = app.ActiveDocument.addObject("Part::FeaturePython", self.NAME) + GearConnector(obj, selection[0], selection[1]) + ViewProviderGearConnector(obj.ViewObject) app.ActiveDocument.recompute() return obj except Exception as e: app.Console.PrintError(f"Error: {str(e)}\n") return None - - diff --git a/freecad/gears/connector.py b/freecad/gears/connector.py index fd20941..821353e 100644 --- a/freecad/gears/connector.py +++ b/freecad/gears/connector.py @@ -100,14 +100,36 @@ class GearConnector(object): QT_TRANSLATE_NOOP("App::Property", "angle at which second gear is placed"), 1, ) + obj.addProperty( + "App::PropertyBool", + "master_gear_stationary", + "gear", + QT_TRANSLATE_NOOP("App::Property", "master gear position is fixed (does not orbit)"), + 0, + ) + obj.addProperty( + "App::PropertyBool", + "slave_gear_stationary", + "gear", + QT_TRANSLATE_NOOP("App::Property", "slave gear position is fixed (does not orbit)"), + 0, + ) obj.version = __version__ obj.master_gear = master_gear obj.slave_gear = slave_gear obj.angle1 = 0 obj.angle2 = 0 + obj.master_gear_stationary = True + obj.slave_gear_stationary = True obj.Proxy = self def onChanged(self, fp, prop): + # Guard: Check if gears are initialized + if not hasattr(fp, 'master_gear') or not hasattr(fp, 'slave_gear'): + return + if fp.master_gear is None or fp.slave_gear is None: + return + # fp.angle2 = fp.master_gear.Placement.Rotation.Angle if isinstance(fp.master_gear.Proxy, InvoluteGear) and isinstance( fp.slave_gear.Proxy, InvoluteGear @@ -128,20 +150,64 @@ class GearConnector(object): fp.slave_gear.shift, ) - mat0 = app.Matrix() # unity matrix - trans = app.Vector(dist) - mat0.move(trans) - rot = app.Rotation(app.Vector(0, 0, 1), fp.angle1).toMatrix() - angle2 = dw_master / dw_slave * fp.angle1.Value - angle4 = dw_master / dw_slave * np.rad2deg(angle_master) - rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() - angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth - rot3 = app.Rotation(app.Vector(0, 0, 1), angle3).toMatrix() - rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() - mat1 = rot * mat0 * rot2 * rot3 * rot4 - mat1.move(fp.master_gear.Placement.Base) - fp.slave_gear.Placement = mat1 - fp.slave_gear.purgeTouched() + # Check if we have the stationary properties (for backward compatibility) + master_stationary = getattr(fp, 'master_gear_stationary', True) + slave_stationary = getattr(fp, 'slave_gear_stationary', False) + + if master_stationary and slave_stationary: + # Both gears stay at their positions, only rotate in place + # Calculate slave position: offset from master by the meshing distance + slave_position = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0) + + # Master rotates by angle1 (only if not controlled by a parent connector) + if not hasattr(fp, 'parent_connector') or fp.parent_connector is None: + rot_master = app.Rotation(app.Vector(0, 0, 1), fp.angle1.Value) + fp.master_gear.Placement = app.Placement(fp.master_gear.Placement.Base, rot_master) + fp.master_gear.purgeTouched() + + # Slave gets positioned at correct distance and rotates based on gear ratio + angle_slave = dw_master / dw_slave * fp.angle1.Value + angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth + rot_slave = app.Rotation(app.Vector(0, 0, 1), -angle_slave + angle3) + fp.slave_gear.Placement = app.Placement(slave_position, rot_slave) + fp.slave_gear.purgeTouched() + + elif master_stationary and not slave_stationary: + # Original behavior: slave gear orbits around master + mat0 = app.Matrix() # unity matrix + trans = app.Vector(dist) + mat0.move(trans) + rot = app.Rotation(app.Vector(0, 0, 1), fp.angle1).toMatrix() + angle2 = dw_master / dw_slave * fp.angle1.Value + angle4 = dw_master / dw_slave * np.rad2deg(angle_master) + rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() + angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth + rot3 = app.Rotation(app.Vector(0, 0, 1), angle3).toMatrix() + rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() + mat1 = rot * mat0 * rot2 * rot3 * rot4 + mat1.move(fp.master_gear.Placement.Base) + fp.slave_gear.Placement = mat1 + fp.slave_gear.purgeTouched() + + elif not master_stationary and slave_stationary: + # Master orbits around slave (inverse behavior) + mat0 = app.Matrix() # unity matrix + trans = app.Vector(dist) + mat0.move(trans) + rot = app.Rotation(app.Vector(0, 0, 1), -fp.angle1).toMatrix() # negative angle for reverse orbit + angle2 = -dw_slave / dw_master * fp.angle1.Value # master's rotation based on orbital motion + angle_slave = fp.slave_gear.Placement.Rotation.Angle * sum( + fp.slave_gear.Placement.Rotation.Axis + ) + angle4 = -dw_slave / dw_master * np.rad2deg(angle_slave) # additional rotation from slave's current angle + rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() + rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() + mat1 = rot * mat0 * rot2 * rot4 + mat1.move(fp.slave_gear.Placement.Base) + fp.master_gear.Placement = mat1 + fp.master_gear.purgeTouched() + + # else: both not stationary - no action needed if isinstance(fp.master_gear.Proxy, InternalInvoluteGear) and isinstance( fp.slave_gear.Proxy, InvoluteGear @@ -162,20 +228,64 @@ class GearConnector(object): fp.slave_gear.shift, ) - mat0 = app.Matrix() # unity matrix - trans = app.Vector(dist) - mat0.move(trans) - rot = app.Rotation(app.Vector(0, 0, 1), fp.angle1).toMatrix() - angle2 = -dw_master / dw_slave * fp.angle1.Value - angle4 = -dw_master / dw_slave * np.rad2deg(angle_master) - rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() - angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth - rot3 = app.Rotation(app.Vector(0, 0, 1), angle3).toMatrix() - rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() - mat1 = rot * mat0 * rot2 * rot3 * rot4 - mat1.move(fp.master_gear.Placement.Base) - fp.slave_gear.Placement = mat1 - fp.slave_gear.purgeTouched() + # Check if we have the stationary properties (for backward compatibility) + master_stationary = getattr(fp, 'master_gear_stationary', True) + slave_stationary = getattr(fp, 'slave_gear_stationary', False) + + if master_stationary and slave_stationary: + # Both gears stay at their positions, only rotate in place + # Calculate slave position: offset from master by the meshing distance (internal gear) + slave_position = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0) + + # Master rotates by angle1 (only if not controlled by a parent connector) + if not hasattr(fp, 'parent_connector') or fp.parent_connector is None: + rot_master = app.Rotation(app.Vector(0, 0, 1), fp.angle1.Value) + fp.master_gear.Placement = app.Placement(fp.master_gear.Placement.Base, rot_master) + fp.master_gear.purgeTouched() + + # Slave gets positioned at correct distance and rotates based on gear ratio (internal gear reverses direction) + angle_slave = -dw_master / dw_slave * fp.angle1.Value + angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth + rot_slave = app.Rotation(app.Vector(0, 0, 1), -angle_slave + angle3) + fp.slave_gear.Placement = app.Placement(slave_position, rot_slave) + fp.slave_gear.purgeTouched() + + elif master_stationary and not slave_stationary: + # Original behavior: slave gear orbits around master (inside internal gear) + mat0 = app.Matrix() # unity matrix + trans = app.Vector(dist) + mat0.move(trans) + rot = app.Rotation(app.Vector(0, 0, 1), fp.angle1).toMatrix() + angle2 = -dw_master / dw_slave * fp.angle1.Value + angle4 = -dw_master / dw_slave * np.rad2deg(angle_master) + rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() + angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth + rot3 = app.Rotation(app.Vector(0, 0, 1), angle3).toMatrix() + rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() + mat1 = rot * mat0 * rot2 * rot3 * rot4 + mat1.move(fp.master_gear.Placement.Base) + fp.slave_gear.Placement = mat1 + fp.slave_gear.purgeTouched() + + elif not master_stationary and slave_stationary: + # Master orbits around slave (inverse behavior) + mat0 = app.Matrix() # unity matrix + trans = app.Vector(dist) + mat0.move(trans) + rot = app.Rotation(app.Vector(0, 0, 1), -fp.angle1).toMatrix() + angle2 = -dw_slave / dw_master * fp.angle1.Value + angle_slave = fp.slave_gear.Placement.Rotation.Angle * sum( + fp.slave_gear.Placement.Rotation.Axis + ) + angle4 = -dw_slave / dw_master * np.rad2deg(angle_slave) + rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() + rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() + mat1 = rot * mat0 * rot2 * rot4 + mat1.move(fp.slave_gear.Placement.Base) + fp.master_gear.Placement = mat1 + fp.master_gear.purgeTouched() + + # else: both not stationary - no action needed if ( isinstance(fp.master_gear.Proxy, InvoluteGear) @@ -211,20 +321,65 @@ class GearConnector(object): dw_master = fp.master_gear.pitch_diameter.Value dw_slave = fp.slave_gear.pitch_diameter.Value dist = (dw_master + dw_slave) / 2 - mat0 = app.Matrix() # unity matrix - trans = app.Vector(dist, 0, 0) - mat0.move(trans) - rot = app.Rotation(app.Vector(0, 0, 1), fp.angle1).toMatrix() - angle2 = dw_master / dw_slave * fp.angle1.Value - angle4 = dw_master / dw_slave * np.rad2deg(angle_master) - rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() - angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth - rot3 = app.Rotation(app.Vector(0, 0, 1), angle3).toMatrix() - rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() - mat1 = rot * mat0 * rot2 * rot3 * rot4 - mat1.move(fp.master_gear.Placement.Base) - fp.slave_gear.Placement = mat1 - fp.slave_gear.purgeTouched() + + # Check if we have the stationary properties (for backward compatibility) + master_stationary = getattr(fp, 'master_gear_stationary', True) + slave_stationary = getattr(fp, 'slave_gear_stationary', False) + + if master_stationary and slave_stationary: + # Both gears stay at their positions, only rotate in place + # Calculate slave position: offset from master by the meshing distance + slave_position = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0) + + # Master rotates by angle1 (only if not controlled by a parent connector) + if not hasattr(fp, 'parent_connector') or fp.parent_connector is None: + rot_master = app.Rotation(app.Vector(0, 0, 1), fp.angle1.Value) + fp.master_gear.Placement = app.Placement(fp.master_gear.Placement.Base, rot_master) + fp.master_gear.purgeTouched() + + # Slave gets positioned at correct distance and rotates based on gear ratio + angle_slave = dw_master / dw_slave * fp.angle1.Value + angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth + rot_slave = app.Rotation(app.Vector(0, 0, 1), -angle_slave + angle3) + fp.slave_gear.Placement = app.Placement(slave_position, rot_slave) + fp.slave_gear.purgeTouched() + + elif master_stationary and not slave_stationary: + # Original behavior: slave gear orbits around master + mat0 = app.Matrix() # unity matrix + trans = app.Vector(dist, 0, 0) + mat0.move(trans) + rot = app.Rotation(app.Vector(0, 0, 1), fp.angle1).toMatrix() + angle2 = dw_master / dw_slave * fp.angle1.Value + angle4 = dw_master / dw_slave * np.rad2deg(angle_master) + rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() + angle3 = abs(fp.slave_gear.num_teeth % 2 - 1) * 180.0 / fp.slave_gear.num_teeth + rot3 = app.Rotation(app.Vector(0, 0, 1), angle3).toMatrix() + rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() + mat1 = rot * mat0 * rot2 * rot3 * rot4 + mat1.move(fp.master_gear.Placement.Base) + fp.slave_gear.Placement = mat1 + fp.slave_gear.purgeTouched() + + elif not master_stationary and slave_stationary: + # Master orbits around slave (inverse behavior) + mat0 = app.Matrix() # unity matrix + trans = app.Vector(dist, 0, 0) + mat0.move(trans) + rot = app.Rotation(app.Vector(0, 0, 1), -fp.angle1).toMatrix() + angle2 = -dw_slave / dw_master * fp.angle1.Value # master's rotation based on orbital motion + angle_slave = fp.slave_gear.Placement.Rotation.Angle * sum( + fp.slave_gear.Placement.Rotation.Axis + ) + angle4 = -dw_slave / dw_master * np.rad2deg(angle_slave) + rot2 = app.Rotation(app.Vector(0, 0, 1), angle2).toMatrix() + rot4 = app.Rotation(app.Vector(0, 0, 1), -angle4).toMatrix() + mat1 = rot * mat0 * rot2 * rot4 + mat1.move(fp.slave_gear.Placement.Base) + fp.master_gear.Placement = mat1 + fp.master_gear.purgeTouched() + + # else: both not stationary - no action needed def execute(self, fp): self.onChanged(fp, None)