Gear and Chain Connector getting closer
This commit is contained in:
303
freecad/gears/chainconnector.py
Normal file
303
freecad/gears/chainconnector.py
Normal 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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user