Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Duet3D/RepRapFirmware.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/GCodes/GCodes.cpp28
-rw-r--r--src/GCodes/GCodes.h2
-rw-r--r--src/GCodes/GCodes2.cpp38
-rw-r--r--src/GCodes/GCodes3.cpp12
-rw-r--r--src/GCodes/GCodes4.cpp34
-rw-r--r--src/Movement/DDA.cpp12
-rw-r--r--src/Movement/RawMove.cpp2
-rw-r--r--src/Movement/RawMove.h20
-rw-r--r--src/Platform/Platform.h1
9 files changed, 93 insertions, 56 deletions
diff --git a/src/GCodes/GCodes.cpp b/src/GCodes/GCodes.cpp
index d0cf1c5d..12ba77aa 100644
--- a/src/GCodes/GCodes.cpp
+++ b/src/GCodes/GCodes.cpp
@@ -1909,13 +1909,13 @@ bool GCodes::DoStraightMove(GCodeBuffer& gb, bool isCoordinated, const char *& e
else
#endif
{
- ToolOffsetTransform(moveState.currentUserPosition, moveState.coords, axesMentioned);
+ ToolOffsetTransform(moveState.currentUserPosition, moveState.coords, axesMentioned); // apply tool offset, baby stepping, Z hop and axis scaling
}
- // apply tool offset, baby stepping, Z hop and axis scaling
+
AxesBitmap effectiveAxesHomed = axesVirtuallyHomed;
if (doingManualBedProbe)
{
- effectiveAxesHomed.ClearBit(Z_AXIS); // if doing a manual Z probe, don't limit the Z movement
+ effectiveAxesHomed.ClearBit(Z_AXIS); // if doing a manual Z probe, don't limit the Z movement
}
const LimitPositionResult lp = reprap.GetMove().GetKinematics().LimitPosition(moveState.coords, moveState.initialCoords, numVisibleAxes, effectiveAxesHomed, moveState.isCoordinated, limitAxes);
@@ -2014,6 +2014,8 @@ bool GCodes::DoStraightMove(GCodeBuffer& gb, bool isCoordinated, const char *& e
}
moveState.doingArcMove = false;
+ moveState.linearAxesMentioned = axesMentioned.Intersects(reprap.GetPlatform().GetLinearAxes());
+ moveState.rotationalAxesMentioned = axesMentioned.Intersects(reprap.GetPlatform().GetRotationalAxes());
FinaliseMove(gb);
UnlockAll(gb); // allow pause
err = nullptr;
@@ -2370,6 +2372,8 @@ bool GCodes::DoArcMove(GCodeBuffer& gb, bool clockwise, const char *& err)
moveState.arcAxis1 = axis1;
moveState.doingArcMove = true;
moveState.xyPlane = (selectedPlane == 0);
+ moveState.linearAxesMentioned = axesMentioned.Intersects(reprap.GetPlatform().GetLinearAxes());
+ moveState.rotationalAxesMentioned = axesMentioned.Intersects(reprap.GetPlatform().GetRotationalAxes());
FinaliseMove(gb);
UnlockAll(gb); // allow pause
// debugPrintf("Radius %.2f, initial angle %.1f, increment %.1f, segments %u\n",
@@ -2441,7 +2445,8 @@ bool GCodes::TravelToStartPoint(GCodeBuffer& gb) noexcept
ToolOffsetTransform(buildObjects.GetInitialPosition().moveCoords, moveState.coords);
moveState.feedRate = buildObjects.GetInitialPosition().feedRate;
moveState.tool = reprap.GetCurrentTool();
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = moveState.rotationalAxesMentioned = true; // assume that both linear and rotational axes might be moving
+ NewSingleSegmentMoveAvailable();
return true;
}
@@ -2573,14 +2578,14 @@ void GCodes::ClearMove() noexcept
moveFractionToSkip = 0.0;
}
-// Flag that a new move is available for consumption by the Move subsystem
+// Flag that a new single-segment move is available for consumption by the Move subsystem
// Code that sets up a new move should ensure that segmentsLeft is zero, then set up all the move parameters,
// then call this function to update SegmentsLeft safely in a multi-threaded environment
-void GCodes::NewMoveAvailable(unsigned int sl) noexcept
+void GCodes::NewSingleSegmentMoveAvailable() noexcept
{
- moveState.totalSegments = sl;
+ moveState.totalSegments = 1;
__DMB(); // make sure that all the move details have been written first
- moveState.segmentsLeft = sl; // set the number of segments to indicate that a move is available to be taken
+ moveState.segmentsLeft = 1; // set the number of segments to indicate that a move is available to be taken
reprap.GetMove().MoveAvailable(); // notify the Move task that we have a move
}
@@ -3850,7 +3855,7 @@ GCodeResult GCodes::RetractFilament(GCodeBuffer& gb, bool retract)
}
moveState.feedRate = currentTool->GetRetractSpeed() * tool->DriveCount();
moveState.canPauseAfter = false; // don't pause after a retraction because that could cause too much retraction
- NewMoveAvailable(1);
+ NewSingleSegmentMoveAvailable();
}
if (currentTool->GetRetractHop() > 0.0)
{
@@ -3864,7 +3869,8 @@ GCodeResult GCodes::RetractFilament(GCodeBuffer& gb, bool retract)
moveState.coords[Z_AXIS] -= moveState.currentZHop;
moveState.currentZHop = 0.0;
moveState.canPauseAfter = false; // don't pause in the middle of a command
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = true; // assume that both linear and rotational axes might be moving
+ NewSingleSegmentMoveAvailable();
gb.SetState(GCodeState::doingFirmwareUnRetraction);
}
else
@@ -3879,7 +3885,7 @@ GCodeResult GCodes::RetractFilament(GCodeBuffer& gb, bool retract)
}
moveState.feedRate = currentTool->GetUnRetractSpeed() * tool->DriveCount();
moveState.canPauseAfter = true;
- NewMoveAvailable(1);
+ NewSingleSegmentMoveAvailable();
}
}
currentTool->SetRetracted(retract);
diff --git a/src/GCodes/GCodes.h b/src/GCodes/GCodes.h
index 49fb067b..9e482fcf 100644
--- a/src/GCodes/GCodes.h
+++ b/src/GCodes/GCodes.h
@@ -504,7 +504,7 @@ private:
void SaveResumeInfo(bool wasPowerFailure) noexcept;
#endif
- void NewMoveAvailable(unsigned int sl) noexcept; // Flag that a new move is available
+ void NewSingleSegmentMoveAvailable() noexcept; // Flag that a new move is available
void NewMoveAvailable() noexcept; // Flag that a new move is available
void SetMoveBufferDefaults() noexcept; // Set up default values in the move buffer
diff --git a/src/GCodes/GCodes2.cpp b/src/GCodes/GCodes2.cpp
index d807702a..0ece485b 100644
--- a/src/GCodes/GCodes2.cpp
+++ b/src/GCodes/GCodes2.cpp
@@ -2511,7 +2511,9 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) THROWS(GCodeEx
SetMoveBufferDefaults();
moveState.feedRate = ConvertSpeedFromMmPerMin(DefaultFeedRate);
moveState.tool = reprap.GetCurrentTool();
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = axesMentioned.Intersects(reprap.GetPlatform().GetLinearAxes());
+ moveState.rotationalAxesMentioned = axesMentioned.Intersects(reprap.GetPlatform().GetRotationalAxes());
+ NewSingleSegmentMoveAvailable();
}
}
else
@@ -3935,12 +3937,21 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) THROWS(GCodeEx
break;
case 673: // Align plane on rotary axis
- if (numTotalAxes < U_AXIS)
+ if (numTotalAxes <= U_AXIS)
{
reply.copy("Insufficient axes configured");
result = GCodeResult::error;
}
- else if (LockMovementAndWaitForStandstill(gb))
+ else if (!LockMovementAndWaitForStandstill(gb))
+ {
+ result = GCodeResult::notFinished;
+ }
+ else if (!AllAxesAreHomed())
+ {
+ reply.copy("Home the axes first");
+ result = GCodeResult::error;
+ }
+ else
{
Move& move = reprap.GetMove();
if (move.GetNumProbedProbePoints() < 2)
@@ -3948,11 +3959,6 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) THROWS(GCodeEx
reply.copy("Insufficient probe points");
result = GCodeResult::error;
}
- else if (!AllAxesAreHomed())
- {
- reply.copy("Home the axes first");
- result = GCodeResult::error;
- }
else
{
// See which rotary axis needs to be compensated (if any)
@@ -3977,11 +3983,18 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) THROWS(GCodeEx
SetMoveBufferDefaults();
if (axisToUse != 0)
{
+ if (!reprap.GetPlatform().IsAxisRotational(axisToUse))
+ {
+ reply.printf("%c axis is not rotary", axisLetters[axisToUse]);
+ result = GCodeResult::error;
+ break;
+ }
+
// An axis letter is given, so try to level the given axis
const float correctionAngle = atanf((z2 - z1) / (a2 - a1)) * 180.0 / M_PI;
const float correctionFactor = gb.Seen('S') ? gb.GetFValue() : 1.0;
moveState.coords[axisToUse] += correctionAngle * correctionFactor;
-
+ moveState.rotationalAxesMentioned = true;
reply.printf("%c axis is off by %.2f deg", axisLetters[axisToUse], (double)correctionAngle);
HandleReply(gb, GCodeResult::notFinished, reply.c_str());
}
@@ -4000,6 +4013,7 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) THROWS(GCodeEx
((z4 - z3) * (a2 - a1) - (z2 - z1) * (a4 - a3));
moveState.coords[(x1 == x2) ? Y_AXIS : X_AXIS] += aS;
moveState.coords[Z_AXIS] += zS;
+ moveState.linearAxesMentioned = true;
reply.printf("%c is offset by %.2fmm, Z is offset by %.2fmm", (x2 == x1) ? 'Y' : 'X', (double)aS, (double)zS);
HandleReply(gb, GCodeResult::notFinished, reply.c_str());
@@ -4019,15 +4033,11 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) THROWS(GCodeEx
moveState.feedRate = gb.LatestMachineState().feedRate;
moveState.usingStandardFeedrate = true;
moveState.tool = reprap.GetCurrentTool();
- NewMoveAvailable(1);
+ NewSingleSegmentMoveAvailable();
gb.SetState(GCodeState::waitingForSpecialMoveToComplete);
}
}
- else
- {
- result = GCodeResult::notFinished;
- }
break;
#if false
diff --git a/src/GCodes/GCodes3.cpp b/src/GCodes/GCodes3.cpp
index 9e1895bb..f5d3acdb 100644
--- a/src/GCodes/GCodes3.cpp
+++ b/src/GCodes/GCodes3.cpp
@@ -998,7 +998,9 @@ bool GCodes::SetupM585ProbingMove(GCodeBuffer& gb) noexcept
moveState.checkEndstops = true;
moveState.canPauseAfter = false;
zProbeTriggered = false;
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = reprap.GetPlatform().IsAxisLinear(m585Settings.axisNumber);
+ moveState.rotationalAxesMentioned = reprap.GetPlatform().IsAxisRotational(m585Settings.axisNumber);
+ NewSingleSegmentMoveAvailable();
return true;
}
@@ -1053,7 +1055,9 @@ bool GCodes::SetupM675ProbingMove(GCodeBuffer& gb, bool towardsMin) noexcept
moveState.checkEndstops = true;
moveState.canPauseAfter = false;
zProbeTriggered = false;
- NewMoveAvailable(1); // kick off the move
+ moveState.linearAxesMentioned = reprap.GetPlatform().IsAxisLinear(m675Settings.axisNumber);
+ moveState.rotationalAxesMentioned = reprap.GetPlatform().IsAxisRotational(m675Settings.axisNumber);
+ NewSingleSegmentMoveAvailable(); // kick off the move
return true;
}
@@ -1064,7 +1068,9 @@ void GCodes::SetupM675BackoffMove(GCodeBuffer& gb, float position) noexcept
moveState.coords[m675Settings.axisNumber] = position;
moveState.feedRate = m675Settings.feedRate;
moveState.canPauseAfter = false;
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = reprap.GetPlatform().IsAxisLinear(m675Settings.axisNumber);
+ moveState.rotationalAxesMentioned = reprap.GetPlatform().IsAxisRotational(m675Settings.axisNumber);
+ NewSingleSegmentMoveAvailable();
}
// Deal with a M905
diff --git a/src/GCodes/GCodes4.cpp b/src/GCodes/GCodes4.cpp
index 81d5e0b8..3d7c6c08 100644
--- a/src/GCodes/GCodes4.cpp
+++ b/src/GCodes/GCodes4.cpp
@@ -487,7 +487,7 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
}
SetMoveBufferDefaults();
ToolOffsetTransform(moveState.currentUserPosition, moveState.coords);
- moveState.feedRate = ConvertSpeedFromMmPerMin(DefaultFeedRate); // ask for a good feed rate, we may have paused during a slow move
+ moveState.feedRate = ConvertSpeedFromMmPerMin(DefaultFeedRate); // ask for a good feed rate, we may have paused during a slow move
moveState.tool = reprap.GetCurrentTool(); // needed so that bed compensation is applied correctly
if (gb.GetState() == GCodeState::resuming1 && currentZ > pauseRestorePoint.moveCoords[Z_AXIS])
{
@@ -500,7 +500,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
// Just move to the saved position in one go
gb.SetState(GCodeState::resuming3);
}
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = moveState.rotationalAxesMentioned = true; // assume that both linear and rotational axes might be moving
+ NewSingleSegmentMoveAvailable();
}
break;
@@ -644,7 +645,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.coords[axis1Num] = axesCoords[axis1Num];
moveState.coords[Z_AXIS] = zp->GetStartingHeight();
moveState.feedRate = zp->GetTravelSpeed();
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = moveState.rotationalAxesMentioned = true; // assume that both linear and rotational axes might be moving
+ NewSingleSegmentMoveAvailable();
InitialiseTaps(false);
gb.AdvanceState();
@@ -724,7 +726,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.reduceAcceleration = true;
moveState.coords[Z_AXIS] = -zp->GetDiveHeight() + zp->GetActualTriggerHeight();
moveState.feedRate = zp->GetProbingSpeed(tapsDone);
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = true; // assume that both linear and rotational axes might be moving
+ NewSingleSegmentMoveAvailable();
gb.AdvanceState();
}
}
@@ -775,7 +778,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.coords[Z_AXIS] = zp->GetStartingHeight();
moveState.feedRate = zp->GetTravelSpeed();
}
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = true;
+ NewSingleSegmentMoveAvailable();
gb.AdvanceState();
break;
@@ -915,7 +919,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.coords[Z_AXIS] = zp->GetStartingHeight();
moveState.feedRate = zp->GetTravelSpeed();
}
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = true;
+ NewSingleSegmentMoveAvailable();
gb.AdvanceState();
break;
@@ -929,7 +934,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
const auto zp = platform.GetZProbeOrDefault(currentZProbeNumber);
moveState.coords[Z_AXIS] = zp->GetStartingHeight();
moveState.feedRate = zp->GetTravelSpeed();
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = moveState.rotationalAxesMentioned = true; // assume that both linear and rotational axes might be moving
+ NewSingleSegmentMoveAvailable();
InitialiseTaps(false);
gb.AdvanceState();
@@ -1008,7 +1014,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
? platform.AxisMinimum(Z_AXIS) - zp->GetDiveHeight() + zp->GetActualTriggerHeight() // Z axis has been homed, so no point in going very far
: -1.1 * platform.AxisTotalLength(Z_AXIS); // Z axis not homed yet, so treat this as a homing move
moveState.feedRate = zp->GetProbingSpeed(tapsDone);
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = true;
+ NewSingleSegmentMoveAvailable();
gb.AdvanceState();
}
}
@@ -1102,7 +1109,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.coords[Z_AXIS] = zp->GetStartingHeight();
moveState.feedRate = zp->GetTravelSpeed();
}
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = true;
+ NewSingleSegmentMoveAvailable();
gb.AdvanceState();
break;
@@ -1297,7 +1305,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.reduceAcceleration = true;
straightProbeSettings.SetCoordsToTarget(moveState.coords);
moveState.feedRate = zp->GetProbingSpeed(0);
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = moveState.rotationalAxesMentioned = true;
+ NewSingleSegmentMoveAvailable();
gb.AdvanceState();
}
}
@@ -1343,7 +1352,8 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.filePos = (&gb == fileGCode) ? gb.GetFilePosition() : noFilePosition;
moveState.canPauseAfter = false; // don't pause after a retraction because that could cause too much retraction
moveState.currentZHop = tool->GetRetractHop();
- NewMoveAvailable(1);
+ moveState.linearAxesMentioned = true;
+ NewSingleSegmentMoveAvailable();
}
gb.SetState(GCodeState::normal);
}
@@ -1366,7 +1376,7 @@ void GCodes::RunStateMachine(GCodeBuffer& gb, const StringRef& reply) noexcept
moveState.feedRate = tool->GetUnRetractSpeed() * tool->DriveCount();
moveState.filePos = (&gb == fileGCode) ? gb.GetFilePosition() : noFilePosition;
moveState.canPauseAfter = true;
- NewMoveAvailable(1);
+ NewSingleSegmentMoveAvailable();
}
gb.SetState(GCodeState::normal);
}
diff --git a/src/Movement/DDA.cpp b/src/Movement/DDA.cpp
index 7050a28e..5ea10f5b 100644
--- a/src/Movement/DDA.cpp
+++ b/src/Movement/DDA.cpp
@@ -345,17 +345,17 @@ bool DDA::InitStandardMove(DDARing& ring, const RawMove &nextMove, bool doMotorM
directionVector[drive] = positionDelta;
if (positionDelta != 0.0)
{
- if (reprap.GetPlatform().IsAxisRotational(drive))
+ if (reprap.GetPlatform().IsAxisRotational(drive) && nextMove.rotationalAxesMentioned)
{
rotationalAxesMoving = true;
}
- else
+ else if (nextMove.linearAxesMentioned)
{
linearAxesMoving = true;
- }
- if (Tool::GetXAxes(nextMove.tool).IsBitSet(drive) || Tool::GetYAxes(nextMove.tool).IsBitSet(drive))
- {
- flags.xyMoving = true; // this move has XY movement in user space, before axis were mapped
+ if (Tool::GetXAxes(nextMove.tool).IsBitSet(drive) || Tool::GetYAxes(nextMove.tool).IsBitSet(drive))
+ {
+ flags.xyMoving = true; // this move has XY movement in user space, before axis were mapped
+ }
}
}
}
diff --git a/src/Movement/RawMove.cpp b/src/Movement/RawMove.cpp
index bdcdba51..d1c1e65e 100644
--- a/src/Movement/RawMove.cpp
+++ b/src/Movement/RawMove.cpp
@@ -18,6 +18,8 @@ void RawMove::SetDefaults(size_t firstDriveToZero) noexcept
checkEndstops = false;
reduceAcceleration = false;
hasPositiveExtrusion = false;
+ linearAxesMentioned = false;
+ rotationalAxesMentioned = false;
filePos = noFilePosition;
tool = nullptr;
cosXyAngle = 1.0;
diff --git a/src/Movement/RawMove.h b/src/Movement/RawMove.h
index 19727e4d..7bccf816 100644
--- a/src/Movement/RawMove.h
+++ b/src/Movement/RawMove.h
@@ -21,21 +21,23 @@ struct RawMove
float proportionDone; // what proportion of the entire move has been done when this segment is complete
float cosXyAngle; // the cosine of the change in XY angle between the previous move and this move
const Tool *tool; // which tool (if any) is being used
-#if SUPPORT_LASER || SUPPORT_IOBITS
- LaserPwmOrIoBits laserPwmOrIoBits; // the laser PWM or port bit settings required
-#else
- uint16_t padding;
-#endif
- uint8_t moveType; // the S parameter from the G0 or G1 command, 0 for a normal move
-
- uint8_t applyM220M221 : 1, // true if this move is affected by M220 and M221 (this could be moved to ExtendedRawMove)
+ uint16_t moveType : 3, // the H parameter from the G0 or G1 command, 0 for a normal move
+ applyM220M221 : 1, // true if this move is affected by M220 and M221 (this could be moved to ExtendedRawMove)
usePressureAdvance : 1, // true if we want to us extruder pressure advance, if there is any extrusion
canPauseAfter : 1, // true if we can pause just after this move and successfully restart
hasPositiveExtrusion : 1, // true if the move includes extrusion; only valid if the move was set up by SetupMove
isCoordinated : 1, // true if this is a coordinated move
usingStandardFeedrate : 1, // true if this move uses the standard feed rate
checkEndstops : 1, // true if any endstops or the Z probe can terminate the move
- reduceAcceleration : 1; // true if Z probing so we should limit the Z acceleration
+ reduceAcceleration : 1, // true if Z probing so we should limit the Z acceleration
+ linearAxesMentioned : 1, // true if any linear axes were mentioned in the movement command
+ rotationalAxesMentioned: 1; // true if any rotational axes were mentioned in the movement command
+
+#if SUPPORT_LASER || SUPPORT_IOBITS
+ LaserPwmOrIoBits laserPwmOrIoBits; // the laser PWM or port bit settings required
+#else
+ uint16_t padding;
+#endif
// If adding any more fields, keep the total size a multiple of 4 bytes so that we can use our optimised assignment operator
void SetDefaults(size_t firstDriveToZero) noexcept; // set up default values
diff --git a/src/Platform/Platform.h b/src/Platform/Platform.h
index 9e0c9986..900b94f6 100644
--- a/src/Platform/Platform.h
+++ b/src/Platform/Platform.h
@@ -499,6 +499,7 @@ public:
inline AxesBitmap GetLinearAxes() const noexcept { return linearAxes; }
inline AxesBitmap GetRotationalAxes() const noexcept { return rotationalAxes; }
+ inline bool IsAxisLinear(size_t axis) const noexcept { return linearAxes.IsBitSet(axis); }
inline bool IsAxisRotational(size_t axis) const noexcept { return rotationalAxes.IsBitSet(axis); }
inline bool IsAxisContinuous(size_t axis) const noexcept { return continuousAxes.IsBitSet(axis); }
#if 0 // shortcut axes not implemented yet