Mat4x4 row labels, no scientific notation, per-component inline editing

- Add row0..row3 labels to Mat4x4 grid display with aligned columns
- Rewrite fmtFloat() to never use scientific notation (plain decimal + trim)
- Enable per-component inline editing for all 16 Mat4x4 floats
- Fix click-to-edit always selecting first component (thread column from hitTestTarget)
- Add isMatrixKind() helper, remove Mat4x4 from context menu edit exclusion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
IChooseYou
2026-02-11 10:36:15 -07:00
parent df07b61144
commit 968476b65a
5 changed files with 84 additions and 42 deletions

View File

@@ -919,6 +919,11 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
addr += subLine * 4; addr += subLine * 4;
editKind = NodeKind::Float; editKind = NodeKind::Float;
} }
// For Mat4x4 components: subLine encodes flat index (row*4 + col), 0-15
if (node.kind == NodeKind::Mat4x4 && subLine >= 0 && subLine < 16) {
addr += subLine * 4;
editKind = NodeKind::Float;
}
bool ok; bool ok;
QByteArray newBytes; QByteArray newBytes;
@@ -1068,7 +1073,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addSeparator(); menu.addSeparator();
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
&& node.kind != NodeKind::Padding && node.kind != NodeKind::Mat4x4 && node.kind != NodeKind::Padding
&& m_doc->provider->isWritable(); && m_doc->provider->isWritable();
if (isEditable) { if (isEditable) {
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() { menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {

View File

@@ -134,6 +134,9 @@ inline constexpr bool isHexNode(NodeKind k) {
inline constexpr bool isVectorKind(NodeKind k) { inline constexpr bool isVectorKind(NodeKind k) {
return k == NodeKind::Vec2 || k == NodeKind::Vec3 || k == NodeKind::Vec4; return k == NodeKind::Vec2 || k == NodeKind::Vec3 || k == NodeKind::Vec4;
} }
inline constexpr bool isMatrixKind(NodeKind k) {
return k == NodeKind::Mat4x4;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) { inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
QStringList out; QStringList out;

View File

@@ -993,7 +993,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
static bool hitTestTarget(QsciScintilla* sci, static bool hitTestTarget(QsciScintilla* sci,
const QVector<LineMeta>& meta, const QVector<LineMeta>& meta,
const QPoint& viewportPos, const QPoint& viewportPos,
int& outLine, EditTarget& outTarget) int& outLine, int& outCol, EditTarget& outTarget)
{ {
long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)viewportPos.x(), (long)viewportPos.y()); (unsigned long)viewportPos.x(), (long)viewportPos.y());
@@ -1002,6 +1002,7 @@ static bool hitTestTarget(QsciScintilla* sci,
(unsigned long)pos); (unsigned long)pos);
int col = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN, int col = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
(unsigned long)pos); (unsigned long)pos);
outCol = col;
if (line < 0 || line >= meta.size()) return false; if (line < 0 || line >= meta.size()) return false;
QString lineText = getLineText(sci, line); QString lineText = getLineText(sci, line);
@@ -1183,12 +1184,12 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
} }
// CommandRow: try chevron/ADDR edit or consume // CommandRow: try chevron/ADDR edit or consume
if (h.nodeId == kCommandRowId) { if (h.nodeId == kCommandRowId) {
int tLine; EditTarget t; int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) { if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
if (t == EditTarget::TypeSelector) if (t == EditTarget::TypeSelector)
emit typeSelectorRequested(); emit typeSelectorRequested();
else else
beginInlineEdit(t, tLine); beginInlineEdit(t, tLine, tCol);
} }
return true; // consume all CommandRow clicks return true; // consume all CommandRow clicks
} }
@@ -1197,11 +1198,11 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)); bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
// Single-click on editable token of already-selected node → edit // Single-click on editable token of already-selected node → edit
int tLine; EditTarget t; int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) { if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
if (alreadySelected && plain) { if (alreadySelected && plain) {
m_pendingClickNodeId = 0; m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine); return beginInlineEdit(t, tLine, tCol);
} }
} }
@@ -1276,14 +1277,14 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
if (obj == m_sci->viewport() && !m_editState.active if (obj == m_sci->viewport() && !m_editState.active
&& event->type() == QEvent::MouseButtonDblClick) { && event->type() == QEvent::MouseButtonDblClick) {
auto* me = static_cast<QMouseEvent*>(event); auto* me = static_cast<QMouseEvent*>(event);
int line; EditTarget t; int line, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), line, t)) { if (hitTestTarget(m_sci, m_meta, me->pos(), line, tCol, t)) {
m_pendingClickNodeId = 0; // cancel deferred selection change m_pendingClickNodeId = 0; // cancel deferred selection change
// Narrow selection to this node before editing // Narrow selection to this node before editing
auto h = hitTest(me->pos()); auto h = hitTest(me->pos());
if (h.nodeId != 0 && h.nodeId != kCommandRowId) if (h.nodeId != 0 && h.nodeId != kCommandRowId)
emit nodeClicked(h.line, h.nodeId, Qt::NoModifier); emit nodeClicked(h.line, h.nodeId, Qt::NoModifier);
return beginInlineEdit(t, line); return beginInlineEdit(t, line, tCol);
} }
return true; // consume even on miss (prevent QScintilla word-select) return true; // consume even on miss (prevent QScintilla word-select)
} }
@@ -1469,14 +1470,14 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
// ── Begin inline edit ── // ── Begin inline edit ──
bool RcxEditor::beginInlineEdit(EditTarget target, int line) { bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit
// Array element type and pointer target: handled by TypeSelectorPopup, not inline edit // Array element type and pointer target: handled by TypeSelectorPopup, not inline edit
if (target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) { if (target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) {
if (line < 0) { if (line < 0) {
int col; int c;
m_sci->getCursorPosition(&line, &col); m_sci->getCursorPosition(&line, &c);
} }
auto* lm = metaForLine(line); auto* lm = metaForLine(line);
if (!lm) return false; if (!lm) return false;
@@ -1498,10 +1499,11 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
m_hintLine = -1; m_hintLine = -1;
if (line >= 0) { if (line >= 0) {
m_sci->setCursorPosition(line, 0); m_sci->setCursorPosition(line, col >= 0 ? col : 0);
}
if (col < 0) {
m_sci->getCursorPosition(&line, &col);
} }
int col;
m_sci->getCursorPosition(&line, &col);
auto* lm = metaForLine(line); auto* lm = metaForLine(line);
if (!lm) return false; if (!lm) return false;
// Allow nodeIdx=-1 only for CommandRow editing (command bar) // Allow nodeIdx=-1 only for CommandRow editing (command bar)
@@ -1522,28 +1524,23 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
QString trimmed = lineText.mid(norm.start, norm.end - norm.start); QString trimmed = lineText.mid(norm.start, norm.end - norm.start);
int vecComponent = 0; // which vector component (0-3) int vecComponent = 0; // which vector/matrix component
// For vector value editing: narrow span to the clicked component // Helper: parse comma-separated components, narrow span to clicked one
if (target == EditTarget::Value && isVectorKind(lm->nodeKind)) { auto narrowToComponent = [&](const QString& inner, int innerAbsStart) {
int cursorCol = col; // col from getCursorPosition above
// Find comma positions within the value span to identify components
QVector<int> compStarts, compEnds; QVector<int> compStarts, compEnds;
int pos = 0; for (int i = 0; i < inner.size(); i++) {
for (int i = 0; i < trimmed.size(); i++) { if (inner[i] == ',') {
if (trimmed[i] == ',') {
compEnds.append(i); compEnds.append(i);
// skip ", " separator
int next = i + 1; int next = i + 1;
while (next < trimmed.size() && trimmed[next] == ' ') next++; while (next < inner.size() && inner[next] == ' ') next++;
compStarts.append(next); compStarts.append(next);
} }
} }
compStarts.prepend(0); compStarts.prepend(0);
compEnds.append(trimmed.size()); compEnds.append(inner.size());
// Find which component the cursor is in int relCol = col - innerAbsStart;
int relCol = cursorCol - norm.start;
vecComponent = 0; vecComponent = 0;
for (int i = 0; i < compStarts.size(); i++) { for (int i = 0; i < compStarts.size(); i++) {
if (relCol >= compStarts[i] && (i == compStarts.size() - 1 || relCol < compStarts[i + 1])) if (relCol >= compStarts[i] && (i == compStarts.size() - 1 || relCol < compStarts[i + 1]))
@@ -1551,27 +1548,47 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
} }
if (vecComponent >= compStarts.size()) vecComponent = compStarts.size() - 1; if (vecComponent >= compStarts.size()) vecComponent = compStarts.size() - 1;
// Narrow span to just this component int cStart = innerAbsStart + compStarts[vecComponent];
int cStart = norm.start + compStarts[vecComponent]; int cEnd = innerAbsStart + compEnds[vecComponent];
int cEnd = norm.start + compEnds[vecComponent];
// Trim trailing spaces from component
while (cEnd > cStart && lineText[cEnd - 1] == ' ') cEnd--; while (cEnd > cStart && lineText[cEnd - 1] == ' ') cEnd--;
norm.start = cStart; norm.start = cStart;
norm.end = cEnd; norm.end = cEnd;
trimmed = lineText.mid(norm.start, norm.end - norm.start); trimmed = lineText.mid(norm.start, norm.end - norm.start);
};
// For vector value editing: narrow span to the clicked component
if (target == EditTarget::Value && isVectorKind(lm->nodeKind)) {
narrowToComponent(trimmed, norm.start);
}
// For Mat4x4 value editing: skip "rowN [...]" and narrow to clicked component
if (target == EditTarget::Value && isMatrixKind(lm->nodeKind)) {
int bracketOpen = trimmed.indexOf('[');
int bracketClose = trimmed.lastIndexOf(']');
if (bracketOpen < 0 || bracketClose <= bracketOpen)
return false;
QString inner = trimmed.mid(bracketOpen + 1, bracketClose - bracketOpen - 1);
int innerAbsStart = norm.start + bracketOpen + 1;
narrowToComponent(inner, innerAbsStart);
} }
m_editState.active = true; m_editState.active = true;
m_editState.line = line; m_editState.line = line;
m_editState.nodeIdx = lm->nodeIdx; m_editState.nodeIdx = lm->nodeIdx;
m_editState.subLine = isVectorKind(lm->nodeKind) ? vecComponent : lm->subLine; m_editState.subLine = lm->subLine;
m_editState.target = target; m_editState.target = target;
m_editState.spanStart = norm.start; m_editState.spanStart = norm.start;
m_editState.original = trimmed; m_editState.original = trimmed;
m_editState.linelenAfterReplace = lineText.size(); m_editState.linelenAfterReplace = lineText.size();
m_editState.editKind = lm->nodeKind; m_editState.editKind = lm->nodeKind;
if (isVectorKind(lm->nodeKind)) if (isVectorKind(lm->nodeKind)) {
m_editState.subLine = vecComponent;
m_editState.editKind = NodeKind::Float; m_editState.editKind = NodeKind::Float;
}
if (isMatrixKind(lm->nodeKind)) {
m_editState.subLine = lm->subLine * 4 + vecComponent; // flat index 0-15
m_editState.editKind = NodeKind::Float;
}
// Store fixed comment column position for value editing // Store fixed comment column position for value editing
// Use large lineLength so commentCol is always computed (padding added dynamically) // Use large lineLength so commentCol is always computed (padding added dynamically)
@@ -2040,8 +2057,8 @@ void RcxEditor::applyHoverCursor() {
} }
auto h = hitTest(m_lastHoverPos); auto h = hitTest(m_lastHoverPos);
int line; EditTarget t; int line, hCol; EditTarget t;
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t); bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, hCol, t);
// Skip hover span on footer lines (nothing editable) // Skip hover span on footer lines (nothing editable)
int hoverLine = h.line; int hoverLine = h.line;

