diff --git a/CLAUDE.md b/CLAUDE.md index daced9b..490b18d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,6 +42,13 @@ When your changes create orphans: The test: Every changed line should trace directly to the user's request. +**Exception — Tech debt must be fixed, not deferred:** When you discover a real bug, +broken behavior, or technical debt while working (even if it predates this change and +you didn't introduce it), fix it — do not use "not introduced by this round / pre-existing" +as a reason to leave it. Surface it, then handle it. (User directive, 2026-06-25, binding.) +This overrides the "don't fix what isn't broken" bias above *for genuine defects* — it does +not license cosmetic refactors or unrequested rewrites. + ## 4. Goal-Driven Execution **Define success criteria. Loop until verified.** diff --git a/src/app/AxesSettingsPanel.cpp b/src/app/AxesSettingsPanel.cpp index 9eba26c..9d3e0ba 100644 --- a/src/app/AxesSettingsPanel.cpp +++ b/src/app/AxesSettingsPanel.cpp @@ -51,13 +51,15 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) { unit_->addItems({QStringLiteral("米"), QStringLiteral("英尺")}); v->addWidget(unit_); - // X / Y / Z 三轴(各:显示开关 + 最小值/最大值)。 + // X / Y / Z 三轴(各:显示开关 + 最小值/最大值)。与上方单位组留间距。 + v->addSpacing(space::kSm); x_ = addAxisRow(v, QStringLiteral("X轴"), {true, -500, 500}); y_ = addAxisRow(v, QStringLiteral("Y轴"), {true, -500, 500}); z_ = addAxisRow(v, QStringLiteral("Z轴"), {true, 0, 200}); - // 放大系数(=垂直夸张):滑块 1~10×(恢复重构前的拖动交互)。拖动实时改标签, - // 松手(sliderReleased)才发信号触发一次重建——VE 改的是几何,必须重建,但不在拖动中连续重建。 + // 放大系数(=垂直夸张):滑块 1~10×。仅改面板内数值/标签,点「应用」才统一生效(不再实时)。 + // 与上方坐标轴组留出间距(用户反馈:滑块离上面项目太近)。 + v->addSpacing(space::kMd); auto* scaleRow = new QHBoxLayout(); auto* scaleLbl = new QLabel(QStringLiteral("放大系数"), this); scaleLbl->setStyleSheet(QStringLiteral("border:none;")); @@ -66,12 +68,12 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) { scaleSlider_->setMinimum(1); scaleSlider_->setMaximum(10); scaleSlider_->setValue(1); + scaleSlider_->setSingleStep(1); + scaleSlider_->setPageStep(1); // 点击轨道按 1 步移动(默认 pageStep=10 → 点一下直接跳到 10 的 bug) scaleLabel_ = new QLabel(QStringLiteral("1.0×"), this); scaleLabel_->setStyleSheet(QStringLiteral("border:none;color:#888;min-width:36px;")); connect(scaleSlider_, &QSlider::valueChanged, this, [this](int v) { scaleLabel_->setText(QStringLiteral("%1.0×").arg(v)); }); - connect(scaleSlider_, &QSlider::sliderReleased, this, - [this] { emit verticalExaggerationChanged(static_cast(scaleSlider_->value())); }); scaleRow->addWidget(scaleSlider_, 1); scaleRow->addWidget(scaleLabel_); v->addLayout(scaleRow); @@ -88,7 +90,8 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) { auto rd = [](const Row& r) { return AxisRange{r.show->isChecked(), r.lo->value(), r.hi->value()}; }; - emit applied(rd(x_), rd(y_), rd(z_), unit_->currentIndex()); + emit applied(rd(x_), rd(y_), rd(z_), unit_->currentIndex(), + static_cast(scaleSlider_->value())); }); btns->addStretch(1); btns->addWidget(cancel); diff --git a/src/app/AxesSettingsPanel.hpp b/src/app/AxesSettingsPanel.hpp index eed8f3d..2e40816 100644 --- a/src/app/AxesSettingsPanel.hpp +++ b/src/app/AxesSettingsPanel.hpp @@ -23,10 +23,9 @@ public: void setValues(AxisRange x, AxisRange y, AxisRange z, int unitIdx, double scale); signals: - // 「应用」:仅坐标轴(显示方式/单位/per-axis 可见性·范围);走增量重建,不重绘数据。 - void applied(AxisRange x, AxisRange y, AxisRange z, int unitIdx); - // 放大系数(=垂直夸张)滑块:拖动实时改数值标签,松手才发此信号触发一次重建。 - void verticalExaggerationChanged(double ve); + // 「应用」一次性下发:坐标轴(显示方式/单位/per-axis 可见性·范围) + 放大系数(scale=垂直夸张)。 + // 放大系数滑块仅改面板内数值,点「应用」才统一生效(与面板其余项一致,不再实时)。 + void applied(AxisRange x, AxisRange y, AxisRange z, int unitIdx, double scale); void closed(); // × 或 取消 private: diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 425d6ba..4a277f8 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -317,25 +317,28 @@ void VtkSceneView::rebuildAxes() { } } -void VtkSceneView::render(bool is2D) { +void VtkSceneView::render(bool is2D, bool resetCamera) { // 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。 double bgR, bgG, bgB; geopro::app::vtkBackground(bgR, bgG, bgB); scene_.renderer()->SetBackground(bgR, bgG, bgB); // 坐标轴仅三维视图显示(2D 俯视测线不需要立体坐标轴)。 if (!is2D) rebuildAxes(); - if (is2D) - geopro::render::applyTop2D(scene_.renderer()); - else - geopro::render::applyFree3D(scene_.renderer()); - double bounds[6]; - if (computeDataBounds(bounds)) - scene_.renderer()->ResetCamera(bounds); // 取景到数据(不含底图,否则数据缩成小点) - else - scene_.renderer()->ResetCamera(); + // 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。 + if (resetCamera) { + if (is2D) + geopro::render::applyTop2D(scene_.renderer()); + else + geopro::render::applyFree3D(scene_.renderer()); + double bounds[6]; + if (computeDataBounds(bounds)) + scene_.renderer()->ResetCamera(bounds); // 取景到数据(不含底图,否则数据缩成小点) + else + scene_.renderer()->ResetCamera(); + } scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图 → 不被"蒙版"切掉 if (renderWindow_) renderWindow_->Render(); - if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖 + if (onCameraChanged) onCameraChanged(); // 相机/数据变了 → 底图按新视锥重算覆盖 } void VtkSceneView::renderIncremental() { diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 3794c6a..9f89fa9 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -56,7 +56,7 @@ public: void applyCameraView(geopro::controller::ViewDir dir) override; void zoom(double factor) override; void fitView() override; - void render(bool is2D) override; + void render(bool is2D, bool resetCamera = true) override; void renderIncremental() override; // ── P3 切片交互:暴露当前体素 image(含 VE 烤入的 origin/spacing)供切片附着 ── diff --git a/src/app/main.cpp b/src/app/main.cpp index dc639d8..0dd51b7 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -761,15 +761,24 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re } }); // 三维体段右键删除:切片→deleteSlice / 异常→deleteAnomaly,删后刷新树。 + // 异常删除须同时 refreshAnomalies(重载异常 actor)——否则列表行没了但场景里异常仍渲染(技术债,已修)。 QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::deleteDatasetRequested, &window, - [scene3dRepo, refreshAnalysis](const QString& dsId, const QString& ddCode) { + [scene3dRepo, refreshAnalysis, refreshAnomalies](const QString& dsId, + const QString& ddCode) { const std::string id = dsId.toStdString(); - auto ok = [refreshAnalysis]() { refreshAnalysis(); }; - auto err = [](const std::string&) {}; - if (ddCode == QStringLiteral("dd_slice")) - scene3dRepo->deleteSlice(id, ok, err); - else if (ddCode == QStringLiteral("dd_anomaly")) - scene3dRepo->deleteAnomaly(id, ok, err); + if (ddCode == QStringLiteral("dd_slice")) { + scene3dRepo->deleteSlice( + id, [refreshAnalysis]() { refreshAnalysis(); }, + [](const std::string&) {}); + } else if (ddCode == QStringLiteral("dd_anomaly")) { + scene3dRepo->deleteAnomaly( + id, + [refreshAnalysis, refreshAnomalies]() { + refreshAnalysis(); // 刷三维体段列表(异常行消失) + refreshAnomalies(); // 重载异常 actor(场景同步移除) + }, + [](const std::string&) {}); + } }); // (O点位置/字体、旧栏「生成三维体」「勾选→渲染」接线均已退役——分别由 analysisTab 的 // generateVolumeRequested / checkedDatasetsChanged 接管。) @@ -943,11 +952,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 垂直夸张:地形须与剖面用同一 VE 才对齐(都按真实高程×VE)。单一来源 kVerticalExaggeration 下发 // 控制器(上方)/底图;运行时 VE 改由坐标轴设置抽屉「应用」下发(旧三维数据集栏滑块已退役)。 basemap->setVerticalExaggeration(kVerticalExaggeration); - // 坐标轴设置抽屉「应用」:仅轴显示开关 + 单位 + per-axis 范围 → 走 setAxesConfig(已改增量重建, - // 不再清场景重绘数据)。放大系数=垂直夸张改由滑块独立信号下发(见下)。 + // 坐标轴设置抽屉「应用」:一次性下发 轴显示/单位/范围(增量重建,不重绘数据)+ 放大系数(VE)。 + // VE 改几何须重建,但走保留相机的重建(setVerticalExaggeration 内 preserveCamera)→ 当前视角直接 + // 重绘,不再先跳远视角;底图同步 VE。仅在 VE 实际变化时下发,避免无谓重建。 + auto lastVE = std::make_shared(kVerticalExaggeration); QObject::connect(axesPanel, &geopro::app::AxesSettingsPanel::applied, &window, - [axesPanel, sceneCtrl](geopro::app::AxisRange x, geopro::app::AxisRange y, - geopro::app::AxisRange z, int unitIdx) { + [axesPanel, sceneCtrl, basemap, lastVE](geopro::app::AxisRange x, + geopro::app::AxisRange y, + geopro::app::AxisRange z, int unitIdx, + double scale) { const bool anyShow = x.show || y.show || z.show; // 每轴:可见性=显示开关;自定义范围=面板 min/max(按当前单位),真正改刻度。 auto cfg = [](const geopro::app::AxisRange& a) { @@ -958,15 +971,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re unitIdx == 1 ? geopro::controller::AxesUnit::Feet : geopro::controller::AxesUnit::Meter, cfg(x), cfg(y), cfg(z)); + if (scale > 0 && scale != *lastVE) { // 放大系数变化 → 保留相机重建 + 底图同步 + *lastVE = scale; + sceneCtrl->setVerticalExaggeration(scale); + basemap->setVerticalExaggeration(scale); + } axesPanel->hide(); }); - // 放大系数滑块(=垂直夸张):松手即下发控制器 + 底图(VE 改几何须一次重建,但不在拖动中连续重建)。 - QObject::connect(axesPanel, &geopro::app::AxesSettingsPanel::verticalExaggerationChanged, &window, - [sceneCtrl, basemap](double ve) { - if (ve <= 0) return; - sceneCtrl->setVerticalExaggeration(ve); - basemap->setVerticalExaggeration(ve); - }); // ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。── // 透明背景 + 鼠标穿透(不挡 QVTK 交互);CenterOverlay 随视口尺寸保持居中; diff --git a/src/app/panels/DatasetListPanel.cpp b/src/app/panels/DatasetListPanel.cpp index fe38637..a97bd96 100644 --- a/src/app/panels/DatasetListPanel.cpp +++ b/src/app/panels/DatasetListPanel.cpp @@ -81,16 +81,13 @@ public: p->fillRect(QRect(opt.rect.left(), opt.rect.top(), 2, opt.rect.height()), geopro::app::tokenColor("accent/primary")); - // 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。 - // 容器节点(项目/GS/TM)虽不画复选框,也保留同宽复选框列——否则其子级(带复选框)相对容器的 - // 视觉缩进 = 树级缩进 + 复选框列宽,比「子带框→孙带框」的缩进大一截(用户实测 #6:三维体段 - // 体相对 TM 缩进过大)。保留同宽列后各级缩进只差一个树级,与对象树一致。 - const int box = 16; - const int checkColPad = 12 + box + 8; // 复选框列总宽(复选框 + 两侧留白) + // 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。容器节点(项目/GS/TM)不画 + // 复选框、名称紧跟展开图标(左留白小,与对象树容器一致;勿为对齐子级而预留空复选框列, + // 否则容器名与展开图标间出现大段空白,见用户 #2 反馈)。 int textLeftPad = 6; const bool checkable = (idx.flags() & Qt::ItemIsUserCheckable); - const bool isContainer = idx.data(kDsDdCodeRole).toString() == QStringLiteral("container"); if (checkable) { + const int box = 16; QRect checkRect(r.left() + 12, r.top() + (r.height() - box) / 2, box, box); const auto cs = static_cast(idx.data(Qt::CheckStateRole).toInt()); QStyleOptionViewItem o(opt); @@ -100,9 +97,7 @@ public: const QWidget* w = opt.widget; QStyle* st = w ? w->style() : QApplication::style(); st->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &o, p, w); - textLeftPad = checkColPad; // 复选框右侧留白后再放文本 - } else if (isContainer) { - textLeftPad = checkColPad; // 容器保留同宽列(不画框),使子级缩进与对象树一致 + textLeftPad = 12 + box + 8; // 复选框右侧留白后再放文本 } QString title = disp, meta; diff --git a/src/app/panels/columns/DateRangeEdit.cpp b/src/app/panels/columns/DateRangeEdit.cpp index a1acdd5..ec29508 100644 --- a/src/app/panels/columns/DateRangeEdit.cpp +++ b/src/app/panels/columns/DateRangeEdit.cpp @@ -27,9 +27,9 @@ DateRangeEdit::DateRangeEdit(QWidget* parent) : QWidget(parent) { btn_, QStringLiteral( "QToolButton{background-color:{{bg/panel}};color:{{text/primary}};" "border:1px solid {{border/default}};border-radius:4px;" - "padding:6px 24px 6px 8px;min-height:16px;text-align:left;" + "padding:6px 22px 6px 8px;min-height:16px;text-align:left;" "background-image:url(:/icons/chevron-down.svg);background-repeat:no-repeat;" - "background-position:right 8px center;}" + "background-origin:padding;background-position:right center;}" "QToolButton:hover{border-color:{{border/strong}};}")); connect(btn_, &QToolButton::clicked, this, &DateRangeEdit::openPopup); lay->addWidget(btn_); diff --git a/src/controller/I3dSceneView.hpp b/src/controller/I3dSceneView.hpp index 21dff19..53175fb 100644 --- a/src/controller/I3dSceneView.hpp +++ b/src/controller/I3dSceneView.hpp @@ -73,8 +73,9 @@ public: // 适配全览(P2):ResetCamera 并提交渲染。 virtual void fitView() = 0; - // 应用相机预设(2D 俯视 / 3D 自由)并提交渲染(全量重建用,会 ResetCamera)。 - virtual void render(bool is2D) = 0; + // 应用相机预设(2D 俯视 / 3D 自由)并提交渲染(全量重建用)。resetCamera=true 时 ResetCamera 取景到 + // 数据;false 时保留当前相机(如改放大系数全量重建但要原地重绘,不跳远视角)。 + virtual void render(bool is2D, bool resetCamera = true) = 0; // 增量提交渲染:重建坐标轴并刷新,但不动相机(勾选/取消单个 ds 时视角不跳)。 virtual void renderIncremental() = 0; }; diff --git a/src/controller/VtkSceneController.cpp b/src/controller/VtkSceneController.cpp index 957eab6..c3ba3c0 100644 --- a/src/controller/VtkSceneController.cpp +++ b/src/controller/VtkSceneController.cpp @@ -221,7 +221,12 @@ void VtkSceneController::setLayer(SceneLayer layer, bool on) { void VtkSceneController::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; + // VE 烤进帘面 SetScale / 体素 image / 地形几何,须全量重建;但保留当前相机 → 原地按新夸张重绘, + // 不先跳远视角再回(用户反馈)。重建中 fitOnArrival_ 也置 false(见 rebuildInternal), + // 异步到场数据经 renderIncremental 在当前相机下显示。 + preserveCameraOnRebuild_ = true; rebuildInternal(); + preserveCameraOnRebuild_ = false; } void VtkSceneController::rebuild() { rebuildInternal(); } @@ -292,7 +297,7 @@ void VtkSceneController::rebuildInternal() { // 坐标轴设置在 clear 后下发:render 末尾据当前场景包围盒重建坐标轴 prop。 view_.setAxes(axesMode_, axesUnit_, kAxesFontSize); view_.setAxesRanges(axisX_, axisY_, axisZ_); - fitOnArrival_ = true; // 全量重建:到场数据自动取景 + fitOnArrival_ = !preserveCameraOnRebuild_; // 保留相机重建(改VE):到场数据不自动取景,留当前视角 // 坏 dsId(loadGrid/loadColorScale 抛异常)= best-effort 跳过:emit loadFailed 但不中断。 try { @@ -322,7 +327,8 @@ void VtkSceneController::rebuildInternal() { emit loadFailed(QString::fromStdString(e.what())); } - view_.render(is2D); // 设背景/相机预设/坐标轴 + ResetCamera(数据到场再由 onDatasetArrived 取景) + // 保留相机重建(改VE):不 ResetCamera,原地按新夸张重绘。 + view_.render(is2D, /*resetCamera=*/!preserveCameraOnRebuild_); } } // namespace geopro::controller diff --git a/src/controller/VtkSceneController.hpp b/src/controller/VtkSceneController.hpp index 0e92f62..15f7571 100644 --- a/src/controller/VtkSceneController.hpp +++ b/src/controller/VtkSceneController.hpp @@ -94,6 +94,7 @@ private: bool showVoxel_ = false; bool showTerrain_ = false; double verticalExaggeration_ = 1.0; + bool preserveCameraOnRebuild_ = false; // 改放大系数等:全量重建但保留当前相机(不跳远视角) // 坐标轴设置(P2):默认标准 + 米;字号固定 12(字体设置待 1.0 确认)。 AxesMode axesMode_ = AxesMode::Standard;