Gear and Chain Connector getting closer

This commit is contained in:
Chris Bruner
2025-11-04 03:40:08 -05:00
committed by lorenz
parent 9ed85f6766
commit 152a70a4eb
3 changed files with 381 additions and 80 deletions

View File

@@ -0,0 +1,303 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This file defines the ChainConnector class for stable multi-gear chains. *
# * *
# * (GNU GPL Header retained)
# * *
# ***************************************************************************
import numpy as np
from freecad import app
from pygears import __version__ # FIX: Import __version__
# Import all gear types the chain might connect
from .involutegear import InvoluteGear
from .internalinvolutegear import InternalInvoluteGear
from .cycloidgear import CycloidGear
# Reuse the standard ViewProvider from the original connector.py
from .connector import ViewProviderGearConnector, GearConnector
QT_TRANSLATE_NOOP = app.Qt.QT_TRANSLATE_NOOP
class Chain(object):
def __init__(self, obj, gear_list):
obj.addProperty("App::PropertyLinkList", "gear_list", "GearChain", "list of gears in the chain")
obj.addProperty("App::PropertyAngle", "angle", "GearChain", "angle of the first gear")
obj.gear_list = gear_list
obj.Proxy = self
self.obj = obj
ViewProviderGearConnector(obj.ViewObject)
# Create connectors
master_gear = gear_list[0]
for i, slave_gear in enumerate(gear_list[1:]):
if i == 0:
connector = app.ActiveDocument.addObject("Part::FeaturePython", "GearConnector")
GearConnector(connector, master_gear, slave_gear)
connector.angle1 = self.obj.angle
self.obj.addProperty("App::PropertyLink", "connector", "GearChain", "main connector")
self.obj.connector = connector
else:
connector = app.ActiveDocument.addObject("Part::FeaturePython", "ChainConnector")
ChainConnector(connector, self.obj.connector, slave_gear)
master_gear = slave_gear
def onChanged(self, fp, prop):
if prop == 'angle':
fp.connector.angle1 = fp.angle
class Chain(object):
def __init__(self, obj, gear_list):
obj.addProperty("App::PropertyLinkList", "gear_list", "GearChain", "list of gears in the chain")
obj.addProperty("App::PropertyAngle", "angle", "GearChain", "angle of the first gear")
obj.gear_list = gear_list
obj.Proxy = self
self.obj = obj
ViewProviderGearConnector(obj.ViewObject)
# Create connectors
master_gear = gear_list[0]
for i, slave_gear in enumerate(gear_list[1:]):
if i == 0:
connector = app.ActiveDocument.addObject("Part::FeaturePython", "GearConnector")
GearConnector(connector, master_gear, slave_gear)
connector.angle1 = self.obj.angle
self.obj.addProperty("App::PropertyLink", "connector", "GearChain", "main connector")
self.obj.connector = connector
else:
connector = app.ActiveDocument.addObject("Part::FeaturePython", "ChainConnector")
ChainConnector(connector, self.obj.connector, slave_gear)
master_gear = slave_gear
def onChanged(self, fp, prop):
if prop == 'angle':
fp.connector.angle1 = fp.angle
class ChainConnector(object):
def __init__(self, obj, master_connector, slave_gear):
# --- PROPERTY DEFINITIONS ---
obj.addProperty("App::PropertyString", "version", "version",
QT_TRANSLATE_NOOP("App::Property", "freecad.gears-version"), 1)
obj.addProperty("App::PropertyLink", "input_connector", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "Previous connector in the chain"), 1)
obj.addProperty("App::PropertyLink", "master_gear", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "The shared master gear (G2)"), 8) # Read-only link to G2
obj.addProperty("App::PropertyLink", "slave_gear", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "The new slave gear (G3)"), 1)
obj.addProperty("App::PropertyAngle", "input_gear_angle", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "Calculated rotation angle of the shared gear (G2)"), 8)
# FIX 2: Add version property assignment
obj.version = __version__
obj.input_connector = master_connector
obj.slave_gear = slave_gear
obj.Proxy = self
# CRITICAL FIX: Attach the ViewProvider to ensure visibility
ViewProviderGearConnector(obj.ViewObject)
# 1. Link master_gear (G2) property to the slave_gear of the previous connector (GC1.slave_gear)
obj.setExpression('master_gear', f'{master_connector.Name}.slave_gear')
# 2. Link the input_gear_angle to G2's rotation. This triggers onChanged whenever G2 moves.
# We convert radians (from .Angle) to degrees for consistency, though
# onChanged will convert it back. Or we can just use the raw Angle.
obj.setExpression('input_gear_angle', f'{master_connector.Name}.slave_gear.Placement.Rotation.Angle')
def onChanged(self, fp, prop):
# Only react when the input angle (G2's rotation) or the final slave gear (G3) link changes
if prop not in ('input_gear_angle', 'slave_gear'):
return
# Ensure we have both gears before proceeding
if fp.master_gear is None or fp.slave_gear is None:
return
# input_gear_angle is linked to Placement.Rotation.Angle, which is in RADIANS
master_angle_rad = fp.input_gear_angle.Value
master_angle_deg = np.rad2deg(master_angle_rad) # Convert to degrees
dw_master = fp.master_gear.pitch_diameter.Value
dw_slave = fp.slave_gear.pitch_diameter.Value
# --- Involute Gear Pair Logic ---
if isinstance(fp.master_gear.Proxy, InvoluteGear) and isinstance(fp.slave_gear.Proxy, InvoluteGear):
dist = (dw_master + dw_slave) / 2
slave_pos = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0)
# Kinematics: Calculate slave rotation (opposite direction)
angle_slave_deg = -(dw_master / dw_slave) * master_angle_deg
# Apply rotation and position to G3 (slave_gear)
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_deg + angle3)
fp.slave_gear.Placement = app.Placement(slave_pos, rot_slave)
fp.slave_gear.purgeTouched()
# --- Internal Involute Gear Logic ---
elif isinstance(fp.master_gear.Proxy, InternalInvoluteGear) and isinstance(fp.slave_gear.Proxy, InvoluteGear):
dist = (dw_master - dw_slave) / 2
slave_pos = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0)
# Kinematics: Calculate slave rotation (same direction)
angle_slave_deg = (dw_master / dw_slave) * master_angle_deg
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_deg + angle3)
fp.slave_gear.Placement = app.Placement(slave_pos, rot_slave)
fp.slave_gear.purgeTouched()
# --- Cycloid Gear Pair Logic ---
elif isinstance(fp.master_gear.Proxy, CycloidGear) and isinstance(fp.slave_gear.Proxy, CycloidGear):
dist = (dw_master + dw_slave) / 2
slave_pos = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0)
# Kinematics: Calculate slave rotation (opposite direction)
angle_slave_deg = -(dw_master / dw_slave) * master_angle_deg
# Apply rotation and position to G3 (slave_gear)
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_deg + angle3)
fp.slave_gear.Placement = app.Placement(slave_pos, rot_slave)
fp.slave_gear.purgeTouched()
# [Add Rack Logic here if chains need to drive racks]
def execute(self, fp):
# When executed, simply trigger the onChanged to use the current expression-linked value
self.onChanged(fp, 'input_gear_angle')
class GearDev(object):
def __init__(self, obj, gear_list):
obj.addProperty("App::PropertyLinkList", "gear_list", "GearChain", "list of gears in the chain")
obj.addProperty("App::PropertyAngle", "angle", "GearChain", "angle of the first gear")
obj.gear_list = gear_list
obj.Proxy = self
self.obj = obj
ViewProviderGearConnector(obj.ViewObject)
# Create connectors
master_gear = gear_list[0]
for i, slave_gear in enumerate(gear_list[1:]):
if i == 0:
connector = app.ActiveDocument.addObject("Part::FeaturePython", "GearConnector")
GearConnector(connector, master_gear, slave_gear)
connector.angle1 = self.obj.angle
self.obj.addProperty("App::PropertyLink", "connector", "GearChain", "main connector")
self.obj.connector = connector
else:
connector = app.ActiveDocument.addObject("Part::FeaturePython", "ChainConnector")
ChainConnector(connector, self.obj.connector, slave_gear)
master_gear = slave_gear
def onChanged(self, fp, prop):
if prop == 'angle':
fp.connector.angle1 = fp.angle
def __init__(self, obj, master_connector, slave_gear):
# --- PROPERTY DEFINITIONS ---
obj.addProperty("App::PropertyString", "version", "version",
QT_TRANSLATE_NOOP("App::Property", "freecad.gears-version"), 1)
obj.addProperty("App::PropertyLink", "input_connector", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "Previous connector in the chain"), 1)
obj.addProperty("App::PropertyLink", "master_gear", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "The shared master gear (G2)"), 8) # Read-only link to G2
obj.addProperty("App::PropertyLink", "slave_gear", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "The new slave gear (G3)"), 1)
obj.addProperty("App::PropertyAngle", "input_gear_angle", "GearChain",
QT_TRANSLATE_NOOP("App::Property", "Calculated rotation angle of the shared gear (G2)"), 8)
# FIX 2: Add version property assignment
obj.version = __version__
obj.input_connector = master_connector
obj.slave_gear = slave_gear
obj.Proxy = self
# CRITICAL FIX: Attach the ViewProvider to ensure visibility
ViewProviderGearConnector(obj.ViewObject)
# 1. Link master_gear (G2) property to the slave_gear of the previous connector (GC1.slave_gear)
obj.setExpression('master_gear', f'{master_connector.Name}.slave_gear')
# 2. Link the input_gear_angle to G2's rotation. This triggers onChanged whenever G2 moves.
# We convert radians (from .Angle) to degrees for consistency, though
# onChanged will convert it back. Or we can just use the raw Angle.
obj.setExpression('input_gear_angle', f'{master_connector.Name}.slave_gear.Placement.Rotation.Angle')
def onChanged(self, fp, prop):
# Only react when the input angle (G2's rotation) or the final slave gear (G3) link changes
if prop not in ('input_gear_angle', 'slave_gear'):
return
# Ensure we have both gears before proceeding
if fp.master_gear is None or fp.slave_gear is None:
return
# input_gear_angle is linked to Placement.Rotation.Angle, which is in RADIANS
master_angle_rad = fp.input_gear_angle.Value
master_angle_deg = np.rad2deg(master_angle_rad) # Convert to degrees
dw_master = fp.master_gear.pitch_diameter.Value
dw_slave = fp.slave_gear.pitch_diameter.Value
# --- Involute Gear Pair Logic ---
if isinstance(fp.master_gear.Proxy, InvoluteGear) and isinstance(fp.slave_gear.Proxy, InvoluteGear):
dist = (dw_master + dw_slave) / 2
slave_pos = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0)
# Kinematics: Calculate slave rotation (opposite direction)
angle_slave_deg = -(dw_master / dw_slave) * master_angle_deg
# Apply rotation and position to G3 (slave_gear)
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_deg + angle3)
fp.slave_gear.Placement = app.Placement(slave_pos, rot_slave)
fp.slave_gear.purgeTouched()
# --- Internal Involute Gear Logic ---
elif isinstance(fp.master_gear.Proxy, InternalInvoluteGear) and isinstance(fp.slave_gear.Proxy, InvoluteGear):
dist = (dw_master - dw_slave) / 2
slave_pos = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0)
# Kinematics: Calculate slave rotation (same direction)
angle_slave_deg = (dw_master / dw_slave) * master_angle_deg
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_deg + angle3)
fp.slave_gear.Placement = app.Placement(slave_pos, rot_slave)
fp.slave_gear.purgeTouched()
# --- Cycloid Gear Pair Logic ---
elif isinstance(fp.master_gear.Proxy, CycloidGear) and isinstance(fp.slave_gear.Proxy, CycloidGear):
dist = (dw_master + dw_slave) / 2
slave_pos = fp.master_gear.Placement.Base + app.Vector(dist, 0, 0)
# Kinematics: Calculate slave rotation (opposite direction)
angle_slave_deg = -(dw_master / dw_slave) * master_angle_deg
# Apply rotation and position to G3 (slave_gear)
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_deg + angle3)
fp.slave_gear.Placement = app.Placement(slave_pos, rot_slave)
fp.slave_gear.purgeTouched()
# [Add Rack Logic here if chains need to drive racks]
def execute(self, fp):
# When executed, simply trigger the onChanged to use the current expression-linked value
self.onChanged(fp, 'input_gear_angle')