View File

@@ -41,7 +41,7 @@ public:
// ── Inline editing ── // ── Inline editing ──
bool isEditing() const { return m_editState.active; } bool isEditing() const { return m_editState.active; }
bool beginInlineEdit(EditTarget target, int line = -1); bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
void cancelInlineEdit(); void cancelInlineEdit();
void applySelectionOverlay(const QSet<uint64_t>& selIds); void applySelectionOverlay(const QSet<uint64_t>& selIds);

View File

@@ -1,4 +1,5 @@
#include "core.h" #include "core.h"
#include <cmath>
#include <cstring> #include <cstring>
#include <limits> #include <limits>
@@ -80,8 +81,24 @@ QString fmtUInt32(uint32_t v) { return hexVal(v); }
QString fmtUInt64(uint64_t v) { return hexVal(v); } QString fmtUInt64(uint64_t v) { return hexVal(v); }
QString fmtFloat(float v) { QString fmtFloat(float v) {
QString s = QString::number(v, 'g', 4); if (std::isnan(v)) return QStringLiteral("NaN");
if (!s.contains('.') && !s.contains('e') && !s.contains('E')) if (std::isinf(v)) return v > 0 ? QStringLiteral("inff") : QStringLiteral("-inff");
// 6 significant digits — covers full single-precision range
QString s = QString::number(v, 'g', 6);
// If 'g' chose scientific notation, reformat as plain decimal
if (s.contains('e') || s.contains('E')) {
s = QString::number(v, 'f', 8);
if (s.contains('.')) {
int i = s.size() - 1;
while (i > 0 && s[i] == '0') i--;
if (s[i] == '.') i++; // keep at least one decimal digit
s.truncate(i + 1);
}
}
if (!s.contains('.'))
s += QStringLiteral(".f"); s += QStringLiteral(".f");
else else
s += QLatin1Char('f'); s += QLatin1Char('f');
@@ -268,7 +285,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
case NodeKind::Mat4x4: { case NodeKind::Mat4x4: {
if (!display) return {}; // not editable as single value if (!display) return {}; // not editable as single value
if (subLine < 0 || subLine >= 4) return QStringLiteral("?"); if (subLine < 0 || subLine >= 4) return QStringLiteral("?");
QString line = QStringLiteral("["); QString line = QStringLiteral("row%1 [").arg(subLine);
for (int c = 0; c < 4; c++) { for (int c = 0; c < 4; c++) {
if (c > 0) line += QStringLiteral(", "); if (c > 0) line += QStringLiteral(", ");
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed(); line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();