idex_modes: COPY and MIRROR mode implementation (#6297)

COPY and MIRROR mode implementation

Correctly apply input shaper params to new dual_carriage

Added SAVE_/RESTORE_IDEX_STATE commands

Documentation updates for the new IDEX modes

Signed-off-by: Dmitry Butyugin <dmbutyugin@google.com>
This commit is contained in:
Dmitry Butyugin 2023-08-01 18:23:52 +02:00 committed by GitHub
parent ea330717cd
commit 36be1cfc51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 458 additions and 250 deletions

View File

@ -62,6 +62,8 @@ endstop_pin: ^ar2
position_endstop: 200 position_endstop: 200
position_max: 200 position_max: 200
homing_speed: 50 homing_speed: 50
# A minimum distance between the carriages to enforce
safe_distance: 40
[extruder1] [extruder1]
step_pin: ar36 step_pin: ar36
@ -94,3 +96,37 @@ gcode:
ACTIVATE_EXTRUDER EXTRUDER=extruder1 ACTIVATE_EXTRUDER EXTRUDER=extruder1
SET_DUAL_CARRIAGE CARRIAGE=1 SET_DUAL_CARRIAGE CARRIAGE=1
SET_GCODE_OFFSET Y=15 SET_GCODE_OFFSET Y=15
# A helper script to activate copy mode
[gcode_macro ACTIVATE_COPY_MODE]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=0 MODE=PRIMARY
G1 X0
ACTIVATE_EXTRUDER EXTRUDER=extruder
SET_DUAL_CARRIAGE CARRIAGE=1 MODE=PRIMARY
G1 X100
SET_DUAL_CARRIAGE CARRIAGE=1 MODE=COPY
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder
# A helper script to activate mirror mode
[gcode_macro ACTIVATE_MIRROR_MODE]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=0 MODE=PRIMARY
G1 X0
ACTIVATE_EXTRUDER EXTRUDER=extruder
SET_DUAL_CARRIAGE CARRIAGE=1 MODE=PRIMARY
G1 X200
SET_DUAL_CARRIAGE CARRIAGE=1 MODE=MIRROR
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder
## An optional input shaper support
#[input_shaper]
## The section is intentionally empty
#
#[delayed_gcode init_shaper]
#initial_duration: 0.1
#gcode:
# SET_DUAL_CARRIAGE CARRIAGE=1
# SET_INPUT_SHAPER SHAPER_TYPE_X=<dual_carriage_shaper> SHAPER_FREQ_X=<dual_carriage_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>
# SET_DUAL_CARRIAGE CARRIAGE=0
# SET_INPUT_SHAPER SHAPER_TYPE_X=<primary_carriage_shaper> SHAPER_FREQ_X=<primary_carriage_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>

View File

@ -8,6 +8,11 @@ All dates in this document are approximate.
## Changes ## Changes
20230729: The exported status for `dual_carriage` is changed. Instead of
exporting `mode` and `active_carriage`, the individual modes for each
carriage are exported as `printer.dual_carriage.carriage_0` and
`printer.dual_carriage.carriage_1`.
20230619: The `relative_reference_index` option has been deprecated 20230619: The `relative_reference_index` option has been deprecated
and superceded by the `zero_reference_position` option. Refer to the and superceded by the `zero_reference_position` option. Refer to the
[Bed Mesh Documentation](./Bed_Mesh.md#the-deprecated-relative_reference_index) [Bed Mesh Documentation](./Bed_Mesh.md#the-deprecated-relative_reference_index)

View File

@ -2007,14 +2007,24 @@ for an example configuration.
### [dual_carriage] ### [dual_carriage]
Support for cartesian printers with dual carriages on a single Support for cartesian and hybrid_corexy/z printers with dual carriages
axis. The active carriage is set via the SET_DUAL_CARRIAGE extended on a single axis. The carriage mode can be set via the
g-code command. The "SET_DUAL_CARRIAGE CARRIAGE=1" command will SET_DUAL_CARRIAGE extended g-code command. For example,
activate the carriage defined in this section (CARRIAGE=0 will return "SET_DUAL_CARRIAGE CARRIAGE=1" command will activate the carriage defined
activation to the primary carriage). Dual carriage support is in this section (CARRIAGE=0 will return activation to the primary carriage).
typically combined with extra extruders - the SET_DUAL_CARRIAGE Dual carriage support is typically combined with extra extruders - the
command is often called at the same time as the ACTIVATE_EXTRUDER SET_DUAL_CARRIAGE command is often called at the same time as the
command. Be sure to park the carriages during deactivation. ACTIVATE_EXTRUDER command. Be sure to park the carriages during deactivation.
Additionally, one could use "SET_DUAL_CARRIAGE CARRIAGE=1 MODE=COPY" or
"SET_DUAL_CARRIAGE CARRIAGE=1 MODE=MIRROR" commands to activate either copying
or mirroring mode of the dual carriage, in which case it will follow the
motion of the carriage 0 accordingly. These commands can be used to print
two parts simultaneously - either two identical parts (in COPY mode) or
mirrored parts (in MIRROR mode). Note that COPY and MIRROR modes also require
appropriate configuration of the extruder on the dual carriage, which can
typically be achieved with
"SYNC_EXTRUDER_MOTION MOTION_QUEUE=extruder EXTRUDER=<dual_carriage_extruder>"
or a similar command.
See [sample-idex.cfg](../config/sample-idex.cfg) for an example See [sample-idex.cfg](../config/sample-idex.cfg) for an example
configuration. configuration.
@ -2024,6 +2034,15 @@ configuration.
axis: axis:
# The axis this extra carriage is on (either x or y). This parameter # The axis this extra carriage is on (either x or y). This parameter
# must be provided. # must be provided.
#safe_distance:
# The minimum distance (in mm) to enforce between the dual and the primary
# carriages. If a G-Code command is executed that will bring the carriages
# closer than the specified limit, such a command will be rejected with an
# error. If safe_distance is not provided, it will be inferred from
# position_min and position_max for the dual and primary carriages. If set
# to 0 (or safe_distance is unset and position_min and position_max are
# identical for the primary and dual carraiges), the carriages proximity
# checks will be disabled.
#step_pin: #step_pin:
#dir_pin: #dir_pin:
#enable_pin: #enable_pin:

View File

@ -310,9 +310,33 @@ The following command is available when the
enabled. enabled.
#### SET_DUAL_CARRIAGE #### SET_DUAL_CARRIAGE
`SET_DUAL_CARRIAGE CARRIAGE=[0|1]`: This command will set the active `SET_DUAL_CARRIAGE CARRIAGE=[0|1] [MODE=[PRIMARY|COPY|MIRROR]]`:
carriage. It is typically invoked from the activate_gcode and This command will change the mode of the specified carriage.
deactivate_gcode fields in a multiple extruder configuration. If no `MODE` is provided it defaults to `PRIMARY`. Setting the mode
to `PRIMARY` deactivates the other carriage and makes the specified
carriage execute subsequent G-Code commands as-is. `COPY` and `MIRROR`
modes are supported only for `CARRIAGE=1`. When set to either of these
modes, carriage 1 will then track the subsequent moves of the carriage 0
and either copy relative movements of it (in `COPY` mode) or execute them
in the opposite (mirror) direction (in `MIRROR` mode).
#### SAVE_DUAL_CARRIAGE_STATE
`SAVE_DUAL_CARRIAGE_STATE [NAME=<state_name>]`: Save the current positions
of the dual carriages and their modes. Saving and restoring DUAL_CARRIAGE
state can be useful in scripts and macros, as well as in homing routine
overrides. If NAME is provided it allows one to name the saved state
to the given string. If NAME is not provided it defaults to "default".
#### RESTORE_DUAL_CARRIAGE_STATE
`RESTORE_DUAL_CARRIAGE_STATE [NAME=<state_name>] [MOVE=[0|1] [MOVE_SPEED=<speed>]]`:
Restore the previously saved positions of the dual carriages and their modes,
unless "MOVE=0" is specified, in which case only the saved modes will be
restored, but not the positions of the carriages. If positions are being
restored and "MOVE_SPEED" is specified, then the toolhead moves will be
performed with the given speed (in mm/s); otherwise the toolhead move will
use the rail homing speed. Note that the carriages restore their positions
only over their own axis, which may be necessary to correctly restore COPY
and MIRROR mode of the dual carraige.
### [endstop_phase] ### [endstop_phase]

View File

@ -418,18 +418,34 @@ if necessary.
### Is dual carriage setup supported with input shapers? ### Is dual carriage setup supported with input shapers?
There is no dedicated support for dual carriages with input shapers, but it does Yes. In this case, one should measure the resonances twice for each carriage.
not mean this setup will not work. One should run the tuning twice for each For example, if the second (dual) carriage is installed on X axis, it is
of the carriages, and calculate the ringing frequencies for X and Y axes for possible to set different input shapers for X axis for the primary and dual
each of the carriages independently. Then put the values for carriage 0 into carriages. However, the input shaper for Y axis should be the same for both
[input_shaper] section, and change the values on the fly when changing carriages (as ultimately this axis is driven by one or more stepper motors each
carriages, e.g. as a part of some macro: commanded to perform exactly the same steps). One possibility to configure
``` the input shaper for such setups is to keep `[input_shaper]` section empty and
SET_DUAL_CARRIAGE CARRIAGE=1 additionally define a `[delayed_gcode]` section in the `printer.cfg` as follows:
SET_INPUT_SHAPER SHAPER_FREQ_X=... SHAPER_FREQ_Y=...
``` ```
[input_shaper]
# Intentionally empty
And similarly when switching back to carriage 0. [delayed_gcode init_shaper]
initial_duration: 0.1
gcode:
SET_DUAL_CARRIAGE CARRIAGE=1
SET_INPUT_SHAPER SHAPER_TYPE_X=<dual_carriage_shaper> SHAPER_FREQ_X=<dual_carriage_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>
SET_DUAL_CARRIAGE CARRIAGE=0
SET_INPUT_SHAPER SHAPER_TYPE_X=<primary_carriage_shaper> SHAPER_FREQ_X=<primary_carriage_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>
```
Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` should be the same in both
commands. It is also possible to put a similar snippet into the start g-code
in the slicer, however then the shaper will not be enabled until any print
is started.
Note that the input shaper only needs to be configured once. Subsequent changes
of the carriages or their modes via `SET_DUAL_CARRIAGE` command will preserve
the configured input shaper parameters.
### Does input_shaper affect print time? ### Does input_shaper affect print time?

View File

@ -503,10 +503,11 @@ The following information is available in the `toolhead` object
The following information is available in The following information is available in
[dual_carriage](Config_Reference.md#dual_carriage) [dual_carriage](Config_Reference.md#dual_carriage)
on a hybrid_corexy or hybrid_corexz robot on a cartesian, hybrid_corexy or hybrid_corexz robot
- `mode`: The current mode. Possible values are: "FULL_CONTROL" - `carriage_0`: The mode of the carriage 0. Possible values are:
- `active_carriage`: The current active carriage. "INACTIVE" and "PRIMARY".
Possible values are: "CARRIAGE_0", "CARRIAGE_1" - `carriage_1`: The mode of the carriage 1. Possible values are:
"INACTIVE", "PRIMARY", "COPY", and "MIRROR".
## virtual_sdcard ## virtual_sdcard

View File

@ -21,7 +21,7 @@ SOURCE_FILES = [
'pollreactor.c', 'msgblock.c', 'trdispatch.c', 'pollreactor.c', 'msgblock.c', 'trdispatch.c',
'kin_cartesian.c', 'kin_corexy.c', 'kin_corexz.c', 'kin_delta.c', 'kin_cartesian.c', 'kin_corexy.c', 'kin_corexz.c', 'kin_delta.c',
'kin_deltesian.c', 'kin_polar.c', 'kin_rotary_delta.c', 'kin_winch.c', 'kin_deltesian.c', 'kin_polar.c', 'kin_rotary_delta.c', 'kin_winch.c',
'kin_extruder.c', 'kin_shaper.c', 'kin_extruder.c', 'kin_shaper.c', 'kin_idex.c',
] ]
DEST_LIB = "c_helper.so" DEST_LIB = "c_helper.so"
OTHER_FILES = [ OTHER_FILES = [
@ -101,7 +101,6 @@ defs_trapq = """
defs_kin_cartesian = """ defs_kin_cartesian = """
struct stepper_kinematics *cartesian_stepper_alloc(char axis); struct stepper_kinematics *cartesian_stepper_alloc(char axis);
struct stepper_kinematics *cartesian_reverse_stepper_alloc(char axis);
""" """
defs_kin_corexy = """ defs_kin_corexy = """
@ -153,6 +152,14 @@ defs_kin_shaper = """
struct stepper_kinematics * input_shaper_alloc(void); struct stepper_kinematics * input_shaper_alloc(void);
""" """
defs_kin_idex = """
void dual_carriage_set_sk(struct stepper_kinematics *sk
, struct stepper_kinematics *orig_sk);
int dual_carriage_set_transform(struct stepper_kinematics *sk
, char axis, double scale, double offs);
struct stepper_kinematics * dual_carriage_alloc(void);
"""
defs_serialqueue = """ defs_serialqueue = """
#define MESSAGE_MAX 64 #define MESSAGE_MAX 64
struct pull_queue_message { struct pull_queue_message {
@ -211,7 +218,7 @@ defs_all = [
defs_itersolve, defs_trapq, defs_trdispatch, defs_itersolve, defs_trapq, defs_trdispatch,
defs_kin_cartesian, defs_kin_corexy, defs_kin_corexz, defs_kin_delta, defs_kin_cartesian, defs_kin_corexy, defs_kin_corexz, defs_kin_delta,
defs_kin_deltesian, defs_kin_polar, defs_kin_rotary_delta, defs_kin_winch, defs_kin_deltesian, defs_kin_polar, defs_kin_rotary_delta, defs_kin_winch,
defs_kin_extruder, defs_kin_shaper, defs_kin_extruder, defs_kin_shaper, defs_kin_idex,
] ]
# Update filenames to an absolute path # Update filenames to an absolute path

View File

@ -49,42 +49,3 @@ cartesian_stepper_alloc(char axis)
} }
return sk; return sk;
} }
static double
cart_reverse_stepper_x_calc_position(struct stepper_kinematics *sk
, struct move *m, double move_time)
{
return -move_get_coord(m, move_time).x;
}
static double
cart_reverse_stepper_y_calc_position(struct stepper_kinematics *sk
, struct move *m, double move_time)
{
return -move_get_coord(m, move_time).y;
}
static double
cart_reverse_stepper_z_calc_position(struct stepper_kinematics *sk
, struct move *m, double move_time)
{
return -move_get_coord(m, move_time).z;
}
struct stepper_kinematics * __visible
cartesian_reverse_stepper_alloc(char axis)
{
struct stepper_kinematics *sk = malloc(sizeof(*sk));
memset(sk, 0, sizeof(*sk));
if (axis == 'x') {
sk->calc_position_cb = cart_reverse_stepper_x_calc_position;
sk->active_flags = AF_X;
} else if (axis == 'y') {
sk->calc_position_cb = cart_reverse_stepper_y_calc_position;
sk->active_flags = AF_Y;
} else if (axis == 'z') {
sk->calc_position_cb = cart_reverse_stepper_z_calc_position;
sk->active_flags = AF_Z;
}
return sk;
}

81
klippy/chelper/kin_idex.c Normal file
View File

@ -0,0 +1,81 @@
// Idex dual carriage kinematics
//
// Copyright (C) 2023 Dmitry Butyugin <dmbutyugin@google.com>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
#include <stddef.h> // offsetof
#include <stdlib.h> // malloc
#include <string.h> // memset
#include "compiler.h" // __visible
#include "itersolve.h" // struct stepper_kinematics
#include "trapq.h" // struct move
#define DUMMY_T 500.0
struct dual_carriage_stepper {
struct stepper_kinematics sk;
struct stepper_kinematics *orig_sk;
struct move m;
double x_scale, x_offs, y_scale, y_offs;
};
double
dual_carriage_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct dual_carriage_stepper *dc = container_of(
sk, struct dual_carriage_stepper, sk);
struct coord pos = move_get_coord(m, move_time);
dc->m.start_pos.x = pos.x * dc->x_scale + dc->x_offs;
dc->m.start_pos.y = pos.y * dc->y_scale + dc->y_offs;
dc->m.start_pos.z = pos.z;
return dc->orig_sk->calc_position_cb(dc->orig_sk, &dc->m, DUMMY_T);
}
void __visible
dual_carriage_set_sk(struct stepper_kinematics *sk
, struct stepper_kinematics *orig_sk)
{
struct dual_carriage_stepper *dc = container_of(
sk, struct dual_carriage_stepper, sk);
dc->sk.calc_position_cb = dual_carriage_calc_position;
dc->sk.active_flags = orig_sk->active_flags;
dc->orig_sk = orig_sk;
}
int __visible
dual_carriage_set_transform(struct stepper_kinematics *sk, char axis
, double scale, double offs)
{
struct dual_carriage_stepper *dc = container_of(
sk, struct dual_carriage_stepper, sk);
if (axis == 'x') {
dc->x_scale = scale;
dc->x_offs = offs;
if (!scale)
dc->sk.active_flags &= ~AF_X;
else if (scale && dc->orig_sk->active_flags & AF_X)
dc->sk.active_flags |= AF_X;
return 0;
}
if (axis == 'y') {
dc->y_scale = scale;
dc->y_offs = offs;
if (!scale)
dc->sk.active_flags &= ~AF_Y;
else if (scale && dc->orig_sk->active_flags & AF_Y)
dc->sk.active_flags |= AF_Y;
return 0;
}
return -1;
}
struct stepper_kinematics * __visible
dual_carriage_alloc(void)
{
struct dual_carriage_stepper *dc = malloc(sizeof(*dc));
memset(dc, 0, sizeof(*dc));
dc->m.move_t = 2. * DUMMY_T;
return &dc->sk;
}

View File

@ -204,11 +204,11 @@ input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis
struct input_shaper *is = container_of(sk, struct input_shaper, sk); struct input_shaper *is = container_of(sk, struct input_shaper, sk);
struct shaper_pulses *sp = axis == 'x' ? &is->sx : &is->sy; struct shaper_pulses *sp = axis == 'x' ? &is->sx : &is->sy;
int status = 0; int status = 0;
if (is->orig_sk->active_flags & (axis == 'x' ? AF_X : AF_Y)) // Ignore input shaper update if the axis is not active
if (is->orig_sk->active_flags & (axis == 'x' ? AF_X : AF_Y)) {
status = init_shaper(n, a, t, sp); status = init_shaper(n, a, t, sp);
else shaper_note_generation_time(is);
sp->num_pulses = 0; }
shaper_note_generation_time(is);
return status; return status;
} }

View File

@ -5,6 +5,7 @@
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
import logging import logging
import stepper import stepper
from . import idex_modes
class CartKinematics: class CartKinematics:
def __init__(self, toolhead, config): def __init__(self, toolhead, config):
@ -16,6 +17,25 @@ class CartKinematics:
for n in 'xyz'] for n in 'xyz']
for rail, axis in zip(self.rails, 'xyz'): for rail, axis in zip(self.rails, 'xyz'):
rail.setup_itersolve('cartesian_stepper_alloc', axis.encode()) rail.setup_itersolve('cartesian_stepper_alloc', axis.encode())
ranges = [r.get_range() for r in self.rails]
self.axes_min = toolhead.Coord(*[r[0] for r in ranges], e=0.)
self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.)
self.dc_module = None
if config.has_section('dual_carriage'):
dc_config = config.getsection('dual_carriage')
dc_axis = dc_config.getchoice('axis', {'x': 'x', 'y': 'y'})
self.dual_carriage_axis = {'x': 0, 'y': 1}[dc_axis]
# setup second dual carriage rail
self.rails.append(stepper.LookupMultiRail(dc_config))
self.rails[3].setup_itersolve('cartesian_stepper_alloc',
dc_axis.encode())
dc_rail_0 = idex_modes.DualCarriagesRail(
self.rails[0], axis=self.dual_carriage_axis, active=True)
dc_rail_1 = idex_modes.DualCarriagesRail(
self.rails[3], axis=self.dual_carriage_axis, active=False)
self.dc_module = idex_modes.DualCarriages(
dc_config, dc_rail_0, dc_rail_1,
axis=self.dual_carriage_axis)
for s in self.get_steppers(): for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq()) s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps) toolhead.register_step_generator(s.generate_steps)
@ -28,31 +48,18 @@ class CartKinematics:
self.max_z_accel = config.getfloat('max_z_accel', max_accel, self.max_z_accel = config.getfloat('max_z_accel', max_accel,
above=0., maxval=max_accel) above=0., maxval=max_accel)
self.limits = [(1.0, -1.0)] * 3 self.limits = [(1.0, -1.0)] * 3
ranges = [r.get_range() for r in self.rails]
self.axes_min = toolhead.Coord(*[r[0] for r in ranges], e=0.)
self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.)
# Check for dual carriage support
if config.has_section('dual_carriage'):
dc_config = config.getsection('dual_carriage')
dc_axis = dc_config.getchoice('axis', {'x': 'x', 'y': 'y'})
self.dual_carriage_axis = {'x': 0, 'y': 1}[dc_axis]
dc_rail = stepper.LookupMultiRail(dc_config)
dc_rail.setup_itersolve('cartesian_stepper_alloc', dc_axis.encode())
for s in dc_rail.get_steppers():
toolhead.register_step_generator(s.generate_steps)
self.dual_carriage_rails = [
self.rails[self.dual_carriage_axis], dc_rail]
self.printer.lookup_object('gcode').register_command(
'SET_DUAL_CARRIAGE', self.cmd_SET_DUAL_CARRIAGE,
desc=self.cmd_SET_DUAL_CARRIAGE_help)
def get_steppers(self): def get_steppers(self):
rails = self.rails return [s for rail in self.rails for s in rail.get_steppers()]
if self.dual_carriage_axis is not None:
dca = self.dual_carriage_axis
rails = rails[:dca] + self.dual_carriage_rails + rails[dca+1:]
return [s for rail in rails for s in rail.get_steppers()]
def calc_position(self, stepper_positions): def calc_position(self, stepper_positions):
return [stepper_positions[rail.get_name()] for rail in self.rails] return [stepper_positions[rail.get_name()] for rail in self.rails]
def update_limits(self, i, range):
l, h = self.limits[i]
# Only update limits if this axis was already homed,
# otherwise leave in un-homed state.
if l <= h:
self.limits[i] = range
def override_rail(self, i, rail):
self.rails[i] = rail
def set_position(self, newpos, homing_axes): def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails): for i, rail in enumerate(self.rails):
rail.set_position(newpos) rail.set_position(newpos)
@ -61,7 +68,7 @@ class CartKinematics:
def note_z_not_homed(self): def note_z_not_homed(self):
# Helper for Safe Z Home # Helper for Safe Z Home
self.limits[2] = (1.0, -1.0) self.limits[2] = (1.0, -1.0)
def _home_axis(self, homing_state, axis, rail): def home_axis(self, homing_state, axis, rail):
# Determine movement # Determine movement
position_min, position_max = rail.get_range() position_min, position_max = rail.get_range()
hi = rail.get_homing_info() hi = rail.get_homing_info()
@ -77,16 +84,10 @@ class CartKinematics:
def home(self, homing_state): def home(self, homing_state):
# Each axis is homed independently and in order # Each axis is homed independently and in order
for axis in homing_state.get_axes(): for axis in homing_state.get_axes():
if axis == self.dual_carriage_axis: if self.dc_module is not None and axis == self.dual_carriage_axis:
dc1, dc2 = self.dual_carriage_rails self.dc_module.home(homing_state)
altc = self.rails[axis] == dc2
self._activate_carriage(0)
self._home_axis(homing_state, axis, dc1)
self._activate_carriage(1)
self._home_axis(homing_state, axis, dc2)
self._activate_carriage(altc)
else: else:
self._home_axis(homing_state, axis, self.rails[axis]) self.home_axis(homing_state, axis, self.rails[axis])
def _motor_off(self, print_time): def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3 self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move): def _check_endstops(self, move):
@ -119,24 +120,6 @@ class CartKinematics:
'axis_minimum': self.axes_min, 'axis_minimum': self.axes_min,
'axis_maximum': self.axes_max, 'axis_maximum': self.axes_max,
} }
# Dual carriage support
def _activate_carriage(self, carriage):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
dc_rail = self.dual_carriage_rails[carriage]
dc_axis = self.dual_carriage_axis
self.rails[dc_axis].set_trapq(None)
dc_rail.set_trapq(toolhead.get_trapq())
self.rails[dc_axis] = dc_rail
pos = toolhead.get_position()
pos[dc_axis] = dc_rail.get_commanded_position()
toolhead.set_position(pos)
if self.limits[dc_axis][0] <= self.limits[dc_axis][1]:
self.limits[dc_axis] = dc_rail.get_range()
cmd_SET_DUAL_CARRIAGE_help = "Set which carriage is active"
def cmd_SET_DUAL_CARRIAGE(self, gcmd):
carriage = gcmd.get_int('CARRIAGE', minval=0, maxval=1)
self._activate_carriage(carriage)
def load_kinematics(toolhead, config): def load_kinematics(toolhead, config):
return CartKinematics(toolhead, config) return CartKinematics(toolhead, config)

View File

@ -33,17 +33,13 @@ class HybridCoreXYKinematics:
self.rails.append(stepper.PrinterRail(dc_config)) self.rails.append(stepper.PrinterRail(dc_config))
self.rails[1].get_endstops()[0][0].add_stepper( self.rails[1].get_endstops()[0][0].add_stepper(
self.rails[3].get_steppers()[0]) self.rails[3].get_steppers()[0])
self.rails[3].setup_itersolve('cartesian_stepper_alloc', b'y') self.rails[3].setup_itersolve('corexy_stepper_alloc', b'+')
dc_rail_0 = idex_modes.DualCarriagesRail( dc_rail_0 = idex_modes.DualCarriagesRail(
self.printer, self.rails[0], axis=0, active=True, self.rails[0], axis=0, active=True)
stepper_alloc_active=('corexy_stepper_alloc', b'-'),
stepper_alloc_inactive=('cartesian_reverse_stepper_alloc',b'y'))
dc_rail_1 = idex_modes.DualCarriagesRail( dc_rail_1 = idex_modes.DualCarriagesRail(
self.printer, self.rails[3], axis=0, active=False, self.rails[3], axis=0, active=False)
stepper_alloc_active=('corexy_stepper_alloc', b'+'), self.dc_module = idex_modes.DualCarriages(
stepper_alloc_inactive=('cartesian_stepper_alloc', b'y')) dc_config, dc_rail_0, dc_rail_1, axis=0)
self.dc_module = idex_modes.DualCarriages(self.printer,
dc_rail_0, dc_rail_1, axis=0)
for s in self.get_steppers(): for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq()) s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps) toolhead.register_step_generator(s.generate_steps)
@ -60,8 +56,8 @@ class HybridCoreXYKinematics:
return [s for rail in self.rails for s in rail.get_steppers()] return [s for rail in self.rails for s in rail.get_steppers()]
def calc_position(self, stepper_positions): def calc_position(self, stepper_positions):
pos = [stepper_positions[rail.get_name()] for rail in self.rails] pos = [stepper_positions[rail.get_name()] for rail in self.rails]
if (self.dc_module is not None and 'CARRIAGE_1' == \ if (self.dc_module is not None and 'PRIMARY' == \
self.dc_module.get_status()['active_carriage']): self.dc_module.get_status()['carriage_1']):
return [pos[0] - pos[1], pos[1], pos[2]] return [pos[0] - pos[1], pos[1], pos[2]]
else: else:
return [pos[0] + pos[1], pos[1], pos[2]] return [pos[0] + pos[1], pos[1], pos[2]]
@ -81,7 +77,7 @@ class HybridCoreXYKinematics:
def note_z_not_homed(self): def note_z_not_homed(self):
# Helper for Safe Z Home # Helper for Safe Z Home
self.limits[2] = (1.0, -1.0) self.limits[2] = (1.0, -1.0)
def _home_axis(self, homing_state, axis, rail): def home_axis(self, homing_state, axis, rail):
position_min, position_max = rail.get_range() position_min, position_max = rail.get_range()
hi = rail.get_homing_info() hi = rail.get_homing_info()
homepos = [None, None, None, None] homepos = [None, None, None, None]
@ -95,14 +91,10 @@ class HybridCoreXYKinematics:
homing_state.home_rails([rail], forcepos, homepos) homing_state.home_rails([rail], forcepos, homepos)
def home(self, homing_state): def home(self, homing_state):
for axis in homing_state.get_axes(): for axis in homing_state.get_axes():
if (self.dc_module is not None and axis == 0): if self.dc_module is not None and axis == 0:
self.dc_module.save_idex_state() self.dc_module.home(homing_state)
for i in [0,1]:
self.dc_module.toggle_active_dc_rail(i)
self._home_axis(homing_state, axis, self.rails[0])
self.dc_module.restore_idex_state()
else: else:
self._home_axis(homing_state, axis, self.rails[axis]) self.home_axis(homing_state, axis, self.rails[axis])
def _motor_off(self, print_time): def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3 self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move): def _check_endstops(self, move):

View File

@ -33,17 +33,13 @@ class HybridCoreXZKinematics:
self.rails.append(stepper.PrinterRail(dc_config)) self.rails.append(stepper.PrinterRail(dc_config))
self.rails[2].get_endstops()[0][0].add_stepper( self.rails[2].get_endstops()[0][0].add_stepper(
self.rails[3].get_steppers()[0]) self.rails[3].get_steppers()[0])
self.rails[3].setup_itersolve('cartesian_stepper_alloc', b'z') self.rails[3].setup_itersolve('corexz_stepper_alloc', b'+')
dc_rail_0 = idex_modes.DualCarriagesRail( dc_rail_0 = idex_modes.DualCarriagesRail(
self.printer, self.rails[0], axis=0, active=True, self.rails[0], axis=0, active=True)
stepper_alloc_active=('corexz_stepper_alloc', b'-'),
stepper_alloc_inactive=('cartesian_reverse_stepper_alloc',b'z'))
dc_rail_1 = idex_modes.DualCarriagesRail( dc_rail_1 = idex_modes.DualCarriagesRail(
self.printer, self.rails[3], axis=0, active=False, self.rails[3], axis=0, active=False)
stepper_alloc_active=('corexz_stepper_alloc', b'+'), self.dc_module = idex_modes.DualCarriages(
stepper_alloc_inactive=('cartesian_stepper_alloc', b'z')) dc_config, dc_rail_0, dc_rail_1, axis=0)
self.dc_module = idex_modes.DualCarriages(self.printer,
dc_rail_0, dc_rail_1, axis=0)
for s in self.get_steppers(): for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq()) s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps) toolhead.register_step_generator(s.generate_steps)
@ -60,8 +56,8 @@ class HybridCoreXZKinematics:
return [s for rail in self.rails for s in rail.get_steppers()] return [s for rail in self.rails for s in rail.get_steppers()]
def calc_position(self, stepper_positions): def calc_position(self, stepper_positions):
pos = [stepper_positions[rail.get_name()] for rail in self.rails] pos = [stepper_positions[rail.get_name()] for rail in self.rails]
if (self.dc_module is not None and 'CARRIAGE_1' == \ if (self.dc_module is not None and 'PRIMARY' == \
self.dc_module.get_status()['active_carriage']): self.dc_module.get_status()['carriage_1']):
return [pos[0] - pos[2], pos[1], pos[2]] return [pos[0] - pos[2], pos[1], pos[2]]
else: else:
return [pos[0] + pos[2], pos[1], pos[2]] return [pos[0] + pos[2], pos[1], pos[2]]
@ -81,7 +77,7 @@ class HybridCoreXZKinematics:
def note_z_not_homed(self): def note_z_not_homed(self):
# Helper for Safe Z Home # Helper for Safe Z Home
self.limits[2] = (1.0, -1.0) self.limits[2] = (1.0, -1.0)
def _home_axis(self, homing_state, axis, rail): def home_axis(self, homing_state, axis, rail):
position_min, position_max = rail.get_range() position_min, position_max = rail.get_range()
hi = rail.get_homing_info() hi = rail.get_homing_info()
homepos = [None, None, None, None] homepos = [None, None, None, None]
@ -95,14 +91,10 @@ class HybridCoreXZKinematics:
homing_state.home_rails([rail], forcepos, homepos) homing_state.home_rails([rail], forcepos, homepos)
def home(self, homing_state): def home(self, homing_state):
for axis in homing_state.get_axes(): for axis in homing_state.get_axes():
if (self.dc_module is not None and axis == 0): if self.dc_module is not None and axis == 0:
self.dc_module.save_idex_state() self.dc_module.home(homing_state)
for i in [0,1]:
self.dc_module.toggle_active_dc_rail(i)
self._home_axis(homing_state, axis, self.rails[0])
self.dc_module.restore_idex_state()
else: else:
self._home_axis(homing_state, axis, self.rails[axis]) self.home_axis(homing_state, axis, self.rails[axis])
def _motor_off(self, print_time): def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3 self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move): def _check_endstops(self, move):

View File

@ -1,23 +1,48 @@
# Support for duplication and mirroring modes for IDEX printers # Support for duplication and mirroring modes for IDEX printers
# #
# Copyright (C) 2021 Fabrice Gallet <tircown@gmail.com> # Copyright (C) 2021 Fabrice Gallet <tircown@gmail.com>
# Copyright (C) 2023 Dmitry Butyugin <dmbutyugin@google.com>
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
import math import math
import chelper import chelper
INACTIVE = 'INACTIVE'
PRIMARY = 'PRIMARY'
COPY = 'COPY'
MIRROR = 'MIRROR'
class DualCarriages: class DualCarriages:
def __init__(self, printer, rail_0, rail_1, axis): VALID_MODES = [PRIMARY, COPY, MIRROR]
self.printer = printer def __init__(self, dc_config, rail_0, rail_1, axis):
self.printer = dc_config.get_printer()
self.axis = axis self.axis = axis
self.dc = (rail_0, rail_1) self.dc = (rail_0, rail_1)
self.saved_state = None self.saved_states = {}
safe_dist = dc_config.getfloat('safe_distance', None, minval=0.)
if safe_dist is None:
dc0_rail = rail_0.get_rail()
dc1_rail = rail_1.get_rail()
safe_dist = min(abs(dc0_rail.position_min - dc1_rail.position_min),
abs(dc0_rail.position_max - dc1_rail.position_max))
self.safe_dist = safe_dist
self.printer.add_object('dual_carriage', self) self.printer.add_object('dual_carriage', self)
self.printer.register_event_handler("klippy:ready", self._handle_ready)
gcode = self.printer.lookup_object('gcode') gcode = self.printer.lookup_object('gcode')
gcode.register_command( gcode.register_command(
'SET_DUAL_CARRIAGE', self.cmd_SET_DUAL_CARRIAGE, 'SET_DUAL_CARRIAGE', self.cmd_SET_DUAL_CARRIAGE,
desc=self.cmd_SET_DUAL_CARRIAGE_help) desc=self.cmd_SET_DUAL_CARRIAGE_help)
def toggle_active_dc_rail(self, index): gcode.register_command(
'SAVE_DUAL_CARRIAGE_STATE',
self.cmd_SAVE_DUAL_CARRIAGE_STATE,
desc=self.cmd_SAVE_DUAL_CARRIAGE_STATE_help)
gcode.register_command(
'RESTORE_DUAL_CARRIAGE_STATE',
self.cmd_RESTORE_DUAL_CARRIAGE_STATE,
desc=self.cmd_RESTORE_DUAL_CARRIAGE_STATE_help)
def get_rails(self):
return self.dc
def toggle_active_dc_rail(self, index, override_rail=False):
toolhead = self.printer.lookup_object('toolhead') toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation() toolhead.flush_step_generation()
pos = toolhead.get_position() pos = toolhead.get_position()
@ -27,104 +52,170 @@ class DualCarriages:
if i != index: if i != index:
if dc.is_active(): if dc.is_active():
dc.inactivate(pos) dc.inactivate(pos)
kin.override_rail(3, dc_rail) if override_rail:
elif dc.is_active() is False: kin.override_rail(3, dc_rail)
newpos = pos[:self.axis] + [dc.axis_position] \ target_dc = self.dc[index]
+ pos[self.axis+1:] if target_dc.mode != PRIMARY:
dc.activate(newpos) newpos = pos[:self.axis] + [target_dc.get_axis_position(pos)] \
kin.override_rail(self.axis, dc_rail) + pos[self.axis+1:]
toolhead.set_position(newpos) target_dc.activate(PRIMARY, newpos, old_position=pos)
kin.update_limits(self.axis, dc_rail.get_range()) if override_rail:
kin.override_rail(self.axis, target_dc.get_rail())
toolhead.set_position(newpos)
kin.update_limits(self.axis, target_dc.get_rail().get_range())
def home(self, homing_state):
kin = self.printer.lookup_object('toolhead').get_kinematics()
for i, dc_rail in enumerate(self.dc):
self.toggle_active_dc_rail(i, override_rail=True)
kin.home_axis(homing_state, self.axis, dc_rail.get_rail())
# Restore the original rails ordering
self.toggle_active_dc_rail(0, override_rail=True)
def get_status(self, eventtime=None): def get_status(self, eventtime=None):
dc0, dc1 = self.dc return {('carriage_%d' % (i,)) : dc.mode
if (dc0.is_active() is True): for (i, dc) in enumerate(self.dc)}
return { 'mode': 'FULL_CONTROL', 'active_carriage': 'CARRIAGE_0' } def get_kin_range(self, toolhead, mode):
pos = toolhead.get_position()
axes_pos = [dc.get_axis_position(pos) for dc in self.dc]
dc0_rail = self.dc[0].get_rail()
dc1_rail = self.dc[1].get_rail()
range_min = dc0_rail.position_min
range_max = dc0_rail.position_max
safe_dist = self.safe_dist
if mode == COPY:
range_min = max(range_min,
axes_pos[0] - axes_pos[1] + dc1_rail.position_min)
range_max = min(range_max,
axes_pos[0] - axes_pos[1] + dc1_rail.position_max)
elif mode == MIRROR:
if dc0_rail.get_homing_info().positive_dir:
range_min = max(range_min,
0.5 * (sum(axes_pos) + safe_dist))
range_max = min(range_max,
sum(axes_pos) - dc1_rail.position_min)
else:
range_max = min(range_max,
0.5 * (sum(axes_pos) - safe_dist))
range_min = max(range_min,
sum(axes_pos) - dc1_rail.position_max)
else: else:
return { 'mode': 'FULL_CONTROL', 'active_carriage': 'CARRIAGE_1' } # mode == PRIMARY
def save_idex_state(self): active_idx = 1 if self.dc[1].is_active() else 0
dc0, dc1 = self.dc inactive_idx = 1 - active_idx
if (dc0.is_active() is True): if active_idx:
mode, active_carriage = ('FULL_CONTROL', 'CARRIAGE_0') range_min = dc1_rail.position_min
range_max = dc1_rail.position_max
if self.dc[active_idx].get_rail().get_homing_info().positive_dir:
range_min = max(range_min, axes_pos[inactive_idx] + safe_dist)
else:
range_max = min(range_max, axes_pos[inactive_idx] - safe_dist)
return (range_min, range_max)
def activate_dc_mode(self, index, mode):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
kin = toolhead.get_kinematics()
if mode == INACTIVE:
self.dc[index].inactivate(toolhead.get_position())
elif mode == PRIMARY:
self.toggle_active_dc_rail(index)
else: else:
mode, active_carriage = ('FULL_CONTROL', 'CARRIAGE_1') self.toggle_active_dc_rail(0)
self.saved_state = { self.dc[index].activate(mode, toolhead.get_position())
'mode': mode, kin.update_limits(self.axis, self.get_kin_range(toolhead, mode))
'active_carriage': active_carriage, def _handle_ready(self):
'axis_positions': (dc0.axis_position, dc1.axis_position) # Apply the transform later during Klipper initialization to make sure
} # that input shaping can pick up the correct stepper kinematic flags.
def restore_idex_state(self): for dc in self.dc:
if self.saved_state is not None: dc.apply_transform()
# set carriage 0 active cmd_SET_DUAL_CARRIAGE_help = "Configure the dual carriages mode"
if (self.saved_state['active_carriage'] == 'CARRIAGE_0'
and self.dc[0].is_active() is False):
self.toggle_active_dc_rail(0)
# set carriage 1 active
elif (self.saved_state['active_carriage'] == 'CARRIAGE_1'
and self.dc[1].is_active() is False):
self.toggle_active_dc_rail(1)
cmd_SET_DUAL_CARRIAGE_help = "Set which carriage is active"
def cmd_SET_DUAL_CARRIAGE(self, gcmd): def cmd_SET_DUAL_CARRIAGE(self, gcmd):
index = gcmd.get_int('CARRIAGE', minval=0, maxval=1) index = gcmd.get_int('CARRIAGE', minval=0, maxval=1)
if (not(self.dc[0].is_active() == self.dc[1].is_active() == True) mode = gcmd.get('MODE', PRIMARY).upper()
and self.dc[index].is_active() is False): if mode not in self.VALID_MODES:
self.toggle_active_dc_rail(index) raise gcmd.error("Invalid mode=%s specified" % (mode,))
if mode in [COPY, MIRROR]:
if index == 0:
raise gcmd.error(
"Mode=%s is not supported for carriage=0" % (mode,))
curtime = self.printer.get_reactor().monotonic()
kin = self.printer.lookup_object('toolhead').get_kinematics()
axis = 'xyz'[self.axis]
if axis not in kin.get_status(curtime)['homed_axes']:
raise gcmd.error(
"Axis %s must be homed prior to enabling mode=%s" %
(axis, mode))
self.activate_dc_mode(index, mode)
cmd_SAVE_DUAL_CARRIAGE_STATE_help = \
"Save dual carriages modes and positions"
def cmd_SAVE_DUAL_CARRIAGE_STATE(self, gcmd):
state_name = gcmd.get('NAME', 'default')
pos = self.printer.lookup_object('toolhead').get_position()
self.saved_states[state_name] = {
'carriage_modes': [dc.mode for dc in self.dc],
'axes_positions': [dc.get_axis_position(pos) for dc in self.dc],
}
cmd_RESTORE_DUAL_CARRIAGE_STATE_help = \
"Restore dual carriages modes and positions"
def cmd_RESTORE_DUAL_CARRIAGE_STATE(self, gcmd):
state_name = gcmd.get('NAME', 'default')
saved_state = self.saved_states.get(state_name)
if saved_state is None:
raise gcmd.error("Unknown DUAL_CARRIAGE state: %s" % (state_name,))
move_speed = gcmd.get('MOVE_SPEED', 0., above=0.)
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
pos = toolhead.get_position()
if gcmd.get_int('MOVE', 1):
for i, dc in enumerate(self.dc):
self.toggle_active_dc_rail(i)
saved_pos = saved_state['axes_positions'][i]
toolhead.manual_move(
pos[:self.axis] + [saved_pos] + pos[self.axis+1:],
move_speed or dc.get_rail().homing_speed)
for i, dc in enumerate(self.dc):
saved_mode = saved_state['carriage_modes'][i]
self.activate_dc_mode(i, saved_mode)
class DualCarriagesRail: class DualCarriagesRail:
ACTIVE=1 ENC_AXES = [b'x', b'y']
INACTIVE=2 def __init__(self, rail, axis, active):
def __init__(self, printer, rail, axis, active, stepper_alloc_active,
stepper_alloc_inactive=None):
self.printer = printer
self.rail = rail self.rail = rail
self.axis = axis self.axis = axis
self.status = (self.INACTIVE, self.ACTIVE)[active] self.mode = (INACTIVE, PRIMARY)[active]
self.stepper_alloc_active = stepper_alloc_active self.offset = 0.
self.stepper_alloc_inactive = stepper_alloc_inactive self.scale = 1. if active else 0.
self.axis_position = -1
self.stepper_active_sk = {}
self.stepper_inactive_sk = {}
for s in rail.get_steppers():
self._save_sk(self.status, s, s.get_stepper_kinematics())
def _alloc_sk(self, alloc_func, *params):
ffi_main, ffi_lib = chelper.get_ffi() ffi_main, ffi_lib = chelper.get_ffi()
return ffi_main.gc(getattr(ffi_lib, alloc_func)(*params), ffi_lib.free) self.dc_stepper_kinematics = []
def _get_sk(self, status, stepper): self.orig_stepper_kinematics = []
sk = None for s in rail.get_steppers():
if status == self.ACTIVE: sk = ffi_main.gc(ffi_lib.dual_carriage_alloc(), ffi_lib.free)
sk = self.stepper_active_sk.get(stepper, None) orig_sk = s.get_stepper_kinematics()
if sk is None and self.stepper_alloc_active: ffi_lib.dual_carriage_set_sk(sk, orig_sk)
sk = self._alloc_sk(*self.stepper_alloc_active) # Set the default transform for the other axis
self._save_sk(status, stepper, sk) ffi_lib.dual_carriage_set_transform(
elif status == self.INACTIVE: sk, self.ENC_AXES[1 - axis], 1., 0.)
sk = self.stepper_inactive_sk.get(stepper, None) self.dc_stepper_kinematics.append(sk)
if sk is None and self.stepper_alloc_inactive: self.orig_stepper_kinematics.append(orig_sk)
sk = self._alloc_sk(*self.stepper_alloc_inactive) s.set_stepper_kinematics(sk)
self._save_sk(status, stepper, sk)
return sk
def _save_sk(self, status, stepper, sk):
if status == self.ACTIVE:
self.stepper_active_sk[stepper] = sk
elif status == self.INACTIVE:
self.stepper_inactive_sk[stepper] = sk
def _update_stepper_alloc(self, position, active=True):
toolhead = self.printer.lookup_object('toolhead')
self.axis_position = position[self.axis]
self.rail.set_trapq(None)
old_status = self.status
self.status = (self.INACTIVE, self.ACTIVE)[active]
for s in self.rail.get_steppers():
sk = self._get_sk(self.status, s)
if sk is None:
return
old_sk = s.set_stepper_kinematics(sk)
self._save_sk(old_status, s, old_sk)
self.rail.set_position(position)
self.rail.set_trapq(toolhead.get_trapq())
def get_rail(self): def get_rail(self):
return self.rail return self.rail
def is_active(self): def is_active(self):
return self.status == self.ACTIVE return self.mode != INACTIVE
def activate(self, position): def get_axis_position(self, position):
self._update_stepper_alloc(position, active=True) return position[self.axis] * self.scale + self.offset
def apply_transform(self):
ffi_main, ffi_lib = chelper.get_ffi()
for sk in self.dc_stepper_kinematics:
ffi_lib.dual_carriage_set_transform(
sk, self.ENC_AXES[self.axis], self.scale, self.offset)
def activate(self, mode, position, old_position=None):
old_axis_position = self.get_axis_position(old_position or position)
self.scale = -1. if mode == MIRROR else 1.
self.offset = old_axis_position - position[self.axis] * self.scale
self.apply_transform()
self.mode = mode
def inactivate(self, position): def inactivate(self, position):
self._update_stepper_alloc(position, active=False) self.offset = self.get_axis_position(position)
self.scale = 0.
self.apply_transform()
self.mode = INACTIVE