View File

@@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * *
# * 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 General Public License for more details. *
# * *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# * *
# ***************************************************************************
import os
@@ -36,7 +36,9 @@ from .lanterngear import LanternGear
from .hypocycloidgear import HypoCycloidGear
# CRITICAL CHANGE: Import both connector types
from .connector import GearConnector, ViewProviderGearConnector
from .chainconnector import ChainConnector, Chain
QT_TRANSLATE_NOOP = app.Qt.QT_TRANSLATE_NOOP
@@ -217,49 +219,35 @@ class CreateGearConnector(BaseCommand):
app.Qt.translate("Log", "Please select two objects (gear+gear or connector+gear).")
)
# Check if first selection is a GearConnector (for chaining)
if isinstance(selection[0].Proxy, GearConnector):
# Get the proxy types for the two selected objects
selection0_proxy = selection[0].Proxy if hasattr(selection[0], 'Proxy') else None
selection1_proxy = selection[1].Proxy if hasattr(selection[1], 'Proxy') else None
# Identify the parent connector and the new slave gear
parent_connector = None
slave_gear = None
# Case 1: Connector (GC1) selected first, Gear (G3) second
if isinstance(selection0_proxy, GearConnector) and isinstance(selection1_proxy, BaseGear):
parent_connector = selection[0]
master_gear = parent_connector.slave_gear
slave_gear = selection[1]
# Case 2: Gear (G3) selected first, Connector (GC1) second
elif isinstance(selection1_proxy, GearConnector) and isinstance(selection0_proxy, BaseGear):
parent_connector = selection[1]
slave_gear = selection[0]
# Validate that slave is a gear
if not isinstance(slave_gear.Proxy, BaseGear):
raise TypeError(
app.Qt.translate("Log", "Second selection must be a gear object.")
)
# Create the chained connector
obj = app.ActiveDocument.addObject("Part::FeaturePython", self.NAME)
GearConnector(obj, master_gear, slave_gear)
# --- CRITICAL DECISION POINT ---
if parent_connector is not None:
# Import the ChainConnector class (we already imported it at the top)
# Chain Creation: Create the dedicated ChainConnector
obj = app.ActiveDocument.addObject("Part::FeaturePython", "ChainConnector")
ChainConnector(obj, parent_connector, 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
# Standard Creation: Two gears selected (G1 and G2)
for obj_sel in selection:
if not isinstance(obj_sel.Proxy, BaseGear):
raise TypeError(
@@ -275,3 +263,4 @@ class CreateGearConnector(BaseCommand):
except Exception as e:
app.Console.PrintError(f"Error: {str(e)}\n")
return None

View File

@@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * *
# * 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 General Public License for more details. *
# * *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# * *
# ***************************************************************************
import os
@@ -64,6 +64,8 @@ class ViewProviderGearConnector(object):
class GearConnector(object):
_recomputing = False
def __init__(self, obj, master_gear, slave_gear):
obj.addProperty(
"App::PropertyString",
@@ -122,15 +124,29 @@ class GearConnector(object):
obj.master_gear_stationary = True
obj.slave_gear_stationary = True
obj.Proxy = self
# FIX 1: Attach ViewProvider to ensure visibility (fixes grey-out)
ViewProviderGearConnector(obj.ViewObject)
def onChanged(self, fp, prop):
if self._recomputing:
return
# 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
# FIX 3: This connector is *always* driven by its angle1 property.
# Removing the 'if prop == angle1' check ensures it runs on
# manual changes (prop='angle1') AND on document recompute (prop=None).
# This provides a stable position for G2, which fixes the chain.
master_angle = fp.angle1.Value # Angle in degrees
# ====================================================================
# INVOLUTE GEAR TO INVOLUTE GEAR
# ====================================================================
if isinstance(fp.master_gear.Proxy, InvoluteGear) and isinstance(
fp.slave_gear.Proxy, InvoluteGear
):
@@ -156,19 +172,19 @@ class GearConnector(object):
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()
# 1. Rotate G1 (master)
rot_master = app.Rotation(app.Vector(0, 0, 1), master_angle)
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
# 2. Calculate G2 (slave) rotation
angle_slave = dw_master / dw_slave * master_angle
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)
# 3. Set G2's placement. This triggers the ChainConnector via Expression link.
fp.slave_gear.Placement = app.Placement(slave_position, rot_slave)
fp.slave_gear.purgeTouched()
@@ -212,6 +228,7 @@ class GearConnector(object):
if isinstance(fp.master_gear.Proxy, InternalInvoluteGear) and isinstance(
fp.slave_gear.Proxy, InvoluteGear
):
# Internal gear logic remains unchanged
angle_master = fp.master_gear.Placement.Rotation.Angle * sum(
fp.master_gear.Placement.Rotation.Axis
)
@@ -228,22 +245,18 @@ class GearConnector(object):
fp.slave_gear.shift,
)
# 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()
# Master rotates by angle1
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)
# 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)
@@ -251,7 +264,6 @@ class GearConnector(object):
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)
@@ -268,7 +280,6 @@ class GearConnector(object):
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)
@@ -285,8 +296,6 @@ class GearConnector(object):
fp.master_gear.Placement = mat1
fp.master_gear.purgeTouched()
# else: both not stationary - no action needed
if (
isinstance(fp.master_gear.Proxy, InvoluteGear)
and isinstance(fp.slave_gear.Proxy, InvoluteGearRack)
@@ -294,6 +303,7 @@ class GearConnector(object):
isinstance(fp.master_gear.Proxy, CycloidGear)
and isinstance(fp.slave_gear.Proxy, CycloidGearRack)
):
# Rack gear logic remains unchanged
angle_master = fp.master_gear.Placement.Rotation.Angle * sum(
fp.master_gear.Placement.Rotation.Axis
)
@@ -315,6 +325,7 @@ class GearConnector(object):
if isinstance(fp.master_gear.Proxy, CycloidGear) and isinstance(
fp.slave_gear.Proxy, CycloidGear
):
# Cycloid logic remains unchanged
angle_master = fp.master_gear.Placement.Rotation.Angle * sum(
fp.master_gear.Placement.Rotation.Axis
)
@@ -322,20 +333,16 @@ class GearConnector(object):
dw_slave = fp.slave_gear.pitch_diameter.Value
dist = (dw_master + dw_slave) / 2
# 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()
# Master rotates by angle1
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
@@ -345,7 +352,6 @@ class GearConnector(object):
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)
@@ -362,7 +368,6 @@ class GearConnector(object):
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)
@@ -378,8 +383,12 @@ class GearConnector(object):
mat1.move(fp.slave_gear.Placement.Base)
fp.master_gear.Placement = mat1
fp.master_gear.purgeTouched()
# else: both not stationary - no action needed
self._recomputing = True
app.ActiveDocument.recompute()
self._recomputing = False
def execute(self, fp):
self.onChanged(fp, None)
# We pass 'angle1' here to ensure the logic runs,
# as the 'prop' check was removed.
self.onChanged(fp, 'angle1')