feat/vtk-3d-view #7
|
|
@ -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.**
|
||||
|
|
|
|||
|
|
@ -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<double>(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<double>(scaleSlider_->value()));
|
||||
});
|
||||
btns->addStretch(1);
|
||||
btns->addWidget(cancel);
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -317,13 +317,15 @@ 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 (resetCamera) {
|
||||
if (is2D)
|
||||
geopro::render::applyTop2D(scene_.renderer());
|
||||
else
|
||||
|
|
@ -333,9 +335,10 @@ void VtkSceneView::render(bool is2D) {
|
|||
scene_.renderer()->ResetCamera(bounds); // 取景到数据(不含底图,否则数据缩成小点)
|
||||
else
|
||||
scene_.renderer()->ResetCamera();
|
||||
}
|
||||
scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图 → 不被"蒙版"切掉
|
||||
if (renderWindow_) renderWindow_->Render();
|
||||
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖
|
||||
if (onCameraChanged) onCameraChanged(); // 相机/数据变了 → 底图按新视锥重算覆盖
|
||||
}
|
||||
|
||||
void VtkSceneView::renderIncremental() {
|
||||
|
|
|
|||
|
|
@ -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)供切片附着 ──
|
||||
|
|
|
|||
|
|
@ -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<double>(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 随视口尺寸保持居中;
|
||||
|
|
|
|||
|
|
@ -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<Qt::CheckState>(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;
|
||||
|
|
|
|||
|
|
@ -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_);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue