Rename to TNCat, add chip chase, G-codes and a Wayland layout

The project is now TNCat, after HEIDENHAIN's TNC controls. Binary, QML module
URI, settings path, bundle id and .desktop entry follow; the environment
switches are TNCAT_*.

Entertainment: a curled steel chip skitters in every minute or two, the cat
runs it down and bats it away a few times before sitting back looking pleased.
Mood changes flash the matching machine code above its head, G00 for a sprint,
G01 for a stroll, M08 while it washes, G28 for a stretch, M30 for a nap.

Window handling was split into two layouts. X11 and macOS keep one small window
per object, positioned by the app. Wayland gets a screen-filling overlay whose
input region is masked down to the cat, since a Wayland client cannot place its
own window. Overlay mode is automatic there and available elsewhere via
TNCAT_OVERLAY=1.

Also adds a ground-line setting so the cat can walk above a softkey row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
JulianandClaude Opus 5 committed 2026-09-10 22:53:40 +02:00
1 parent acef398f49
commit 0f1df75a1d
19 files changed
+817 -185

No files matched your search

+128 -18
View File
@@ -4,7 +4,12 @@
#include <QCursor>
#include <QMetaEnum>
#include <QRandomGenerator>
#include <QWindow>
namespace {
constexpr qreal kChipReach = 26.0; // how close the nose has to get
constexpr int kChipMinDelayMs = 45000;
constexpr int kChipMaxDelayMs = 150000;
}
CatController::CatController(AppSettings *settings, QObject *parent)
: QObject(parent)
@@ -23,8 +28,11 @@ CatController::CatController(AppSettings *settings, QObject *parent)
connect(&m_cursorTimer, &QTimer::timeout, this, &CatController::trackCursor);
m_cursorTimer.start();
// WORKCAT_POSE=Sleeping freezes the cat in one mood (handy for screenshots).
const QByteArray pose = qgetenv("WORKCAT_POSE");
m_chipTimer.setSingleShot(true);
connect(&m_chipTimer, &QTimer::timeout, this, &CatController::tossChip);
// TNCAT_POSE=Sleeping freezes the cat in one mood (handy for screenshots).
const QByteArray pose = qgetenv("TNCAT_POSE");
if (!pose.isEmpty()) {
const QMetaEnum me = QMetaEnum::fromType<Mood>();
bool ok = false;
@@ -34,13 +42,15 @@ CatController::CatController(AppSettings *settings, QObject *parent)
setMood(static_cast<Mood>(value));
}
}
if (!m_frozen)
if (!m_frozen) {
pickNextMood();
scheduleChip();
}
}
void CatController::setWindow(QWindow *window)
qreal CatController::catNose() const
{
m_window = window;
return m_facingRight ? m_x + m_catWidth * 0.78 : m_x + m_catWidth * 0.22;
}
void CatController::setX(qreal x)
@@ -51,12 +61,12 @@ void CatController::setX(qreal x)
emit xChanged();
}
void CatController::setScreenWidth(qreal width)
void CatController::setStageWidth(qreal width)
{
if (qFuzzyCompare(width, m_screenWidth))
if (qFuzzyCompare(width, m_stageWidth))
return;
m_screenWidth = width;
emit screenWidthChanged();
m_stageWidth = width;
emit stageWidthChanged();
}
void CatController::setCatWidth(qreal width)
@@ -67,6 +77,14 @@ void CatController::setCatWidth(qreal width)
emit catWidthChanged();
}
void CatController::setCatCenter(const QPointF &center)
{
if (center == m_catCenter)
return;
m_catCenter = center;
emit catCenterChanged();
}
void CatController::setPaused(bool paused)
{
if (paused == m_paused)
@@ -75,9 +93,12 @@ void CatController::setPaused(bool paused)
emit pausedChanged();
if (paused) {
m_moodTimer.stop();
m_chipTimer.stop();
endChase();
setMood(Mood::Sleeping);
} else {
pickNextMood();
scheduleChip();
}
}
@@ -110,6 +131,7 @@ void CatController::pet()
if (m_paused || m_frozen)
return;
m_settings->incrementPetCount();
endChase();
holdMood(Mood::Loving, 1800);
}
@@ -118,6 +140,7 @@ void CatController::doTrick(Mood mood)
if (m_paused)
return;
m_frozen = false;
endChase();
holdMood(mood, mood == Mood::Sleeping ? 15000 : 3500);
}
@@ -127,17 +150,85 @@ void CatController::holdMood(Mood mood, int ms)
m_moodTimer.start(ms);
}
// A curled metal chip skitters in from one side and the cat goes after it.
void CatController::tossChip()
{
if (m_paused || m_frozen || m_chipVisible)
return;
if (!m_settings->chipsEnabled()) {
scheduleChip();
return;
}
auto *rng = QRandomGenerator::global();
const bool fromLeft = rng->bounded(2) == 0;
const qreal margin = qMin(200.0, m_stageWidth * 0.2);
setChipX(fromLeft ? margin : m_stageWidth - margin);
m_batsLeft = rng->bounded(2, 5);
m_chipVisible = true;
emit chipVisibleChanged();
m_moodTimer.stop();
setMood(Mood::Chasing);
}
void CatController::batChip()
{
auto *rng = QRandomGenerator::global();
++m_chipKick;
emit chipKickChanged();
const qreal distance = rng->bounded(140, 380);
qreal next = m_chipX + (m_facingRight ? distance : -distance);
const qreal edge = 40.0;
if (next < edge || next > m_stageWidth - edge) {
next = qBound(edge, next, m_stageWidth - edge);
--m_batsLeft; // a chip against the wall is nearly done for
}
setChipX(next);
if (--m_batsLeft <= 0) {
endChase();
holdMood(Mood::Sitting, 2500); // pleased with itself
scheduleChip();
} else {
holdMood(Mood::Batting, 420);
}
}
void CatController::endChase()
{
if (!m_chipVisible)
return;
m_chipVisible = false;
m_batsLeft = 0;
emit chipVisibleChanged();
}
void CatController::scheduleChip()
{
m_chipTimer.start(QRandomGenerator::global()->bounded(kChipMinDelayMs, kChipMaxDelayMs));
}
void CatController::setChipX(qreal x)
{
if (qFuzzyCompare(x, m_chipX))
return;
m_chipX = x;
emit chipXChanged();
}
void CatController::trackCursor()
{
if (!m_window || !m_settings->followCursor()) {
if (m_catCenter.isNull() || !m_settings->followCursor()) {
if (!m_look.isNull()) {
m_look = {};
emit lookChanged();
}
return;
}
const QPointF center = m_window->geometry().center();
QPointF delta = QCursor::pos() - center;
QPointF delta = QCursor::pos() - m_catCenter;
if (!m_facingRight)
delta.setX(-delta.x());
const qreal range = 400.0;
@@ -154,14 +245,28 @@ void CatController::tick()
const qreal dt = qMin(now - m_lastTickMs, qint64(100)) / 1000.0;
m_lastTickMs = now;
const bool moving = m_mood == Mood::Walking || m_mood == Mood::Running;
if (!moving || m_dragging || m_paused || m_frozen)
if (m_dragging || m_paused || m_frozen)
return;
const qreal maxX = qMax(0.0, m_stageWidth - m_catWidth);
if (m_mood == Mood::Chasing) {
const qreal delta = m_chipX - catNose();
setFacingRight(delta >= 0);
if (qAbs(delta) <= kChipReach) {
batChip();
return;
}
const qreal step = m_settings->walkSpeed() * 4.0 * dt;
setX(qBound(0.0, m_x + (delta > 0 ? step : -step), maxX));
return;
}
if (m_mood != Mood::Walking && m_mood != Mood::Running)
return;
const qreal speed = m_settings->walkSpeed() * (m_mood == Mood::Running ? 4.5 : 1.0);
const qreal step = speed * dt;
qreal next = m_x + (m_facingRight ? step : -step);
const qreal maxX = qMax(0.0, m_screenWidth - m_catWidth);
qreal next = m_x + (m_facingRight ? speed * dt : -speed * dt);
if (next >= maxX) {
next = maxX;
setFacingRight(false);
@@ -196,6 +301,11 @@ void CatController::pickNextMood()
{
if (m_paused || m_dragging || m_frozen)
return;
if (m_chipVisible) { // a chase is still on, keep after the chip
setMood(Mood::Chasing);
return;
}
auto *rng = QRandomGenerator::global();
const int roll = rng->bounded(100);