Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
14 kB · 457 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458#include "SplitTree.h"
// Split-tree node implementation: traversal + mutation + JSON io. The JSON// shapes mirror SplitContainer.serialize() / Shell.buildFromSession()// (axis strings "v"/"h", splits ratio|-1, single-child containers// collapse on load).
#include <QJsonArray>
#include <algorithm>#include <cmath>#include <limits>#include <ranges>#include <utility>
// --- SplitNode -------------------------------------------------------------
SplitNode::SplitNode(QObject* parent) : QObject(parent) {}
SplitNode::~SplitNode() = default;
SplitContainerNode* SplitNode::asContainer(){ // Guarded by kind(): safe by construction. if (kind() != NodeKind::Container) { return nullptr; } return static_cast<SplitContainerNode*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)}
const SplitContainerNode* SplitNode::asContainer() const{ if (kind() != NodeKind::Container) { return nullptr; } return static_cast<const SplitContainerNode*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)}
PaneNode* SplitNode::asPane(){ if (kind() != NodeKind::Pane) { return nullptr; } return static_cast<PaneNode*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)}
const PaneNode* SplitNode::asPane() const{ if (kind() != NodeKind::Pane) { return nullptr; } return static_cast<const PaneNode*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)}
// --- PaneNode --------------------------------------------------------------
PaneNode::PaneNode(QString paneId, QObject* parent, PaneKind kind) : SplitNode(parent), m_paneId(std::move(paneId)), m_kind(kind){}
PaneNode::~PaneNode() = default;
void PaneNode::setPaneKind(PaneKind kind){ if (m_kind == kind) { return; } m_kind = kind; emit paneKindChanged();}
// --- SplitContainerNode ------------------------------------------------------
SplitContainerNode::SplitContainerNode(SplitAxis axis, QString containerId, QObject* parent) : SplitNode(parent), m_axis(axis), m_containerId(std::move(containerId)){}
SplitContainerNode::~SplitContainerNode() = default;
int SplitContainerNode::axis() const{ return m_axis == SplitAxis::Horizontal ? 0 : 1;}
void SplitContainerNode::setAxis(SplitAxis axis){ if (m_axis == axis) { return; } m_axis = axis; emit axisChanged();}
SplitNode* SplitContainerNode::childAt(int index) const{ return m_children.at(static_cast<qsizetype>(index));}
qreal SplitContainerNode::ratioAt(int index) const{ return m_ratios.at(static_cast<qsizetype>(index));}
void SplitContainerNode::setRatioAt(int index, qreal ratio){ if (m_ratios.at(static_cast<qsizetype>(index)) == ratio) { return; } m_ratios[static_cast<qsizetype>(index)] = ratio; emit ratiosChanged();}
void SplitContainerNode::setRatios(QList<qreal> ratios){ if (m_ratios == ratios) { return; } m_ratios = std::move(ratios); emit ratiosChanged();}
void SplitContainerNode::insertChild(int index, SplitNode* child, qreal ratio){ const auto clamped = std::clamp(index, 0, static_cast<int>(m_children.size())); const auto childPos = m_children.begin() + static_cast<qsizetype>(clamped); const auto ratioPos = m_ratios.begin() + static_cast<qsizetype>(clamped); m_children.insert(childPos, child); m_ratios.insert(ratioPos, ratio); child->setParent(this); emit childrenChanged(); emit ratiosChanged();}
void SplitContainerNode::appendChild(SplitNode* child, qreal ratio){ insertChild(static_cast<int>(m_children.size()), child, ratio);}
SplitNode* SplitContainerNode::takeChild(int index){ const auto childIt = m_children.begin() + static_cast<qsizetype>(index); const auto ratioIt = m_ratios.begin() + static_cast<qsizetype>(index); SplitNode* child = *childIt; m_children.erase(childIt); m_ratios.erase(ratioIt); if (child->parent() == this) { child->setParent(nullptr); } emit childrenChanged(); emit ratiosChanged(); return child;}
// --- traversal / helpers -----------------------------------------------------
SplitContainerNode* findContainer(SplitNode& root, const QString& containerId){ // The traversal is read-only; shedding the const on the result keeps // one implementation for both lookup flavors. const SplitContainerNode* found = nullptr; walk(root, [&](const auto& typed) { using T = std::decay_t<decltype(typed)>; if constexpr (std::is_same_v<T, SplitContainerNode>) { if (typed.containerId() == containerId) { found = &typed; } } }); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) return const_cast<SplitContainerNode*>(found);}
const SplitContainerNode* findContainer(const SplitNode& root, const QString& containerId){ const SplitContainerNode* found = nullptr; walk(root, [&](const auto& typed) { using T = std::decay_t<decltype(typed)>; if constexpr (std::is_same_v<T, SplitContainerNode>) { if (typed.containerId() == containerId) { found = &typed; } } }); return found;}
QStringList paneIdsInLayoutOrder(const SplitNode& root){ QStringList ids; walk(root, [&](const auto& typed) { using T = std::decay_t<decltype(typed)>; if constexpr (std::is_same_v<T, PaneNode>) { if (!typed.paneId().isEmpty()) { ids.append(typed.paneId()); } } }); return ids;}
// --- normalized pane-rect geometry -------------------------------------------
qreal coalescedShare(const SplitContainerNode& container, const int index){ const qreal r = container.ratioAt(index); if (r > 0) { return r; } return 1.0 / static_cast<qreal>(container.childCount());}
namespace {
void walkRects(const SplitNode& node, const qreal x, const qreal y, const qreal w, const qreal h, QList<PaneRect>& out){ if (const auto* const pane = node.asPane()) { out.append({.paneId = pane->paneId(), .x = x, .y = y, .w = w, .h = h}); return; } const auto* const container = node.asContainer(); if (container == nullptr || container->empty()) { return; } const auto shares = std::views::iota(0, container->childCount()) | std::views::transform( [&](const int i) { return coalescedShare(*container, i); }); const qreal sum = std::ranges::fold_left(shares, 0.0, std::plus{}); if (sum <= 0) { return; } const bool horizontal = container->axis() == 0; qreal cx = x; qreal cy = y; for (const int i : std::views::iota(0, container->childCount())) { const SplitNode* child = container->childAt(i); const qreal share = coalescedShare(*container, i) / sum; const qreal sw = horizontal ? share * w : w; const qreal sh = horizontal ? h : share * h; if (child != nullptr) { walkRects(*child, cx, cy, sw, sh, out); } if (horizontal) { cx += sw; } else { cy += sh; } }}
} // namespace
QList<PaneRect> paneRectsNormalized(const SplitNode& root){ QList<PaneRect> out; walkRects(root, 0, 0, 1, 1, out); return out;}
QString spatialNeighborPaneId(const SplitNode& root, const QString& fromPaneId, const QString& direction){ if (fromPaneId.isEmpty()) { return {}; } const QList<PaneRect> rects = paneRectsNormalized(root); const auto sourceIt = std::ranges::find_if( rects, [&](const PaneRect& rect) { return rect.paneId == fromPaneId; }); if (sourceIt == rects.end()) { return {}; } const qreal sourceX = sourceIt->x + (sourceIt->w / 2.0); const qreal sourceY = sourceIt->y + (sourceIt->h / 2.0); // Parallel offset floor in normalized units: the old pixel-space epsilon // (5 px on a ~1000 px pane) is about 0.005; 0.01 keeps the same meaning. constexpr qreal kDirectionEpsilon = 0.01; QString bestId; qreal bestScore = std::numeric_limits<qreal>::max(); for (const PaneRect& rect : rects) { if (rect.paneId == fromPaneId) { continue; } const qreal dx = (rect.x + (rect.w / 2.0)) - sourceX; const qreal dy = (rect.y + (rect.h / 2.0)) - sourceY; bool inDirection = false; qreal parallel = 0.0; qreal perpendicular = 0.0; if (direction == QLatin1String("right") && dx > kDirectionEpsilon) { inDirection = true; parallel = dx; perpendicular = std::abs(dy); } else if (direction == QLatin1String("left") && dx < -kDirectionEpsilon) { inDirection = true; parallel = -dx; perpendicular = std::abs(dy); } else if (direction == QLatin1String("down") && dy > kDirectionEpsilon) { inDirection = true; parallel = dy; perpendicular = std::abs(dx); } else if (direction == QLatin1String("up") && dy < -kDirectionEpsilon) { inDirection = true; parallel = -dy; perpendicular = std::abs(dx); } if (!inDirection) { continue; } const qreal score = (perpendicular * 3.0) + parallel; if (score < bestScore) { bestScore = score; bestId = rect.paneId; } } return bestId;}
void renormalize(SplitContainerNode& container){ qreal sum = 0.0; for (const qreal ratio : container.ratios()) { if (ratio < 0.0) { return; // an unmeasured sibling: QML coalesces, leave the rest alone } sum += ratio; } if (sum <= 0.0) { return; } QList<qreal> normalized; normalized.reserve(container.ratios().size()); for (const qreal ratio : container.ratios()) { normalized.append(ratio / sum); } container.setRatios(normalized);}
void equalizeSplits(SplitNode& root){ auto* container = root.asContainer(); if (container == nullptr) { return; } const auto count = container->childCount(); QList<qreal> even; even.reserve(count); for (int i = 0; i < count; ++i) { Q_UNUSED(i); even.append(1.0 / static_cast<qreal>(count)); } container->setRatios(even); for (SplitNode* child : container->children()) { equalizeSplits(*child); }}
// --- JSON ---------------------------------------------------------------------
QJsonObject toJson(const SplitNode& node){ QJsonObject object; if (const auto* pane = node.asPane()) { object.insert(QStringLiteral("type"), QStringLiteral("leaf")); object.insert(QStringLiteral("paneId"), pane->paneId()); object.insert(QStringLiteral("paneKind"), paneKindToJson(pane->paneKind())); return object; } const auto& container = *node.asContainer(); object.insert(QStringLiteral("type"), QStringLiteral("node")); object.insert(QStringLiteral("axis"), splitAxisToJson( static_cast<SplitAxis>(container.axis() == 0 ? SplitAxis::Horizontal : SplitAxis::Vertical))); // Additive id field (old readers ignore it). object.insert(QStringLiteral("containerId"), container.containerId()); QJsonArray children; QJsonArray splits; for (auto&& [ratio, child] : std::views::zip(container.ratios(), container)) { splits.append(ratio >= 0.0 ? QJsonValue(ratio) : QJsonValue(-1)); children.append(toJson(*child)); } object.insert(QStringLiteral("children"), children); object.insert(QStringLiteral("splits"), splits); return object;}
std::unique_ptr<SplitNode> fromJson(const QJsonObject& object){ const QString type = object.value(QLatin1String("type")).toString(); if (type == QLatin1String("leaf")) { QString paneId = object.value(QLatin1String("paneId")).toString(); if (paneId.isEmpty()) { paneId = newUuid(); // legacy files carry no pane ids } PaneKind kind = paneKindFromJson(object.value(QLatin1String("paneKind")).toString()); // Invariant: an editor pane has a file. A leaf that names no file is a // chooser, so a hand-edited or truncated session cannot open an empty // editor. if (kind == PaneKind::Editor && object.value(QLatin1String("editorPath")).toString().isEmpty()) { kind = PaneKind::Picker; } return std::make_unique<PaneNode>(std::move(paneId), nullptr, kind); } if (type != QLatin1String("node")) { // buildFromSession substitutes an empty leaf for unknown trees; the // caller (AppModel::loadSession) does the same on nullopt. return nullptr; } const SplitAxis axis = splitAxisFromJson(object.value(QLatin1String("axis")).toString()); QString containerId = object.value(QLatin1String("containerId")).toString(); if (containerId.isEmpty()) { containerId = newUuid(); } auto container = std::make_unique<SplitContainerNode>(axis, containerId, nullptr); const QJsonArray children = object.value(QStringLiteral("children")).toArray(); const QJsonArray splits = object.value(QStringLiteral("splits")).toArray(); for (const auto& childValue : children) { if (!childValue.isObject()) { continue; } auto child = fromJson(childValue.toObject()); if (child == nullptr) { continue; } const int index = container->childCount(); const QJsonValue want = splits.at(index); const qreal ratio = want.isDouble() && want.toDouble() > 0.0 ? want.toDouble() : -1.0; container->appendChild(child.release(), ratio); } if (container->childCount() == 0) { // A childless node restores as a single empty leaf (the // emptyLeaf(rws.path) tolerance in buildFromSession). return std::make_unique<PaneNode>(newUuid(), nullptr); } // A single-child container from a legacy file collapses into its sole // child (the >=2-children invariant). takeChild clears the QObject // parent, so the sole child is returned ownerless and unique_ptr keeps it // alive. if (container->childCount() == 1) { return std::unique_ptr<SplitNode>(container->takeChild(0)); } return std::unique_ptr<SplitNode>(container.release());}