diff --git a/app/render/EdgeSelectorHighlight.cpp b/app/render/EdgeSelectorHighlight.cpp index b0706cf2ed644804c56e31c75a2f974332254155..346fef357e52f6e08d76d8215e648fcdc3104f73 100644 --- a/app/render/EdgeSelectorHighlight.cpp +++ b/app/render/EdgeSelectorHighlight.cpp @@ -1,16 +1,24 @@ +#include "MeshAreaPick.h" #include "CoincidentTopology.h" #include "MeshActor.h" #include "MeshActorSelectOp.h" #include "MeshIdQuery.h" #include "Selection.h" #include "SelectorHighlight.h" +#include #include +#include #include +#include +#include +#include #include +#include #include #include #include #include +#include #include #include @@ -128,16 +136,19 @@ void EdgeSelectorHighlight::select(double posx, double posy) picker->AddPickList(&select_op_.getSolidActor()); picker->Pick(posx, posy, 0, renderer_); - // 获取选中的CellId (面或者是边) - vtkIdType picked_cell_id = picker->GetCellId(); - if (picked_cell_id == -1) { // 没选到 + select(posx, posy, picker.GetPointer(), picker->GetActor(), + picker->GetCellId(), picker->GetPointId()); +} + +void EdgeSelectorHighlight::select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType /*picked_point_id*/) +{ + if (!picked_actor || picked_cell_id == -1) { clear(); return; } - // 获取选中的 cell - vtkActor* picked_actor = picker->GetActor(); - assert(picked_actor); vtkPolyDataMapper* picked_mapper = vtkPolyDataMapper::SafeDownCast(picked_actor->GetMapper()); assert(picked_mapper); vtkPolyData* picked_poly = picked_mapper->GetInput(); @@ -181,3 +192,155 @@ void EdgeSelectorHighlight::setupHighlightStyle(vtkActor& actor, vtkMapper& mapp prop->SetLineWidth(5); actor.SetProperty(prop); } + +void EdgeSelectorHighlight::selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) +{ + // 与点选对齐:框选同样从 face/edge/solid 三个 actor 取边,逐 actor 走 CELLS 拾取后合并端点对。 + // face CELLS 主路径健壮(面表面有 z 遮挡);edge actor 补独立/物化边;solid 表面补体网格表面边。 + // 每个 actor 拾取时其余 actor 作 keepVisible 填 z-buffer(非穿透),结果按端点对去重合并。 + std::set> picked_edge_set; + + // 对单个 actor 执行 CELLS 框选并把命中 cell 派生为"局部点 id 端点对" + auto pick_edges = [&](vtkActor* target, vtkPolyData* poly, + const std::vector& keep_visible) { + if (!target || !poly || poly->GetNumberOfCells() == 0) + return; + const std::vector* pKeep = keep_visible.empty() ? nullptr : &keep_visible; + auto picked = area_pick::executeAreaPickWithGuard(renderer_, target, + xmin, ymin, xmax, ymax, vtkDataObject::FIELD_ASSOCIATION_CELLS, pKeep); + if (picked.empty()) + return; + + // 裁剪链上点 id 会重排:vTK 拾取后经 vtkOriginalPointIds 还原组件局部点 id + auto* orig_pt_ids = vtkIdTypeArray::SafeDownCast( + poly->GetPointData()->GetArray("vtkOriginalPointIds")); + for (vtkIdType cell_id : picked) { + if (cell_id < 0 || cell_id >= poly->GetNumberOfCells()) + continue; + vtkCell* cell = poly->GetCell(cell_id); + if (!cell) + continue; + // 线 cell 本身就是边;面 cell 经 GetEdge 拆边 + const bool is_line = cell->GetCellType() == VTK_LINE; + for (int e = 0; e < (is_line ? 1 : cell->GetNumberOfEdges()); ++e) { + vtkIdList* pt_ids = nullptr; + if (is_line) { + pt_ids = cell->GetPointIds(); + } else { + vtkCell* edge_cell = cell->GetEdge(e); + if (!edge_cell) + continue; + pt_ids = edge_cell->GetPointIds(); + } + if (!pt_ids || pt_ids->GetNumberOfIds() < 2) + continue; + const vtkIdType rp0 = pt_ids->GetId(0); + const vtkIdType rp1 = pt_ids->GetId(1); + double pp0[3], pp1[3]; + poly->GetPoint(rp0, pp0); + poly->GetPoint(rp1, pp1); + // 线段与框相交即选中(端点落入或穿过矩形均算),无需框到端点 + if (!area_pick::isScreenSegmentIntersectsBox(renderer_, target, + pp0[0], pp0[1], pp0[2], pp1[0], pp1[1], pp1[2], + xmin, ymin, xmax, ymax)) + continue; + vtkIdType p0 = rp0, p1 = rp1; + if (orig_pt_ids) { + if (rp0 < 0 || rp0 >= orig_pt_ids->GetNumberOfTuples() + || rp1 < 0 || rp1 >= orig_pt_ids->GetNumberOfTuples()) + continue; + p0 = orig_pt_ids->GetValue(rp0); + p1 = orig_pt_ids->GetValue(rp1); + } + if (p0 < 0 || p1 < 0) + continue; + picked_edge_set.insert({ std::min(p0, p1), std::max(p0, p1) }); + } + } + }; + + // 1) face 表面(主路径) + auto* face_actor = vtkActor::SafeDownCast(&select_op_.getFaceActor()); + if (auto* face_poly = face_actor ? vtkPolyData::SafeDownCast( + vtkPolyDataMapper::SafeDownCast(face_actor->GetMapper())->GetInput()) : nullptr) { + std::vector keep; + if (auto* solid_a = vtkActor::SafeDownCast(&select_op_.getSolidActor())) + if (solid_a != face_actor) + keep.push_back(solid_a); + pick_edges(face_actor, face_poly, keep); + } + + // 2) edge actor:独立/物化边(线 cell) + auto* edge_actor = vtkActor::SafeDownCast(&select_op_.getEdgeActor()); + if (auto* edge_poly = edge_actor ? vtkPolyData::SafeDownCast( + vtkPolyDataMapper::SafeDownCast(edge_actor->GetMapper())->GetInput()) : nullptr) { + if (edge_poly->GetNumberOfLines() > 0) { + std::vector keep; + if (auto* face_a = vtkActor::SafeDownCast(&select_op_.getFaceActor())) + if (face_a != edge_actor) + keep.push_back(face_a); + if (auto* solid_a = vtkActor::SafeDownCast(&select_op_.getSolidActor())) + if (solid_a != edge_actor) + keep.push_back(solid_a); + pick_edges(edge_actor, edge_poly, keep); + } + } + + // 3) solid 表面:体网格表面边(face 无 cell 时是主要来源) + auto* solid_actor = vtkActor::SafeDownCast(&select_op_.getSolidActor()); + if (auto* solid_poly = solid_actor ? vtkPolyData::SafeDownCast( + vtkPolyDataMapper::SafeDownCast(solid_actor->GetMapper())->GetInput()) : nullptr) { + pick_edges(solid_actor, solid_poly, {}); + } + + spdlog::debug("[EdgeArea] picked_edge_set.size()={}", picked_edge_set.size()); + if (picked_edge_set.empty()) + return; + + // 端点对 -> 稳定局部 edge id(与点选路径一致) + std::vector picked_edges; + picked_edges.reserve(picked_edge_set.size()); + for (const auto& key : picked_edge_set) { + Index edge_id = -1; + if (id_query_) { + auto resolved = id_query_->findEdgeByEndpoints(component_id_, + static_cast(key.first), static_cast(key.second)); + if (resolved) + edge_id = *resolved; + } + picked_edges.push_back({ { key.first, key.second }, edge_id }); + } + + // 修饰键驱动 add/remove/toggle(与点选"端点对相同即同一条边"语义一致) + auto match_selected = [&](const SelectedEdge& e) { + return std::find_if(selections_.begin(), selections_.end(), + [&](const SelectedEdge& s) { + return _is_selected(e.endpoints, std::optional>(s.endpoints)); + }); + }; + + if (remove_only) { + for (const auto& e : picked_edges) { + auto it = match_selected(e); + if (it != selections_.end()) + selections_.erase(it); + } + } else if (add_only) { + for (const auto& e : picked_edges) { + auto it = match_selected(e); + if (it == selections_.end()) + selections_.push_back(e); + } + } else { + for (const auto& e : picked_edges) { + auto it = match_selected(e); + if (it != selections_.end()) + selections_.erase(it); + else + selections_.push_back(e); + } + } + + enableHighlight(); +} diff --git a/app/render/FaceSelectorHighlight.cpp b/app/render/FaceSelectorHighlight.cpp index 6b1e8da6d21618584acb001a1541000cc28f5adb..0e3122915279217c4512d83373d0a698f5328517 100644 --- a/app/render/FaceSelectorHighlight.cpp +++ b/app/render/FaceSelectorHighlight.cpp @@ -1,3 +1,4 @@ +#include "MeshAreaPick.h" #include "CoincidentTopology.h" #include "MeshActorSelectOp.h" #include "Selection.h" @@ -14,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -291,20 +293,25 @@ void FaceSelectorHighlight::updateSpreadCache(vtkPolyData& poly) void FaceSelectorHighlight::select(double posx, double posy) { + // 兼容路径:自行构建 picker 并拾取;生产路径由 MeshSelectManager 预拾后调下方的 picker 重载。 vtkNew picker; picker->PickFromListOn(); picker->AddPickList(&select_op_.getFaceActor()); picker->Pick(posx, posy, 0, renderer_); - vtkIdType picked_cell_id = picker->GetCellId(); - if (picked_cell_id == -1) { + select(posx, posy, picker.GetPointer(), picker->GetActor(), + picker->GetCellId(), picker->GetPointId()); +} + +void FaceSelectorHighlight::select(double posx, double posy, + vtkHardwarePicker* /*picker*/, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType /*picked_point_id*/) +{ + if (!picked_actor || picked_cell_id == -1) { clear(); return; } - // 获取选中的 cell - vtkActor* picked_actor = picker->GetActor(); - assert(picked_actor); vtkPolyDataMapper* picked_mapper = vtkPolyDataMapper::SafeDownCast(picked_actor->GetMapper()); assert(picked_mapper); vtkPolyData* picked_poly = picked_mapper->GetInput(); @@ -344,3 +351,39 @@ void FaceSelectorHighlight::setupHighlightStyle(vtkActor& actor, vtkMapper& mapp prop->SetEdgeColor(1.0, 0.0, 0.0); actor.SetProperty(prop); } + +void FaceSelectorHighlight::selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) +{ + // face_actor 是面 primitive:自遮挡足够(face cell 覆盖 viewport),keepVisible=nullptr + vtkActor* target = vtkActor::SafeDownCast(&select_op_.getFaceActor()); + if (!target) + return; + + auto picked = area_pick::executeAreaPickWithGuard(renderer_, target, + xmin, ymin, xmax, ymax, vtkDataObject::FIELD_ASSOCIATION_CELLS, nullptr); + + spdlog::debug("[FaceArea] picked.size()={}", picked.size()); + if (picked.empty()) + return; + + // 与 select 路径对齐:selections_ 存 picker 报的 render cell id(无 clip 时 = 原 face id; + // 有 clip 时为 face_clipper_->output 上的 cell 索引,PrecessFaceIds 数组跟着裁剪链透传) + std::unordered_set cur(selections_.begin(), selections_.end()); + if (remove_only) { + for (auto id : picked) + cur.erase(id); + } else if (add_only) { + for (auto id : picked) + cur.insert(id); + } else { + for (auto id : picked) { + if (cur.count(id)) + cur.erase(id); + else + cur.insert(id); + } + } + selections_.assign(cur.begin(), cur.end()); + enableHighlight(); +} diff --git a/app/render/MeshActor.cpp b/app/render/MeshActor.cpp index a3c53aa5d1bad7910d0b4b235165d2a90f1d4db0..38a9ccf67a31d14853a703fb60e1d0e86c38f489 100644 --- a/app/render/MeshActor.cpp +++ b/app/render/MeshActor.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -108,6 +109,9 @@ MeshActor::MeshActor(vtkRenderer* renderer) this->renderer_->AddActor(this->edge_actor_); this->renderer_->AddActor(this->glyph3D_actor_); + // edge_actor:框选 CELLS 模式依赖 actor 实际把线段渲染到 FBO,picker 才能读到 cell id。 + // vtkProperty 默认 Representation=SURFACE 会忽略 Lines,因此显式设为 Wireframe。 + this->edge_actor_->GetProperty()->SetRepresentationToWireframe(); this->edge_actor_->GetProperty()->SetLineWidth(2); this->solid_actor_->SetMapper(solid_mapper_); @@ -204,6 +208,29 @@ void MeshActor::loadModelData(const MeshDataVtk& model_data) edge_poly->SetLines(edge_cells); edge_poly->GetPointData()->AddArray(original_point_ids_.GetPointer()); + + // 模型未物化边(vtk_edge_cells_ 为空)时,从 face 派生 boundary edges, + // 让 edge picker 至少有可拾取的数据。否则整片 Edge 模式失效。 + if (edge_poly->GetNumberOfLines() == 0 && face_poly->GetNumberOfPolys() > 0) { + spdlog::warn("[MeshActor] edge_vertices_ empty, deriving boundary edges from face data " + "(component_id={})", this->model_data_->component_id); + vtkNew derived; + derived->SetPoints(points_); + derived->SetPolys(face_poly->GetPolys()); + derived->GetPointData()->ShallowCopy(face_poly->GetPointData()); + + vtkNew feat; + feat->SetInputData(derived); + feat->BoundaryEdgesOn(); + feat->FeatureEdgesOff(); + feat->NonManifoldEdgesOff(); + feat->ManifoldEdgesOff(); + feat->Update(); + + auto* out = feat->GetOutput(); + edge_poly->SetLines(out->GetLines()); + edge_poly->GetPointData()->ShallowCopy(out->GetPointData()); + } } // solid data diff --git a/app/render/MeshActorManager.cpp b/app/render/MeshActorManager.cpp index 1f35a9888c38d5949fd2379be011e3807e82f00e..975af88d62a6a93308961553e9bc594fb608aefc 100644 --- a/app/render/MeshActorManager.cpp +++ b/app/render/MeshActorManager.cpp @@ -34,6 +34,15 @@ std::shared_ptr MeshActorManager::getComponentActor(Index component_i } } +std::vector MeshActorManager::getAllComponentIds() const +{ + std::vector ids; + ids.reserve(component_actors_.size()); + for (const auto& [id, actor] : component_actors_) + ids.push_back(id); + return ids; +} + void MeshActorManager::deleteComponent(Index component_id) { auto it = component_actors_.find(component_id); diff --git a/app/render/MeshActorManager.h b/app/render/MeshActorManager.h index ac44352a20349d3b0e4bbc930147a84162d9ba03..10db79a4d838d37e20388388d9439b5db6a0d562 100644 --- a/app/render/MeshActorManager.h +++ b/app/render/MeshActorManager.h @@ -4,6 +4,7 @@ #include "MeshActor.h" #include "MeshActorManagerSelectOp.h" #include +#include #include class vtkRenderer; @@ -14,6 +15,8 @@ public: void bindRender(vtkRenderer* renderer); bool hasComponent(Index component_id) const; std::shared_ptr getComponentActor(Index component_id) const; + //! @brief 列出所有已加载的网格组件 id(权威注册表 component_actors_) + std::vector getAllComponentIds() const; void deleteComponent(Index component_id); void loadMesh(Index component_id, const MeshDataVtk& model_data, vtkRenderer* renderer); diff --git a/app/render/MeshActorManagerSelectOp.cpp b/app/render/MeshActorManagerSelectOp.cpp index 85c17e54fd61c314a6e9c8f66d221cdbfe715e34..f821b28c848a2bfef79088aa07d2412e79c0ee7b 100644 --- a/app/render/MeshActorManagerSelectOp.cpp +++ b/app/render/MeshActorManagerSelectOp.cpp @@ -40,6 +40,12 @@ std::optional MeshActorManagerSelectOp::getSelectOp(Index com return MeshActorSelectOp(actor); } +std::vector MeshActorManagerSelectOp::getAllComponentIds() const +{ + // 组件枚举走 manager 的权威注册表 component_actors_,不从 prop_to_component_ 反向索引推导 + return manager_->getAllComponentIds(); +} + void MeshActorManagerSelectOp::registerProps(Index component_id, std::shared_ptr actor) { MeshActorSelectOp op(actor); diff --git a/app/render/MeshActorManagerSelectOp.h b/app/render/MeshActorManagerSelectOp.h index cbf121dd60bfab28a7ca393322aaa344ab971db4..e52aebca639c5e4059d368586d478b7d642da6ce 100644 --- a/app/render/MeshActorManagerSelectOp.h +++ b/app/render/MeshActorManagerSelectOp.h @@ -23,6 +23,8 @@ public: void observePickList(vtkPropCollection* pick_list); void unobservePickList(vtkPropCollection* pick_list); std::optional getSelectOp(Index component_id) const; + //! @brief 列出当前已注册的所有组件 id,供框选遍历用 + std::vector getAllComponentIds() const; void registerProps(Index component_id, std::shared_ptr actor); void unregisterProps(std::shared_ptr actor); diff --git a/app/render/MeshAreaPick.h b/app/render/MeshAreaPick.h new file mode 100644 index 0000000000000000000000000000000000000000..4798fcf9f514e042ae725a2e3fafed2027f21e8f --- /dev/null +++ b/app/render/MeshAreaPick.h @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/** + * @file MeshAreaPick.h + * @brief 网格框选的拾取与隔离工具(header-only) + * + * 设计原则(来自 D:\PreCessDemo\AreaPickDemo.cpp,**逐行移植**,不"优化"): + * - 调 vtkHardwareSelector 选目标 actor 自身,避免穿透到其它 actor。 + * - 通过临时 visibility 开关把场景里其它 actor 隔离掉;keepVisible 列表内的 actor + * 保持可见并填充 z-buffer,使 POINTS/EDGES 模式不会拾到背后的面/体。 + * - 返回 render id(局部 cell/point id),由各 selector 自行反查为模型 id。 + * + * 与 demo 的差异(PreCess-specific): + * - 加 PickingOpacityGuard:透明模式下临时把 target + keepVisible 的 opacity 置 1.0, + * 避免半透明 actor 因深度写入关闭导致 picker 误命中背面元素。 + * - 函数内不修改 actor 的 visibility 状态以外的内容,调用方负责 RAII 围一圈。 + */ +#ifndef MESH_AREA_PICK_H +#define MESH_AREA_PICK_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace area_pick { + +//! @brief 临时把目标 actor 与 keepVisible 列表内 actor 的 opacity 强制设为 1.0, +//! 出作用域自动还原。PreCess 的 Transparent25/50/75 模式下 actor 半透明, +//! VTK 默认对半透明 actor 关闭深度写入,导致 picker 拾到背面元素。 +//! 用 RAII 围一圈 executeAreaPick 即可修复。 +class PickingOpacityGuard { +public: + PickingOpacityGuard(vtkActor* target, const std::vector* keep_visible) + { + if (target) + saveAndSet(target); + if (keep_visible) { + for (auto* a : *keep_visible) + saveAndSet(a); + } + } + ~PickingOpacityGuard() + { + for (auto& [actor, opacity] : saved_) { + actor->GetProperty()->SetOpacity(opacity); + } + } + + PickingOpacityGuard(const PickingOpacityGuard&) = delete; + PickingOpacityGuard& operator=(const PickingOpacityGuard&) = delete; + +private: + void saveAndSet(vtkActor* actor) + { + if (!actor) + return; + saved_.emplace_back(actor, actor->GetProperty()->GetOpacity()); + actor->GetProperty()->SetOpacity(1.0); + } + + std::vector> saved_; +}; + +//! @brief 在指定屏幕矩形内对 target actor 执行硬件拾取 +//! @param renderer 渲染器 +//! @param target_actor 拾取目标 actor(其它 actor 临时隐藏,仅 keep_visible 列表中的保持可见) +//! @param xmin ymin xmax ymax 屏幕像素坐标矩形(一般由 interactor 给出) +//! @param field_association vtkDataObject::FIELD_ASSOCIATION_POINTS / _CELLS +//! @param keep_visible 可选:保持可见用于填充 z-buffer 的 actor 列表(POINTS/EDGES 模式填 face/solid) +//! @return 命中单元 id 集合(render id,与 target_actor 的 cell/point id 空间一致) +inline std::set executeAreaPick( + vtkRenderer* renderer, + vtkActor* target_actor, + int xmin, int ymin, int xmax, int ymax, + int field_association, + const std::vector* keep_visible = nullptr) +{ + std::set result; + if (!renderer || !target_actor) + return result; + + spdlog::debug("[AreaPick] target={} rect=({},{},{},{}) fieldAssoc={} keepVis={} target.vis={} target.repr={}", + target_actor->GetClassName(), xmin, ymin, xmax, ymax, + field_association, keep_visible ? keep_visible->size() : 0, + target_actor->GetVisibility(), + target_actor->GetProperty()->GetRepresentationAsString()); + + // 诊断 target actor 的 mapper 数据:pick 行为依赖 mapper 实际写 cell/point id 到 FBO + if (auto* mapper = target_actor->GetMapper()) { + if (auto* poly = vtkPolyData::SafeDownCast(mapper->GetInput())) { + spdlog::debug("[AreaPick] target.mapper input: points={} verts={} lines={} polys={} strips={}", + poly->GetNumberOfPoints(), poly->GetNumberOfVerts(), + poly->GetNumberOfLines(), poly->GetNumberOfPolys(), + poly->GetNumberOfStrips()); + } else { + spdlog::debug("[AreaPick] target.mapper input is not vtkPolyData"); + } + } + + // 1) 临时隔离:除 target + keepVisible 外的 actor 全部 VisibilityOff; + // vtkActor2D(如橡皮筋)也临时 VisibilityOff——picker.FBO 不应拾 2D props + vtkPropCollection* props = renderer->GetViewProps(); + std::vector> saved_visibility; + std::vector> saved_actor2d_vis; + int saved_target_vis = target_actor->GetVisibility(); + target_actor->VisibilityOn(); + for (int i = 0; i < props->GetNumberOfItems(); ++i) { + vtkObject* obj = props->GetItemAsObject(i); + vtkProp* prop = vtkProp::SafeDownCast(obj); + if (!prop || prop == target_actor) + continue; + // 2D props(如橡皮筋 actor2D)单独保存 visibility 后 VisibilityOff + if (vtkActor2D::SafeDownCast(prop)) { + saved_actor2d_vis.emplace_back(prop, prop->GetVisibility()); + prop->VisibilityOff(); + continue; + } + auto* actor = vtkActor::SafeDownCast(prop); + if (!actor) + continue; + if (keep_visible + && std::find(keep_visible->begin(), keep_visible->end(), actor) != keep_visible->end()) + continue; // keepVisible:用于填充 z-buffer,保持可见 + saved_visibility.emplace_back(actor, actor->GetVisibility()); + actor->VisibilityOff(); + } + + // 2) HardwareSelector 拾取 + vtkNew shared_selector; + shared_selector->SetRenderer(renderer); + shared_selector->SetArea(xmin, ymin, xmax, ymax); + shared_selector->SetFieldAssociation(field_association); + vtkSelection* sel = shared_selector->Select(); + + // 3) 恢复 visibility + target_actor->SetVisibility(saved_target_vis); + for (auto& [prop, vis] : saved_visibility) { + prop->SetVisibility(vis); + } + for (auto& [prop, vis] : saved_actor2d_vis) { + prop->SetVisibility(vis); + } + + if (!sel) { + spdlog::warn("[AreaPick] shared_selector->Select() returned null"); + return result; + } + + spdlog::debug("[AreaPick] sel nodes={} props_in_renderer={}", + sel->GetNumberOfNodes(), + renderer->GetViewProps()->GetNumberOfItems()); + + // 4) 按 PROP 过滤:只保留 targetActor 产生的节点;keepVisible actor 的元素被剔除 + for (unsigned int i = 0; i < sel->GetNumberOfNodes(); ++i) { + vtkSelectionNode* node = sel->GetNode(i); + if (!node) + continue; + vtkProp* node_prop = vtkProp::SafeDownCast( + node->GetProperties()->Get(vtkSelectionNode::PROP())); + auto* list = vtkIdTypeArray::SafeDownCast(node->GetSelectionList()); + const bool matches_target = (node_prop == target_actor); + spdlog::debug("[AreaPick] node[{}]: prop={} matchesTarget={} listSize={}", + i, + node_prop ? node_prop->GetClassName() : "(null)", + matches_target, + list ? list->GetNumberOfTuples() : 0); + if (node_prop && !matches_target) + continue; + if (!list) + continue; + for (vtkIdType j = 0; j < list->GetNumberOfTuples(); ++j) { + vtkIdType id = list->GetValue(j); + if (id >= 0) + result.insert(id); + } + } + spdlog::debug("[AreaPick] result.size()={}", result.size()); + + return result; +} + +//! @brief 把 executeAreaPick 与 PickingOpacityGuard 串成一次性调用,便于 selector 复用 +inline std::set executeAreaPickWithGuard( + vtkRenderer* renderer, + vtkActor* target_actor, + int xmin, int ymin, int xmax, int ymax, + int field_association, + const std::vector* keep_visible = nullptr) +{ + PickingOpacityGuard guard(target_actor, keep_visible); + return executeAreaPick(renderer, target_actor, + xmin, ymin, xmax, ymax, field_association, keep_visible); +} + +//! @brief 数据空间 (lx,ly,lz) → world → display,测试屏幕投影是否落在框选矩形内。 +//! 用于 face CELLS 拾取后的二次过滤,避免"面只一角进框、所有点/边都高亮"。 +inline bool isWorldPointInScreenBox(vtkRenderer* renderer, + vtkActor* transform_actor, + double lx, double ly, double lz, + int xmin, int ymin, int xmax, int ymax) +{ + if (!renderer) + return false; + double world[4] = { lx, ly, lz, 1.0 }; + vtkMatrix4x4* m = transform_actor ? transform_actor->GetMatrix() : nullptr; + if (m) + m->MultiplyPoint(world, world); + renderer->SetWorldPoint(world); + renderer->WorldToDisplay(); + const double* dp = renderer->GetDisplayPoint(); + return dp[0] >= xmin && dp[0] <= xmax + && dp[1] >= ymin && dp[1] <= ymax; +} + +//! @brief 线段(world→display 投影后)是否与框选矩形相交。 +//! 任一端点在框内、或线段穿过矩形(含擦边)均视为相交("与边相交即选中"语义)。 +inline bool isScreenSegmentIntersectsBox(vtkRenderer* renderer, + vtkActor* transform_actor, + double lx0, double ly0, double lz0, + double lx1, double ly1, double lz1, + int xmin, int ymin, int xmax, int ymax) +{ + if (!renderer) + return false; + + // 端点投影路径与 isWorldPointInScreenBox 完全一致 + auto project = [&](double lx, double ly, double lz) { + double world[4] = { lx, ly, lz, 1.0 }; + vtkMatrix4x4* m = transform_actor ? transform_actor->GetMatrix() : nullptr; + if (m) + m->MultiplyPoint(world, world); + renderer->SetWorldPoint(world); + renderer->WorldToDisplay(); + const double* dp = renderer->GetDisplayPoint(); + return std::pair { dp[0], dp[1] }; + }; + + const auto p0 = project(lx0, ly0, lz0); + const auto p1 = project(lx1, ly1, lz1); + + auto in_box = [&](double x, double y) { + return x >= xmin && x <= xmax && y >= ymin && y <= ymax; + }; + if (in_box(p0.first, p0.second) || in_box(p1.first, p1.second)) + return true; + + // Liang–Barsky:2D 线段 vs 轴对齐矩形(无端点落入时判是否穿过) + const double dx = p1.first - p0.first; + const double dy = p1.second - p0.second; + const double p[4] = { -dx, dx, -dy, dy }; + const double q[4] = { p0.first - xmin, xmax - p0.first, + p0.second - ymin, ymax - p0.second }; + double t0 = 0.0, t1 = 1.0; + for (int i = 0; i < 4; ++i) { + if (p[i] == 0.0) { + if (q[i] < 0.0) + return false; // 平行于该轴且在矩形外 + } else { + const double r = q[i] / p[i]; + if (p[i] < 0.0) { + if (r > t1) + return false; + if (r > t0) + t0 = r; + } else { + if (r < t0) + return false; + if (r < t1) + t1 = r; + } + } + } + return true; +} + +} // namespace area_pick + +#endif // MESH_AREA_PICK_H \ No newline at end of file diff --git a/app/render/MeshSelectManager.cpp b/app/render/MeshSelectManager.cpp index e6345c441b75810b756288e0b4abf34cb817ebfc..bb2f90a1e3bce2f23315074044beb76126f986db 100644 --- a/app/render/MeshSelectManager.cpp +++ b/app/render/MeshSelectManager.cpp @@ -37,6 +37,15 @@ void MeshSelectManager::select(double posx, double posy) if (this->select_mode_ == SelectMode::None) return; + // 仅调一次 picker.Pick:selector 内部再 Pick 一次会污染 Qt FBO 渲染上下文的 + // picking buffer,导致紧随其后的 executeAreaPick 框选路径失效。 + if (this->select_mode_ == SelectMode::Vertex) { + component_picker_->SnapToMeshPointOn(); + component_picker_->SetPixelTolerance(5); + } else { + component_picker_->SnapToMeshPointOff(); + } + component_picker_->Pick(posx, posy, 0, renderer_); vtkActor* picked_actor = component_picker_->GetActor(); @@ -45,7 +54,21 @@ void MeshSelectManager::select(double posx, double posy) return; if (auto* sel = getOrCreateSelector(*component_id)) - sel->select(posx, posy); + sel->select(posx, posy, component_picker_.GetPointer(), picked_actor, + component_picker_->GetCellId(), component_picker_->GetPointId()); +} + +void MeshSelectManager::selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) +{ + if (this->select_mode_ == SelectMode::None) + return; + + // 框选遍历全部组件(与点选不同:不依赖 component_picker 锁定单一组件) + for (Index comp_id : op_->getAllComponentIds()) { + if (auto* sel = getOrCreateSelector(comp_id)) + sel->selectArea(xmin, ymin, xmax, ymax, add_only, remove_only); + } } void MeshSelectManager::setSelectMode(SelectMode select_mode) diff --git a/app/render/MeshSelectManager.h b/app/render/MeshSelectManager.h index a8f41a2fe4cf0ed4c64c8a2069ce7714c313180f..085f650a3791d0d8771fdfed46ef722fe4be9c2a 100644 --- a/app/render/MeshSelectManager.h +++ b/app/render/MeshSelectManager.h @@ -28,6 +28,13 @@ public: MeshSelectManager(vtkRenderer& renderer, vtkActor& highlight_actor, MeshActorManagerSelectOp& op); void select(double posx, double posy); + /** + * @brief 矩形框选入口(Ctrl+左键拖拽),遍历所有组件交给具体 selector 处理 + * @param add_only Shift 修饰:仅追加,不影响已选 + * @param remove_only Alt 修饰:仅移除,不影响其他已选 + */ + void selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only); void setSelectMode(SelectMode select_mode); /** * @brief 设置面选择的角度扩散参数,并同步到已创建的面选择器 diff --git a/app/render/QRenderWindowStyle.cpp b/app/render/QRenderWindowStyle.cpp index 1693d76896a682855bb85cc3dc9ef1a8a555aa7f..2ca8751f8a6b8aa65cbc7df31462f531083cefab 100644 --- a/app/render/QRenderWindowStyle.cpp +++ b/app/render/QRenderWindowStyle.cpp @@ -3,6 +3,7 @@ #include "SelectManager.h" #include #include +#include vtkStandardNewMacro(QRenderWindowStyle); @@ -21,30 +22,141 @@ void QRenderWindowStyle::SetInteractionService(InteractionService* service) this->interaction_service_ = service; } +vtkRenderer* QRenderWindowStyle::currentRenderer() +{ + auto* iren = this->GetInteractor(); + if (!iren) + return nullptr; + auto* rw = iren->GetRenderWindow(); + if (!rw) + return nullptr; + auto* ren = this->GetCurrentRenderer(); + if (!ren) { + auto* renderers = rw->GetRenderers(); + ren = renderers ? renderers->GetFirstRenderer() : nullptr; + } + return ren; +} + +void QRenderWindowStyle::attachRubberBand() +{ + if (rubber_band_attached_) + return; + auto* ren = currentRenderer(); + if (!ren) + return; + + // 5 个点的折线(首末闭合)画矩形 + rubber_band_points_->SetNumberOfPoints(5); + rubber_band_cells_->InsertNextCell(5); + for (vtkIdType i = 0; i < 5; ++i) + rubber_band_cells_->InsertCellPoint(i); + rubber_band_poly_->SetPoints(rubber_band_points_); + rubber_band_poly_->SetLines(rubber_band_cells_); + rubber_band_mapper_->SetInputData(rubber_band_poly_); + rubber_band_actor_->SetMapper(rubber_band_mapper_); + vtkNew prop; + prop->SetColor(0.0, 1.0, 0.0); + prop->SetLineWidth(2.0); + rubber_band_actor_->SetProperty(prop); + ren->AddActor2D(rubber_band_actor_); + rubber_band_attached_ = true; +} + +void QRenderWindowStyle::updateRubberBand() +{ + int xmin = std::min(box_start_[0], box_end_[0]); + int xmax = std::max(box_start_[0], box_end_[0]); + int ymin = std::min(box_start_[1], box_end_[1]); + int ymax = std::max(box_start_[1], box_end_[1]); + rubber_band_points_->SetPoint(0, xmin, ymin, 0.0); + rubber_band_points_->SetPoint(1, xmax, ymin, 0.0); + rubber_band_points_->SetPoint(2, xmax, ymax, 0.0); + rubber_band_points_->SetPoint(3, xmin, ymax, 0.0); + rubber_band_points_->SetPoint(4, xmin, ymin, 0.0); + rubber_band_points_->Modified(); + if (auto* iren = this->GetInteractor()) + iren->Render(); +} + +void QRenderWindowStyle::detachRubberBand() +{ + if (!rubber_band_attached_) + return; + auto* ren = currentRenderer(); + if (ren) + ren->RemoveActor2D(rubber_band_actor_); + rubber_band_attached_ = false; +} + +void QRenderWindowStyle::OnLeftButtonDown() +{ + auto* iren = this->GetInteractor(); + if (iren && iren->GetControlKey()) { + // Ctrl+左键 → 进入框选模式;不调父类,相机不被旋转/平移 + box_selecting_ = true; + box_add_only_ = iren->GetShiftKey(); + box_remove_only_ = iren->GetAltKey(); + iren->GetEventPosition(box_start_); + box_end_[0] = box_start_[0]; + box_end_[1] = box_start_[1]; + attachRubberBand(); + updateRubberBand(); + return; + } + vtkInteractorStyleTrackballCamera::OnLeftButtonDown(); +} + void QRenderWindowStyle::OnLeftButtonUp() { - if (click_) { - click_ = false; - int pos[2]; - this->GetInteractor()->GetEventPosition(pos); - if (interaction_service_ && interaction_service_->hasActiveState()) - interaction_service_->pick(pos[0], pos[1]); - else if (select_manager_) - select_manager_->select(pos[0], pos[1]); - } + if (box_selecting_) { + // 矩形太小不触发(与 demo 对齐:>=2 像素) + const int dx = std::abs(box_end_[0] - box_start_[0]); + const int dy = std::abs(box_end_[1] - box_start_[1]); + if (dx >= 2 && dy >= 2 && select_manager_) { + int xmin = std::min(box_start_[0], box_end_[0]); + int xmax = std::max(box_start_[0], box_end_[0]); + int ymin = std::min(box_start_[1], box_end_[1]); + int ymax = std::max(box_start_[1], box_end_[1]); + select_manager_->selectArea(xmin, ymin, xmax, ymax, + box_add_only_, box_remove_only_); + } + detachRubberBand(); + box_selecting_ = false; + // 吞掉 click_ 状态:框选路径不应触发点选 + click_ = false; + return; + } + + if (click_) { + click_ = false; + int pos[2]; + this->GetInteractor()->GetEventPosition(pos); + if (interaction_service_ && interaction_service_->hasActiveState()) + interaction_service_->pick(pos[0], pos[1]); + else if (select_manager_) + select_manager_->select(pos[0], pos[1]); + } - vtkInteractorStyleTrackballCamera::OnLeftButtonUp(); + vtkInteractorStyleTrackballCamera::OnLeftButtonUp(); } void QRenderWindowStyle::OnMouseMove() { - vtkInteractorStyleTrackballCamera::OnMouseMove(); + vtkInteractorStyleTrackballCamera::OnMouseMove(); - // 仅在无拖拽(非旋转/平移等相机操作)时做交互动态预览 - if (interaction_service_ && interaction_service_->hasActiveState() && this->State == VTKIS_NONE) { - int pos[2]; - this->GetInteractor()->GetEventPosition(pos); - interaction_service_->hover(pos[0], pos[1]); - this->GetInteractor()->Render(); - } -} + if (box_selecting_) { + // 更新橡皮筋;不进入交互动态预览(避免与框选拖动视觉冲突) + this->GetInteractor()->GetEventPosition(box_end_); + updateRubberBand(); + return; + } + + // 仅在无拖拽(非旋转/平移等相机操作)时做交互动态预览 + if (interaction_service_ && interaction_service_->hasActiveState() && this->State == VTKIS_NONE) { + int pos[2]; + this->GetInteractor()->GetEventPosition(pos); + interaction_service_->hover(pos[0], pos[1]); + this->GetInteractor()->Render(); + } +} \ No newline at end of file diff --git a/app/render/QRenderWindowStyle.h b/app/render/QRenderWindowStyle.h index 1cef65fbfd6582724e4b7f0f297fd075a7e24466..1ff35ad53966b66e6b1123a732095af03f228520 100644 --- a/app/render/QRenderWindowStyle.h +++ b/app/render/QRenderWindowStyle.h @@ -6,7 +6,17 @@ */ #ifndef Q_RENDER_WINDOW_STYLE_H #define Q_RENDER_WINDOW_STYLE_H +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include class SelectManager; class InteractionService; @@ -19,11 +29,35 @@ public: void SetSelectManager(SelectManager* select_manager); //! @brief 交互服务激活时点击/悬停走交互拾取,否则走选择系统 void SetInteractionService(InteractionService* service); + void OnLeftButtonDown() override; void OnLeftButtonUp() override; void OnMouseMove() override; private: + //! @brief 取当前主渲染器(兜底用 DefaultRenderer / 第一个 renderer) + vtkRenderer* currentRenderer(); + //! @brief 把橡皮筋 actor 添加到当前 renderer(box 开始时调用一次) + void attachRubberBand(); + //! @brief 按当前 start/end 更新橡皮筋几何 + void updateRubberBand(); + //! @brief 移除橡皮筋 actor + void detachRubberBand(); + bool click_ {}; + bool box_selecting_ {}; //> 框选拖拽中(Ctrl+左键) + int box_start_[2] { 0, 0 }; + int box_end_[2] { 0, 0 }; + bool box_add_only_ {}; //> Shift 修饰:仅追加 + bool box_remove_only_ {}; //> Alt 修饰:仅移除 + + // 橡皮筋:用 5 个顶点的折线(首末闭合)画矩形;actor2d 直接坐标用屏幕像素 + vtkNew rubber_band_points_; + vtkNew rubber_band_cells_; + vtkNew rubber_band_poly_; + vtkNew rubber_band_mapper_; + vtkNew rubber_band_actor_; + bool rubber_band_attached_ {}; + SelectManager* select_manager_{}; InteractionService* interaction_service_{}; }; diff --git a/app/render/SelectManager.cpp b/app/render/SelectManager.cpp index 0a111ea69718a49c149584b3ef893d137137cc4e..7aff64e73e4cc51a23ed66511057b7a4b43d6891 100644 --- a/app/render/SelectManager.cpp +++ b/app/render/SelectManager.cpp @@ -36,6 +36,14 @@ void SelectManager::select(double posx, double posy) geom_->select(posx, posy); } +void SelectManager::selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) +{ + // 几何/组件框选后续再补;当前仅网格支持框选 + if (is_mesh_mode(select_mode_)) + mesh_->selectArea(xmin, ymin, xmax, ymax, add_only, remove_only); +} + void SelectManager::setSelectMode(const std::string& select_mode) { SelectMode mode = SelectMode::None; diff --git a/app/render/SelectManager.h b/app/render/SelectManager.h index 843f7885289c73ad158e5d49c9a6842b60c9eab5..fe54ef9a799b526894617017ea6fce302636f866 100644 --- a/app/render/SelectManager.h +++ b/app/render/SelectManager.h @@ -25,6 +25,12 @@ public: MeshActorManagerSelectOp& mesh_op, GeometryActorManagerSelectOp& geom_op); ~SelectManager(); void select(double posx, double posy); + /** + * @brief 矩形框选入口(Ctrl+左键拖拽),目前仅转发给网格选择管理器; + * 几何/组件框选后续再补 + */ + void selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only); void setSelectMode(const std::string& select_mode); /** * @brief 设置网格面选择的角度扩散参数 diff --git a/app/render/SelectorHighlight.h b/app/render/SelectorHighlight.h index 311e179b151948f5b23e44db0236898ad38ddebc..d396784b0282e991dbc1166fc7fa063fd7a360d1 100644 --- a/app/render/SelectorHighlight.h +++ b/app/render/SelectorHighlight.h @@ -41,7 +41,22 @@ struct FaceSelectionSpreadOptions { class SelectorHighlight { public: virtual ~SelectorHighlight() = default; + //! @brief 单点拾取(内部建 picker 做硬件 picking)。生产路径由 MeshSelectManager 预拾后调下面的 picker 重载。 virtual void select(double posx, double posy) = 0; + //! @brief 单点拾取:使用外层预 picker.Pick 拾取结果,避免两次 picker.Pick 污染 picking buffer。 + virtual void select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType picked_point_id) = 0; + /** + * @brief 矩形框选入口(Ctrl+左键拖拽) + * @param xmin ymin xmax ymax 屏幕像素矩形 + * @param add_only Shift 修饰:仅追加,不影响已选 + * @param remove_only Alt 修饰:仅移除,不影响其他已选 + * @param toggle 二者皆否:每个 id 单独 toggle(与点选切换语义对齐) + */ + virtual void selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) + = 0; /** * @brief 清空选中元素,并取消高亮 */ @@ -65,6 +80,11 @@ public: unsigned int partition_id, MeshActorSelectOp select_op); ~FaceSelectorHighlight() override; void select(double posx, double posy) override; + void select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType picked_point_id) override; + void selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) override; void clear() override; void disableHighlight() override; void enableHighlight() override; @@ -112,6 +132,11 @@ public: Index component_id, const IMeshIdQuery* id_query); ~EdgeSelectorHighlight() override; void select(double posx, double posy) override; + void select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType picked_point_id) override; + void selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) override; void clear() override; void disableHighlight() override; void enableHighlight() override; @@ -142,6 +167,11 @@ public: unsigned int partition_id, MeshActorSelectOp select_op); ~SolidSelectorHighlight() override; void select(double posx, double posy) override; + void select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType picked_point_id) override; + void selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) override; void clear() override; void disableHighlight() override; void enableHighlight() override; @@ -166,6 +196,11 @@ public: Index component_id, const IMeshIdQuery* id_query); ~VertexSelectorHighlight() override; void select(double posx, double posy) override; + void select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType picked_point_id) override; + void selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) override; void clear() override; void disableHighlight() override; void enableHighlight() override; diff --git a/app/render/SolidSelectorHighlight.cpp b/app/render/SolidSelectorHighlight.cpp index 828c7570cbb94426cd071f69e66a760aa5a499b2..fd3c1f3359ba9a74cb11eeba7d2834e35d4f7316 100644 --- a/app/render/SolidSelectorHighlight.cpp +++ b/app/render/SolidSelectorHighlight.cpp @@ -1,9 +1,11 @@ +#include "MeshAreaPick.h" #include "CoincidentTopology.h" #include "MeshActorSelectOp.h" #include "SelectorHighlight.h" #include #include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include @@ -85,18 +88,30 @@ void SolidSelectorHighlight::enableHighlight() void SolidSelectorHighlight::select(double posx, double posy) { - // 获取 picked_cell_id和picked_data_set + // 兼容路径:自行构建 picker 并拾取;生产路径由 MeshSelectManager 预拾后调下方的 picker 重载。 vtkNew picker; picker->PickFromListOn(); picker->AddPickList(&select_op_.getSolidActor()); picker->Pick(posx, posy, 0, renderer_); - vtkIdType picked_cell_id = picker->GetCellId(); - if (picked_cell_id < 0) { + + select(posx, posy, picker.GetPointer(), picker->GetActor(), + picker->GetCellId(), picker->GetPointId()); +} + +void SolidSelectorHighlight::select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType picked_cell_id, vtkIdType /*picked_point_id*/) +{ + if (!picked_actor || picked_cell_id < 0) { clear(); spdlog::debug("No cell picked, selection cleared."); return; } - vtkDataSet* picked_data_set = picker->GetDataSet(); + vtkDataSet* picked_data_set = picker ? picker->GetDataSet() : nullptr; + if (!picked_data_set) { + clear(); + return; + } // 获取对应的体id selected_solid_id auto solid_id_array = vtkIdTypeArray::SafeDownCast(picked_data_set->GetCellData()->GetArray("vtkOriginalCellIds")); @@ -135,3 +150,57 @@ void SolidSelectorHighlight::setupHighlightStyle(vtkActor& actor, vtkMapper& map prop->SetLineWidth(2.0); actor.SetProperty(prop); } + +void SolidSelectorHighlight::selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) +{ + // solid_actor 是体表面 primitive:自遮挡足够,keepVisible=nullptr + vtkActor* target = vtkActor::SafeDownCast(&select_op_.getSolidActor()); + if (!target) + return; + + auto picked = area_pick::executeAreaPickWithGuard(renderer_, target, + xmin, ymin, xmax, ymax, vtkDataObject::FIELD_ASSOCIATION_CELLS, nullptr); + + spdlog::debug("[SolidArea] picked.size()={}", picked.size()); + if (picked.empty()) + return; + + // 反查 render cell id -> 原 solid id:solid_actor mapper 输入 poly data 上挂的 + // vtkOriginalCellIds 跟着 solid_filter_(可能含 clip)透传,索引是 render cell 下标 + auto* mapper = vtkPolyDataMapper::SafeDownCast(target->GetMapper()); + vtkPolyData* poly = mapper ? mapper->GetInput() : nullptr; + auto* orig_cell_ids = poly + ? vtkIdTypeArray::SafeDownCast(poly->GetCellData()->GetArray("vtkOriginalCellIds")) + : nullptr; + if (!orig_cell_ids) + return; + + selected_ids_->ClearLookup(); + if (remove_only) { + for (vtkIdType cid : picked) { + vtkIdType orig = orig_cell_ids->GetValue(cid); + vtkIdType idx = selected_ids_->LookupTypedValue(orig); + if (idx >= 0) + selected_ids_->RemoveTuple(idx); + } + } else if (add_only) { + for (vtkIdType cid : picked) { + vtkIdType orig = orig_cell_ids->GetValue(cid); + if (orig >= 0) + selected_ids_->InsertNextValue(orig); + } + } else { + // toggle + for (vtkIdType cid : picked) { + vtkIdType orig = orig_cell_ids->GetValue(cid); + vtkIdType idx = selected_ids_->LookupTypedValue(orig); + if (idx >= 0) + selected_ids_->RemoveTuple(idx); + else if (orig >= 0) + selected_ids_->InsertNextValue(orig); + } + } + selected_ids_->Modified(); + enableHighlight(); +} diff --git a/app/render/VertexSelectorHighlight.cpp b/app/render/VertexSelectorHighlight.cpp index 522ecbea9c9769c064ec0af76f89f85c8dec0756..ccacae105811ad3578250d6b15f87dc8244eae44 100644 --- a/app/render/VertexSelectorHighlight.cpp +++ b/app/render/VertexSelectorHighlight.cpp @@ -1,10 +1,14 @@ +#include "MeshAreaPick.h" #include "CoincidentTopology.h" #include "MeshActorSelectOp.h" #include "MeshIdQuery.h" #include "Selection.h" #include "SelectorHighlight.h" #include +#include #include +#include +#include #include #include #include @@ -12,6 +16,7 @@ #include #include #include +#include #include #include @@ -90,7 +95,7 @@ void VertexSelectorHighlight::enableHighlight() void VertexSelectorHighlight::select(double posx, double posy) { - // 获取 picked_point_id + // 兼容路径:自行构建 picker 并拾取;生产路径由 MeshSelectManager 预拾后调下方的 picker 重载。 vtkNew picker; picker->SnapToMeshPointOn(); // 启用贴近网格点 picker->SetPixelTolerance(5); // 设置点拾取像素容差 @@ -99,8 +104,17 @@ void VertexSelectorHighlight::select(double posx, double posy) picker->AddPickList(&select_op_.getFaceActor()); picker->AddPickList(&select_op_.getEdgeActor()); picker->Pick(posx, posy, 0, renderer_); - vtkIdType picked_point_id = picker->GetPointId(); - if (picked_point_id == -1) { + + select(posx, posy, picker.GetPointer(), picker->GetActor(), + picker->GetCellId(), picker->GetPointId()); +} + +void VertexSelectorHighlight::select(double posx, double posy, + vtkHardwarePicker* picker, vtkActor* picked_actor, + vtkIdType /*picked_cell_id*/, vtkIdType picked_point_id) +{ + // 未命中点(SnapToMeshPoint 取不到)按原语义保留已有选择 + if (!picker || !picked_actor || picked_point_id == -1) { spdlog::debug("VertexSelectorHighlight::select: no point picked."); return; } @@ -110,7 +124,7 @@ void VertexSelectorHighlight::select(double posx, double posy) auto vertex_id_array = vtkIdTypeArray::SafeDownCast(picked_data_set->GetPointData()->GetArray("vtkOriginalPointIds")); if (!vertex_id_array) { clear(); - spdlog::debug("Picked cell id: {}, no vertex id array found.", picked_point_id); + spdlog::debug("Picked point id: {}, no vertex id array found.", picked_point_id); return; } vtkIdType selected_vertex_id = vertex_id_array->GetValue(picked_point_id); @@ -142,3 +156,113 @@ void VertexSelectorHighlight::setupHighlightStyle(vtkActor& actor, vtkMapper& ma prop->SetPointSize(6.0); actor.SetProperty(prop); } + +void VertexSelectorHighlight::selectArea(int xmin, int ymin, int xmax, int ymax, + bool add_only, bool remove_only) +{ + // 与点选对齐:框选同样从 face/edge/solid 三个 actor 取点,逐 actor 走 CELLS 拾取后合并。 + // POINTS pass 不渲染面会穿透("点选后框选失效"根因),统一走 CELLS + 屏幕投影二次过滤。 + std::set local_ids; + + // 对单个 actor 执行 CELLS 框选并把命中 cell 派生为"组件局部点 id" + auto pick_points = [&](vtkActor* target, vtkPolyData* poly, + const std::vector& keep_visible) { + if (!target || !poly || poly->GetNumberOfCells() == 0) + return; + const std::vector* pKeep = keep_visible.empty() ? nullptr : &keep_visible; + auto picked = area_pick::executeAreaPickWithGuard(renderer_, target, + xmin, ymin, xmax, ymax, vtkDataObject::FIELD_ASSOCIATION_CELLS, pKeep); + if (picked.empty()) + return; + + // 裁剪链上点 id 会重排:vTK 拾取后经 vtkOriginalPointIds 还原组件局部点 id + auto* orig_pt_ids = vtkIdTypeArray::SafeDownCast( + poly->GetPointData()->GetArray("vtkOriginalPointIds")); + for (vtkIdType cell_id : picked) { + if (cell_id < 0 || cell_id >= poly->GetNumberOfCells()) + continue; + vtkCell* cell = poly->GetCell(cell_id); + if (!cell) + continue; + for (int i = 0; i < cell->GetNumberOfPoints(); ++i) { + vtkIdType rp = cell->GetPointId(i); + if (rp < 0 || rp >= poly->GetNumberOfPoints()) + continue; + double p[3]; + poly->GetPoint(rp, p); + // 像素在框内才保留(demo POINTS 语义) + if (!area_pick::isWorldPointInScreenBox(renderer_, target, + p[0], p[1], p[2], xmin, ymin, xmax, ymax)) + continue; + const vtkIdType lp = (orig_pt_ids && rp < orig_pt_ids->GetNumberOfTuples()) + ? orig_pt_ids->GetValue(rp) + : rp; + if (lp >= 0) + local_ids.insert(lp); + } + } + }; + + // 1) face 表面(主路径) + auto* face_actor = vtkActor::SafeDownCast(&select_op_.getFaceActor()); + if (auto* face_poly = face_actor ? vtkPolyData::SafeDownCast( + vtkPolyDataMapper::SafeDownCast(face_actor->GetMapper())->GetInput()) : nullptr) { + std::vector keep; + if (auto* solid_a = vtkActor::SafeDownCast(&select_op_.getSolidActor())) + if (solid_a != face_actor) + keep.push_back(solid_a); + pick_points(face_actor, face_poly, keep); + } + + // 2) edge actor:独立/物化边上的点(线 cell 端点) + auto* edge_actor = vtkActor::SafeDownCast(&select_op_.getEdgeActor()); + if (auto* edge_poly = edge_actor ? vtkPolyData::SafeDownCast( + vtkPolyDataMapper::SafeDownCast(edge_actor->GetMapper())->GetInput()) : nullptr) { + if (edge_poly->GetNumberOfLines() > 0) { + std::vector keep; + if (auto* face_a = vtkActor::SafeDownCast(&select_op_.getFaceActor())) + if (face_a != edge_actor) + keep.push_back(face_a); + if (auto* solid_a = vtkActor::SafeDownCast(&select_op_.getSolidActor())) + if (solid_a != edge_actor) + keep.push_back(solid_a); + pick_points(edge_actor, edge_poly, keep); + } + } + + // 3) solid 表面:体网格表面点(face 无 cell 时是主要来源) + auto* solid_actor = vtkActor::SafeDownCast(&select_op_.getSolidActor()); + if (auto* solid_poly = solid_actor ? vtkPolyData::SafeDownCast( + vtkPolyDataMapper::SafeDownCast(solid_actor->GetMapper())->GetInput()) : nullptr) { + pick_points(solid_actor, solid_poly, {}); + } + + spdlog::debug("[VertexArea] local_ids.size()={}", local_ids.size()); + if (local_ids.empty()) + return; + + selected_ids_->ClearLookup(); + if (remove_only) { + for (vtkIdType v : local_ids) { + vtkIdType idx = _is_selected(v, *selected_ids_); + if (idx >= 0) + selected_ids_->RemoveTuple(idx); + } + } else if (add_only) { + for (vtkIdType v : local_ids) { + if (_is_selected(v, *selected_ids_) < 0) + selected_ids_->InsertNextValue(v); + } + } else { + // toggle:按点 id 在 selected_ids_ 中查重 + for (vtkIdType v : local_ids) { + vtkIdType idx = _is_selected(v, *selected_ids_); + if (idx >= 0) + selected_ids_->RemoveTuple(idx); + else + selected_ids_->InsertNextValue(v); + } + } + selected_ids_->Modified(); + enableHighlight(); +}