Highlight the faces a selected feature made in the Design tab

Selections are drawn as opaque faces in the selection colour with a cased outline instead of a
translucent tint over the body, so they read on a body of any colour. Selecting a Feature tree
row lights the faces that feature made rather than its whole body, which also makes fillet and
chamfer rows highlight again.
This commit is contained in:
SoftFever
2026-10-05 02:50:14 +08:00
parent be72afc9f7
commit 073e2d9c44
10 changed files with 866 additions and 203 deletions
+24
View File
@@ -187,6 +187,30 @@ degenerate edges are left out (`GeometryEngine::display_edges`), and the polylin
once per shape, keyed by its `TShape`, because a recompute that leaves a body unchanged is the
common case.
## Showing what is selected
A selection is drawn on the faces it names, never as a tint over the body: a translucent
selection colour blended into the body's own colour turns a different hue on every body and
vanishes on one close to it. Selected faces are split out of their body into a volume of their
own, which the canvas draws opaque in the selection colour through the same shader and lighting
as the body (`DesignCanvas::rebuild_bodies`); the sketch overlay outlines them with a cased line
— a dark band under a selection-coloured one — so the outline still reads on a body that wears
the selection colour itself. A body picked whole, a face picked in the viewport and the faces of
the Feature tree's selected feature all draw this way. The hover pre-highlight is the outline
alone, uncased: it promises a click, it is not one.
Selecting a feature row lights the faces that feature made, not the whole body it sits on, so a
fillet row shows its round and the extrude under it keeps the faces the fillet trimmed.
`CadDocument::faces_made_by` answers it without per-feature history: it replays the recipe to
just before the feature and then the feature alone, and a face of the finished model belongs to
the feature when an interior point of it lies on the boundary afterwards and not before, facing
the same way — the facing keeps a block stacked on a base the owner of its bottom face. A feature
that makes no face of its own, such as a Boolean union, answers with the bodies it changed. The
replay costs up to a recompute, so the panel finds the faces once per row and topology
generation, off the UI thread, and only while no feature card is open. One selection is live at
a time: a viewport pick clears the feature row and a feature row clears the viewport pick, as the
Feature tree and Bodies list do between themselves.
## Following the app
The tab is a page of Orca's main window and answers to the same settings as Prepare.
+214 -32
View File
@@ -84,6 +84,7 @@
#include <initializer_list>
#include <math.h>
#include <map>
#include <memory>
#include <set>
#include <stdexcept>
#include <algorithm>
@@ -92,6 +93,11 @@
#include <cereal/archives/binary.hpp>
#include <cereal/types/array.hpp>
#include <BRepTools.hxx>
#include <BRepClass_FaceClassifier.hxx> // faces_made_by: is a point on a face
#include <BRepLProp_SLProps.hxx>
#include <GeomAPI_ProjectPointOnSurf.hxx>
#include <Poly_Triangulation.hxx>
#include <Precision.hxx>
#include <string>
#include <vector>
#include <utility>
@@ -4017,6 +4023,39 @@ void CadDocument::route_feature(std::vector<CadBody>& bodies, const CadFeature&
}
}
void CadDocument::bind_expressions()
{
std::map<std::string, double> varvals = evaluate_variables(variables);
for (CadFeature& f : features)
for (const auto& [field, e] : f.expr)
assign_field(f, field, eval_expr(e, varvals));
}
void CadDocument::replay_feature(size_t fi, std::vector<CadBody>& built)
{
CadFeature& f = features[fi];
if (!f.enabled) return;
if (f.type == CadFeatureType::Sketch) return; // consumed by an extrude
if (f.type == CadFeatureType::Helix) return; // consumed by Sweep as a path
if (f.type == CadFeatureType::Plane) return; // datum: no solid, derived on demand
if (f.type == CadFeatureType::Axis) return; // datum axis
if (f.type == CadFeatureType::CoordSys) return; // datum coordinate system
settle_body_refs(f, built, true);
if (f.type == CadFeatureType::Project) { apply_project(built, f); }
else { route_feature(built, f); }
// Record which feature made each body. "Still unset?" is the whole rule, and it is
// sufficient because of an invariant worth stating: NO feature ever replaces a whole
// CadBody. Every in-place op writes only `.shape` (boolean, cut, mirror-fuse,
// transform, dress-up — checked, all 8 sites), so an existing body keeps the stamp it
// was born with; a consumed body is erased outright, taking its stamp with it; and
// the only bodies still at -1 here are the ones THIS feature just pushed. That also
// means a feature type added later needs no change here, as long as it keeps to the
// same invariant.
for (CadBody& b : built)
if (b.source_feature < 0)
b.source_feature = int(fi);
}
bool CadDocument::recompute()
{
error.clear();
@@ -4030,39 +4069,13 @@ bool CadDocument::recompute()
// the 3MF recipe only "on success", so nothing was written, and deserialize_recipe ends with
// `return recompute()`, so a project that did carry a recipe was refused on load with
// "Could not restore the CAD model" while its features sat correctly in the list. mtav.
bool any_solid_feature = false;
const bool any_solid_feature = std::any_of(features.begin(), features.end(), [](const CadFeature& f) {
return f.enabled && produces_body(f.type);
});
try {
// Parametric pass: evaluate document variables, then each feature's expression bindings,
// writing the results into the feature's numeric fields before geometry runs.
std::map<std::string, double> varvals = evaluate_variables(variables);
for (CadFeature& f : features)
for (const auto& [field, e] : f.expr)
assign_field(f, field, eval_expr(e, varvals));
for (size_t fi = 0; fi < features.size(); ++fi) {
CadFeature& f = features[fi];
if (!f.enabled) continue;
if (f.type == CadFeatureType::Sketch) continue; // consumed by an extrude
if (f.type == CadFeatureType::Helix) continue; // consumed by Sweep as a path
if (f.type == CadFeatureType::Plane) continue; // datum: no solid, derived on demand
if (f.type == CadFeatureType::Axis) continue; // datum axis
if (f.type == CadFeatureType::CoordSys) continue; // datum coordinate system
// Past the skips. Project runs here too but leaves no body of its own.
if (produces_body(f.type)) any_solid_feature = true;
settle_body_refs(f, built, true);
if (f.type == CadFeatureType::Project) { apply_project(built, f); }
else { route_feature(built, f); }
// Record which feature made each body. "Still unset?" is the whole rule, and it is
// sufficient because of an invariant worth stating: NO feature ever replaces a whole
// CadBody. Every in-place op writes only `.shape` (boolean, cut, mirror-fuse,
// transform, dress-up — checked, all 8 sites), so an existing body keeps the stamp it
// was born with; a consumed body is erased outright, taking its stamp with it; and
// the only bodies still at -1 here are the ones THIS feature just pushed. That also
// means a feature type added later needs no change here, as long as it keeps to the
// same invariant.
for (CadBody& b : built)
if (b.source_feature < 0)
b.source_feature = int(fi);
}
bind_expressions();
for (size_t fi = 0; fi < features.size(); ++fi)
replay_feature(fi, built);
} catch (const Standard_Failure& e) {
// OCCT raises Standard_Failure (NOT a std::exception) — must be caught
// here or it escapes the event handler and terminates the app.
@@ -4217,6 +4230,175 @@ bool CadDocument::preview(const CadFeature& candidate, TriangleMesh& out_mesh, s
return preview(candidate, out_mesh, ignore, err);
}
namespace {
// A face of a replayed body and what the boundary test needs from it. The projector is built on
// first use: few probes pass the box test, and building one on a freeform surface samples the
// whole surface.
struct FaceProbe {
TopoDS_Face face;
Handle(Geom_Surface) surface;
Bnd_Box box;
mutable std::unique_ptr<GeomAPI_ProjectPointOnSurf> proj;
};
// How close a point must be to lie on a face: a face rebuilt on the same surface is within
// 1e-7 mm of it, a new face nowhere near.
constexpr double kOnFaceTol = 1e-4;
std::vector<FaceProbe> face_probes(const std::vector<CadBody>& bodies)
{
std::vector<FaceProbe> out;
for (const CadBody& b : bodies)
for (TopExp_Explorer e(b.shape, TopAbs_FACE); e.More(); e.Next()) {
FaceProbe p;
p.face = TopoDS::Face(e.Current());
p.surface = BRep_Tool::Surface(p.face);
if (p.surface.IsNull()) continue;
BRepBndLib::Add(p.face, p.box);
p.box.Enlarge(kOnFaceTol);
out.push_back(std::move(p));
}
return out;
}
bool outward_normal(const TopoDS_Face& face, double u, double v, gp_Dir& n)
{
BRepAdaptor_Surface surf(face);
BRepLProp_SLProps props(surf, u, v, 1, Precision::Confusion());
if (!props.IsNormalDefined()) return false;
n = props.Normal();
if (face.Orientation() == TopAbs_REVERSED) n.Reverse();
return true;
}
// A point inside `face` and its outward normal there: the UV centroid of the largest triangle of
// the face's triangulation, which lies inside the trimmed face even where the centre of its UV
// bounds falls in a hole. Without a triangulation, that centre, if it is inside.
bool interior_point(const TopoDS_Face& face, gp_Pnt& p, gp_Dir& n)
{
TopLoc_Location loc;
const Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(face, loc);
double u = 0., v = 0., best = -1.;
if (!tri.IsNull() && tri->HasUVNodes())
for (int i = 1; i <= tri->NbTriangles(); ++i) {
int a, b, c;
tri->Triangle(i).Get(a, b, c);
const gp_Pnt pa = tri->Node(a);
const double area = gp_Vec(pa, tri->Node(b)).Crossed(gp_Vec(pa, tri->Node(c))).SquareMagnitude();
if (area <= best) continue;
best = area;
const gp_XY uv = (tri->UVNode(a).XY() + tri->UVNode(b).XY() + tri->UVNode(c).XY()) / 3.;
u = uv.X();
v = uv.Y();
}
if (best < 0.) {
double u0, u1, v0, v1;
BRepTools::UVBounds(face, u0, u1, v0, v1);
u = 0.5 * (u0 + u1);
v = 0.5 * (v0 + v1);
if (BRepClass_FaceClassifier(face, gp_Pnt2d(u, v), Precision::Confusion()).State() != TopAbs_IN)
return false;
}
p = BRepAdaptor_Surface(face).Value(u, v);
return outward_normal(face, u, v, n);
}
// Does `p` lie on one of `probes`, on a face turned the same way as `n`? The facing tells apart
// coincident faces of different bodies: a block stacked on a base has its bottom in the base's
// top face, turned the other way.
bool on_boundary(const std::vector<FaceProbe>& probes, const gp_Pnt& p, const gp_Dir& n)
{
for (const FaceProbe& fp : probes) {
if (fp.box.IsOut(p)) continue;
try {
if (!fp.proj) {
// The setup GeomAPI_ProjectPointOnSurf(p, surface) repeats for every point, done once.
double u0, u1, v0, v1;
fp.surface->Bounds(u0, u1, v0, v1);
fp.proj = std::make_unique<GeomAPI_ProjectPointOnSurf>();
fp.proj->Init(fp.surface, u0, u1, v0, v1, Precision::Confusion());
}
GeomAPI_ProjectPointOnSurf& proj = *fp.proj;
proj.Perform(p);
if (proj.NbPoints() == 0 || proj.LowerDistance() > kOnFaceTol) continue;
double u, v;
proj.LowerDistanceParameters(u, v);
if (BRepClass_FaceClassifier(fp.face, gp_Pnt2d(u, v), kOnFaceTol).State() == TopAbs_OUT) continue;
gp_Dir m;
if (outward_normal(fp.face, u, v, m) && m.Dot(n) > 0.) return true;
} catch (const Standard_Failure&) {
// A surface the projection cannot handle says nothing about the point.
}
}
return false;
}
} // namespace
std::vector<std::pair<int, int>> CadDocument::faces_made_by(int index) const
{
std::vector<std::pair<int, int>> out;
if (index < 0 || index >= int(features.size()) || !features[index].enabled
|| !produces_body(features[index].type) || bodies.empty())
return out;
// Replay up to the feature, then the feature alone on a copy, so a body it left alone is the
// same shape on both sides. On a scratch document, as a replay settles references in the
// features it runs; it reads only the features, the variables and the process-wide weld rule.
CadDocument tmp;
tmp.features = features;
tmp.variables = variables;
std::vector<CadBody> before, after;
try {
set_sketch_auto_close(auto_close_loops);
tmp.bind_expressions();
for (size_t fi = 0; fi < size_t(index); ++fi)
tmp.replay_feature(fi, before);
after = before;
tmp.replay_feature(size_t(index), after);
} catch (...) { // OCCT's Standard_Failure as well as std::exception
return out;
}
// The bodies the step made or replaced. A face on any other body was on the boundary before
// the step too, so only these are probed after it.
std::vector<int> changed;
std::vector<CadBody> changed_bodies;
for (int j = 0; j < int(after.size()); ++j)
if (std::none_of(before.begin(), before.end(), [&](const CadBody& w) { return w.shape.IsSame(after[j].shape); })) {
changed.push_back(j);
changed_bodies.push_back(after[j]);
}
const std::vector<FaceProbe> was = face_probes(before), now = face_probes(changed_bodies);
for (int b = 0; b < int(bodies.size()); ++b) {
int id = 0;
for (TopExp_Explorer e(bodies[b].shape, TopAbs_FACE); e.More(); e.Next(), ++id) {
gp_Pnt p;
gp_Dir n;
try {
if (interior_point(TopoDS::Face(e.Current()), p, n) && on_boundary(now, p, n) && !on_boundary(was, p, n))
out.emplace_back(b, id);
} catch (const Standard_Failure&) {
// A face that cannot be sampled is left out, not the whole answer.
}
}
}
if (!out.empty())
return out;
// No face of its own: answer with every face of each body the step changed, found again in
// the finished model by the id a body keeps across the rest of the history.
for (int j : changed) {
const int b = find_body(bodies, body_id_of(after, j));
if (b < 0) continue;
const int n = GeometryEngine::face_count(bodies[b].shape);
for (int id = 0; id < n; ++id)
out.emplace_back(b, id);
}
return out;
}
std::string brep_to_string(const TopoDS_Shape& s)
{
if (s.IsNull()) return {};
+17
View File
@@ -848,6 +848,16 @@ public:
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh,
std::vector<TriangleMesh>& out_body_meshes, std::string& err) const;
// The faces of the current bodies that features[index] made, as (body, face id) pairs: what
// the Design tab highlights when that feature is selected. A face counts when it lies on the
// model's boundary, facing the same way, right after the feature and not right before it, so
// a face a later feature trimmed still belongs to the one that made it. Positions are
// compared, no history is kept: a face a later feature rebuilt in place stays the earlier
// feature's. A feature with no face left of its own (a Boolean union, or one whose faces a
// later feature removed) answers with every face of each body it changed. Empty for a feature
// that leaves no body, a hidden one, or a history that no longer rebuilds up to it.
std::vector<std::pair<int, int>> faces_made_by(int index) const;
private:
TopoDS_Wire build_sketch_wire(const CadFeature& sketch, bool closed_only = false) const;
// The planar region an Extrude sweeps: the sketch's outer loop with its inner loops as
@@ -864,6 +874,13 @@ private:
// starts a new body (empty list, or an Extrude with mode New) vs mutates an existing
// one, then apply_feature. Shared by recompute() (replay all) and preview() (candidate).
void route_feature(std::vector<CadBody>& bodies, const CadFeature& f) const;
// The parametric pass every replay starts with: evaluate the variables and write each
// feature's expression bindings into its numeric fields.
void bind_expressions();
// One step of the replay: route features[fi] into `built` and stamp the bodies it created
// with fi. A hidden feature, a sketch, a helix or a datum leaves `built` alone. Throws on
// failure.
void replay_feature(size_t fi, std::vector<CadBody>& built);
// Boolean between two existing bodies: resolve target + tool, optionally snap the tool so
// the picked faces mate, run the OCCT op (with fuzzy tolerance), write the result back to the
// target and erase the consumed tool. Mutates the bodies vector directly (unlike apply_feature,
+96 -51
View File
@@ -16,6 +16,7 @@
#include "libslic3r/Config.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <climits>
#include <cstdio>
#include <cstdlib>
@@ -331,33 +332,25 @@ void DesignCanvas::reload(bool keep_view)
for (int i = 0; i < (int)m_model.objects.size(); ++i)
m_canvas->load_object(m_model, i);
const ColorRGBA sel_gold = design_selection_color(); // same colour as every other selection
const ColorRGBA ghost(0.26f, 0.66f, 1.0f, 0.45f);
const auto& volumes = m_canvas->get_volumes().volumes;
for (auto* v : volumes) {
int obj_idx = v->object_idx();
if (obj_idx == 0) {
// Object 0 holds one volume per body — colour each by its body index so
// multiple coexisting solids are visually distinct (Onshape per-part colour).
const int b = v->volume_idx();
// Object 0 holds the bodies (rebuild_bodies): each volume in its body's colour, so
// coexisting solids read as distinct parts (Onshape per-part colour), or in the
// selection colour when it holds the body's selected faces.
const int vi = v->volume_idx();
const int b = vi < int(m_volumes.size()) ? m_volumes[vi].body : vi;
const bool lit = vi < int(m_volumes.size()) && m_volumes[vi].lit;
bool hidden = (b >= 0 && b < int(m_body_visible.size())) && !m_body_visible[b];
// Preview-only mode (fillet/chamfer/draft, once a valid target is picked): hide
// every base body so only the result ghost is on screen until Confirm.
if (m_body_hidden) hidden = true;
v->is_active = !hidden; // per-body visibility toggle
if (!hidden) {
// An EXPLICIT colour outranks the selection tint. m_body_selected is a
// document-wide flag raised whenever a non-Sketch feature row is selected —
// the normal resting state after any modelling operation — so painting every
// body gold on it made the Color tool look broken: the override was written,
// carried across recompute and read back correctly, and then overpainted here
// every single frame. A body the user deliberately coloured keeps its colour;
// the rest still tint, which is all the tint was ever for.
const bool overridden = m_color_bodies != nullptr && b >= 0
&& b < int(m_color_bodies->size())
&& (*m_color_bodies)[b].has_color;
ColorRGBA c = (m_body_selected && !overridden) ? sel_gold : body_color(b);
ColorRGBA c = lit ? design_selection_color() : body_color(b);
if (b == m_hl_body_target) c = ColorRGBA(0.30f, 0.90f, 0.70f, 1.0f); // target = teal-green
else if (b == m_hl_body_tool) c = ColorRGBA(1.00f, 0.55f, 0.15f, 1.0f); // tool = orange
if (m_body_translucent) c.a(0.30f);
@@ -385,45 +378,23 @@ void DesignCanvas::reload(bool keep_view)
m_canvas_widget->Refresh();
}
void DesignCanvas::set_mesh(const TriangleMesh& mesh)
{
if (m_model.objects.empty()) {
auto* obj = m_model.add_object();
obj->add_volume(mesh);
obj->add_instance();
} else {
ModelObject* obj = m_model.objects.front();
obj->clear_volumes();
obj->add_volume(mesh);
if (obj->instances.empty())
obj->add_instance();
}
reload(!m_first_frame);
}
void DesignCanvas::set_bodies(const std::vector<TriangleMesh>& body_meshes,
void DesignCanvas::set_bodies(const std::vector<TriangleMesh>* body_meshes,
const std::vector<bool>& visible)
{
// Object 0 carries one GLVolume per body so reload() can colour each distinctly.
// Falls back to a single-volume object when there's only one body (identical look
// to the old set_mesh path). Picking still uses the combined mesh via set_solid_pick.
// Object 0 is built by rebuild_bodies; picking uses the combined mesh from set_solid_pick.
m_body_visible = visible; // empty => all visible; reload() reads this per volume
if (body_meshes.empty()) { clear_mesh(); return; }
ModelObject* obj = m_model.objects.empty() ? m_model.add_object()
: m_model.objects.front();
obj->clear_volumes();
for (const TriangleMesh& m : body_meshes)
obj->add_volume(m);
if (obj->instances.empty())
obj->add_instance();
if (body_meshes == nullptr || body_meshes->empty()) { clear_mesh(); return; }
m_body_meshes = body_meshes;
m_lit_faces = m_sketch_tool.selected_faces();
rebuild_bodies();
reload(!m_first_frame);
}
void DesignCanvas::clear_mesh()
{
m_body_meshes = nullptr;
m_volumes.clear();
if (!m_model.objects.empty()) {
m_model.delete_object((size_t)0);
reload(true);
@@ -682,6 +653,7 @@ void DesignCanvas::clear_loop_pick()
void DesignCanvas::clear_solid_pick()
{
m_sketch_tool.clear_solid_selection();
sync_selected_faces();
}
void DesignCanvas::set_loop_pick(int feature, int region)
@@ -711,7 +683,9 @@ void DesignCanvas::set_solid_pick(const std::vector<CadBody>* bodies, const Tria
const std::vector<Transform3d>* xform)
{
m_color_bodies = bodies; // stable address (m_doc.bodies); reload() reads colour overrides
m_tri_face = tri_face;
m_sketch_tool.set_solid_pick(bodies, mesh, tri_face, tri_body, visible, xform);
sync_selected_faces(); // the tool just reset its pick
}
// Effective display colour for a body: per-body override (Color tool) when set, else the
@@ -914,12 +888,19 @@ std::vector<int> DesignCanvas::selected_solid_edges() const
void DesignCanvas::set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb)
{
m_sketch_tool.on_solid_selection_changed = std::move(cb);
// Wrapped so every pick change in the tool also re-splits the filled faces.
m_on_solid_selection_changed = std::move(cb);
m_sketch_tool.on_solid_selection_changed = [this](int level, int body, int face, int edge) {
sync_selected_faces();
if (m_on_solid_selection_changed)
m_on_solid_selection_changed(level, body, face, edge);
};
}
void DesignCanvas::select_body(int body)
{
m_sketch_tool.select_body(body);
sync_selected_faces();
request_repaint();
}
@@ -1211,11 +1192,75 @@ void DesignCanvas::render_hud()
chip("##design_readout", m_hud_last, ds.x - margin, 1.f, 0.f, &ImGuiWrapper::COL_ORCA);
}
void DesignCanvas::set_body_highlight(bool on)
void DesignCanvas::set_highlight_faces(const std::vector<std::pair<int, int>>& faces)
{
if (m_body_selected == on) return;
m_body_selected = on;
reload(true); // recolours the body volume (selected = cyan tint)
m_sketch_tool.set_highlight_faces(faces);
sync_selected_faces();
request_repaint();
}
void DesignCanvas::sync_selected_faces()
{
// Deferred to the end of the current event, which may change the selection several times:
// every re-split reloads all the bodies. A set_bodies in between splits by the current
// selection itself and leaves this nothing to do.
if (m_split_pending)
return;
m_split_pending = true;
CallAfter([this] {
m_split_pending = false;
std::vector<std::pair<int, int>> want = m_sketch_tool.selected_faces();
if (want == m_lit_faces)
return;
m_lit_faces = std::move(want);
if (m_body_meshes == nullptr || m_model.objects.empty())
return;
rebuild_bodies();
reload(true);
});
}
// Object 0: one volume per body, with its selected faces split off into a volume of their own.
// The per-triangle face ids (all bodies, in order) match triangles to faces; when their count
// does not match the meshes, nothing is split.
void DesignCanvas::rebuild_bodies()
{
ModelObject* obj = m_model.objects.empty() ? m_model.add_object() : m_model.objects.front();
obj->clear_volumes();
m_volumes.clear();
const std::vector<TriangleMesh>& meshes = *m_body_meshes;
size_t ntri = 0;
for (const TriangleMesh& m : meshes)
ntri += m.its.indices.size();
const bool mapped = m_tri_face != nullptr && m_tri_face->size() == ntri;
size_t off = 0;
for (int b = 0; b < int(meshes.size()); ++b) {
const TriangleMesh& mesh = meshes[b];
const auto first = std::lower_bound(m_lit_faces.begin(), m_lit_faces.end(), std::make_pair(b, INT_MIN));
const auto last = std::lower_bound(first, m_lit_faces.end(), std::make_pair(b + 1, INT_MIN));
if (!mapped || first == last) {
obj->add_volume(mesh);
m_volumes.push_back({ b, false });
} else {
indexed_triangle_set parts[2]; // [0] the rest of the body, [1] its selected faces
for (size_t i = 0; i < mesh.its.indices.size(); ++i) {
const int f = (*m_tri_face)[off + i];
const bool lit = std::binary_search(first, last, std::make_pair(b, f));
parts[lit].indices.push_back(mesh.its.indices[i]);
}
for (int lit = 0; lit < 2; ++lit) {
if (parts[lit].indices.empty())
continue;
parts[lit].vertices = mesh.its.vertices;
its_compactify_vertices(parts[lit]);
obj->add_volume(TriangleMesh(std::move(parts[lit])));
m_volumes.push_back({ b, lit == 1 });
}
}
off += mesh.its.indices.size();
}
if (obj->instances.empty())
obj->add_instance();
}
void DesignCanvas::set_operand_bodies(int target_body, int tool_body)
@@ -1223,7 +1268,7 @@ void DesignCanvas::set_operand_bodies(int target_body, int tool_body)
if (m_hl_body_target == target_body && m_hl_body_tool == tool_body) return;
m_hl_body_target = target_body;
m_hl_body_tool = tool_body;
reload(true); // recolours the body volumes (same idiom set_body_highlight uses)
reload(true); // recolours the body volumes
}
void DesignCanvas::set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl)
+20 -5
View File
@@ -41,10 +41,10 @@ public:
explicit DesignCanvas(wxWindow* parent);
~DesignCanvas() override;
void set_mesh(const TriangleMesh& mesh);
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
// `visible` (optional, indexed by body) hides bodies whose flag is false.
void set_bodies(const std::vector<TriangleMesh>& body_meshes,
// `visible` (optional, indexed by body) hides bodies whose flag is false. `body_meshes` is kept
// by address (a stable panel member) and read again whenever the selection changes.
void set_bodies(const std::vector<TriangleMesh>* body_meshes,
const std::vector<bool>& visible = {});
void clear_mesh();
@@ -239,7 +239,9 @@ public:
// Mate connectors, drawn as frames so their verse and polarity are visible (wgsc).
void set_mate_connectors(std::vector<DesignSketchTool::MateConnectorGlyph> g);
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> l);
void set_body_highlight(bool on); // tint the solid when its feature is tree-selected
// Draw these (body, face id) faces as selected: the faces the Feature tree's selected feature
// made (CadDocument::faces_made_by). Empty clears them.
void set_highlight_faces(const std::vector<std::pair<int, int>>& faces);
// The status line, shown along the BASE OF THE VIEWPORT rather than in the side panel:
// the panel clips it at ~73 characters with no warning (8cc), the viewport's
// bottom margin has the whole window width to spare. Empty text hides it.
@@ -375,6 +377,20 @@ private:
void reload(bool keep_view);
void swap_camera(); // enter_viewport / leave_viewport, in the one direction they share
// Selected faces are filled by the canvas: each body's selected faces become a volume of their
// own, drawn opaque in the selection colour with the body's shader and lighting, so a selection
// is the same colour on every body. The faces are the sketch tool's selected_faces() (the
// Feature tree row's and the committed face or body pick), which the tool outlines.
void rebuild_bodies(); // object 0 from m_body_meshes, split by m_lit_faces
void sync_selected_faces(); // re-split and reload, once queued, if the selection changed
struct BodyVolume { int body; bool lit; }; // an object 0 volume: its body, and whether it holds selected faces
const std::vector<TriangleMesh>* m_body_meshes{nullptr}; // set_bodies
const std::vector<int>* m_tri_face{nullptr}; // per-triangle face id, all bodies in order
std::vector<std::pair<int, int>> m_lit_faces; // what object 0 is split by now
std::vector<BodyVolume> m_volumes; // object 0's volumes, in order
bool m_split_pending{false};
std::function<void(int, int, int, int)> m_on_solid_selection_changed;
wxGLCanvas* m_canvas_widget{nullptr};
GLCanvas3D* m_canvas{nullptr};
int m_sw_gl{-1}; // -1 unknown, 0 hardware GL, 1 software GL
@@ -394,7 +410,6 @@ private:
bool m_camera_swapped{false}; // guards a leave without an enter, and the reverse
Model m_model;
bool m_first_frame{true};
bool m_body_selected{false}; // tree selected a body feature → tint the solid
int m_hl_body_target{-1};
int m_hl_body_tool{-1};
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
+67 -14
View File
@@ -3093,9 +3093,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_tree->SetBackgroundColour(dp_panel_bg());
tree_inner->Add(m_tree, 0, wxEXPAND | wxALL, 12);
// Selecting a body-producing feature (Extrude/Fillet/Chamfer/Hole/Thread) in the
// tree highlights the solid in the viewport; a Sketch row clears the highlight
// (its face is already shown via the persistent sketch overlay).
// Selecting a feature that leaves a body (Extrude, Fillet, Chamfer, Hole, ...) lights the
// faces it made in the viewport — the fillet's round, not the whole part it sits on.
m_tree->on_select = [this] {
if (!m_viewport) return;
// Picking a feature drops any body selection, so the two lists never both claim to be
@@ -3104,10 +3103,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
// THIS handler, which would otherwise clear the body row the user had just clicked.
if (m_parts && tree_selection() != wxNOT_FOUND) m_parts->unselect();
const int sel = tree_selection();
const bool body = (sel >= 0 && sel < int(m_doc.features.size()) &&
m_doc.features[sel].type != CadFeatureType::Sketch &&
!m_doc.body.IsNull());
m_viewport->set_body_highlight(body);
// Likewise a viewport pick, which would be drawn just like the feature's faces. Not while
// a card is open: the card reads that pick.
if (sel != wxNOT_FOUND && m_active == Tool::None)
drop_solid_pick(); // not the loop pick: clicking a sketch loop selects its row
request_feature_highlight();
// A conflicting mate says WHY on selection, and names the one action that resolves it.
// Suppress is the generic per-feature enable toggle (the eye), so the answer is already
// one click away on a row the user has just selected — the message points at it instead
@@ -3268,7 +3268,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
// One selection at a time: a body row and a feature row mean different things to the
// op bar, so clear the feature tree's highlight when a body takes over.
if (m_tree) m_tree->unselect();
m_viewport->set_body_highlight(false); // the per-body overlay does the tint
m_viewport->select_body(b); // also drops the vertex/edge marker
m_sel_solid_body = b;
m_sel_solid_face = m_sel_solid_edge = -1;
@@ -3623,9 +3622,12 @@ DesignPanel::DesignPanel(wxWindow* parent)
// has focus; we lay the selected body face on the bed. Returns false when no face is
// selected so the key can fall through to the default handler.
// Clicking a solid cycles whole -> face -> edge. The tool draws the cyan overlay for ALL
// levels now (per-body, so other bodies stay untinted) — no whole-compound set_body_highlight.
// Clicking a solid cycles whole -> face -> edge. The tool draws the selection for every
// level, per body, so other bodies keep their own look.
m_viewport->set_on_solid_selection_changed([this](int level, int body, int face, int edge) {
// One selection at a time: a pick replaces the Feature tree row. Not while a card is open:
// the feature being edited keeps its row while the card's picks are made.
if (level >= 1 && m_active == Tool::None && m_tree) m_tree->unselect();
// A pick that fell through the move gizmo (clicked off the arrows) exits move mode.
if (m_viewport->moving_body()) m_viewport->clear_move_gizmo();
// Remember which body + face/edge so Extrude / dress-up target the RIGHT body.
@@ -7462,6 +7464,7 @@ void DesignPanel::refresh_tree()
m_tree->set_rows(std::move(rows));
refresh_parts(); // bodies live in their own list below the tree, never clipped by history
m_tree->select(keep);
request_feature_highlight(); // set_rows and select() say nothing when `keep` is gone
if (m_form && m_form->GetSizer()) { update_cards_frame(); m_form->Layout(); m_form->FitInside(); }
}
@@ -7610,7 +7613,50 @@ void DesignPanel::feed_bodies()
// place), so it needs no re-call here — the whole/face/edge selection survives a move drag.
if (m_viewport == nullptr) return;
rebuild_disp_meshes();
m_viewport->set_bodies(m_disp_body_meshes, m_body_visible);
m_viewport->set_bodies(&m_disp_body_meshes, m_body_visible);
// A new topology may renumber faces, so the feature's faces are found again; until then the
// viewport drops those on a body whose shape has changed.
if (m_hl_generation != m_doc.topo_generation)
request_feature_highlight();
}
void DesignPanel::request_feature_highlight()
{
if (m_hl_pending) return;
m_hl_pending = true;
// Not on the stack of the click or the rebuild that asked: finding the faces may put up the
// busy dialog, and the document must have settled.
CallAfter([this] { update_feature_highlight(); });
}
void DesignPanel::update_feature_highlight()
{
m_hl_pending = false;
if (m_viewport == nullptr) return;
// A rebuild's busy loop runs queued events while its worker owns the document. Skipped, not
// re-queued (the loop would run it again at once and spin): the rebuild's own refresh
// (feed_bodies, refresh_tree) asks again when it is done.
if (s_doc_worker_busy.load() > 0) return;
const int sel = tree_selection();
// Hidden while a feature card is open: the card's ghost and picks are what the view is about.
const bool wanted = sel >= 0 && sel < int(m_doc.features.size()) && m_active == Tool::None;
if (wanted && (sel != m_hl_feature || m_hl_generation != m_doc.topo_generation)) {
std::vector<std::pair<int, int>> faces;
const CadFeature& f = m_doc.features[sel];
if (f.enabled && CadDocument::produces_body(f.type)) // the rest make no faces
run_off_ui_thread(this, _L("Finding the feature's faces…"), [this, sel, &faces] {
try {
faces = m_doc.faces_made_by(sel);
} catch (...) {
faces.clear(); // a highlight is not worth an escaped exception
}
});
m_hl_faces = std::move(faces);
m_hl_feature = sel;
m_hl_generation = m_doc.topo_generation;
}
// Re-sending the same faces is cheap: the viewport reuses what it has.
m_viewport->set_highlight_faces(wanted ? m_hl_faces : std::vector<std::pair<int, int>>{});
}
// Boolean (combine bodies) — one gate for every door onto the tool. A body-body operation
@@ -7774,15 +7820,20 @@ void DesignPanel::set_tree_selection(int row)
// delete, reorder) drops it, the viewport's highlights with it. The solid highlight too: a rebuild
// that leaves no body never reaches set_solid_pick. The callers repaint.
void DesignPanel::drop_selection()
{
if (m_viewport != nullptr)
m_viewport->clear_loop_pick();
drop_solid_pick();
}
void DesignPanel::drop_solid_pick()
{
m_sel_solid_body = m_sel_solid_face = m_sel_solid_edge = -1;
m_sel_solid_edges.clear();
m_sel_solid_vertex = false;
m_pick_face = m_pick_face_body = -1;
if (m_viewport != nullptr) {
m_viewport->clear_loop_pick();
if (m_viewport != nullptr)
m_viewport->clear_solid_pick();
}
}
// The shared front of delete and reorder, which renumber the feature list. A sketch or constrain
@@ -11417,6 +11468,7 @@ void DesignPanel::open_tool(Tool t)
// the card appears always means "keep this one" regardless of what the last session did.
if (t == Tool::Boolean)
m_bool_next_slot = 0;
request_feature_highlight(); // the card's ghost and picks take the view over
}
void DesignPanel::close_tool()
@@ -11486,6 +11538,7 @@ void DesignPanel::close_tool()
update_cards_frame(); m_form->Layout();
m_form->FitInside();
update_action_bar(); // no feature tool active -> hide the bar (unless a mode keeps it)
request_feature_highlight(); // the selected row's faces come back with the card gone
}
void DesignPanel::confirm_tool()
+11
View File
@@ -294,6 +294,7 @@ private:
void on_edit_feature(); // reopen the selected feature's dialog populated
void after_tree_edit(bool ok); // shared post-op refresh of tree/viewport/status
void drop_selection(); // forget the selection (solid, hit face, sketch loop): the feature list was replaced or renumbered
void drop_solid_pick(); // the solid and hit-face part of drop_selection
bool begin_renumber(); // before delete/reorder: refuse while an index is held, else close the card, checkpoint, drop picks
void load_feature_into_dialog(const CadFeature& f);
void reset_edit_state(); // back to add-mode (m_edit_index = -1)
@@ -905,6 +906,16 @@ private:
// own Edit / Show-hide / Delete icons. Callers use row indices via
// tree_selection()/set_tree_selection(); refresh_tree() rebuilds the rows.
DesignRowList* m_tree{nullptr};
// The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected.
// Finding them replays the history, so they are kept per row and topology generation.
// request_feature_highlight() refreshes them after the current event; every change of row,
// card or topology calls it.
int m_hl_feature{-1}; // the row m_hl_faces were found for...
uint64_t m_hl_generation{0}; // ...on this topology
std::vector<std::pair<int, int>> m_hl_faces;
bool m_hl_pending{false};
void request_feature_highlight();
void update_feature_highlight();
// Bodies list under the feature tree: one row per body (parallel to m_doc.bodies). Selecting
// one highlights that body and makes it the target for the next op.
DesignRowList* m_parts{nullptr};
+220 -96
View File
@@ -21,8 +21,11 @@
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <BRepBndLib.hxx>
#include <BRep_Builder.hxx>
#include <Bnd_Box.hxx>
#include <Standard_Failure.hxx>
#include <TopoDS_Compound.hxx>
#include <map>
#include "libslic3r/TriangleMesh.hpp"
#include <GL/glew.h>
@@ -3450,50 +3453,105 @@ void DesignSketchTool::set_solid_pick(const std::vector<CadBody>* bodies, const
clear_solid_selection();
}
// The chord tolerance body edges are sampled at: a thousandth of the body's size, so round edges
// stay round at any zoom that shows the whole body, without sampling a large import into millions
// of segments.
static double display_edge_tol(const TopoDS_Shape& shape)
{
Bnd_Box box;
BRepBndLib::Add(shape, box);
return std::max(1e-3 * (box.IsVoid() ? 1.0 : std::sqrt(box.SquareExtent())), 0.005);
}
void DesignSketchTool::refresh_body_edges()
{
const size_t n = m_solid_bodies != nullptr ? m_solid_bodies->size() : 0;
m_body_edges.resize(n);
m_body_edges_key.resize(n, nullptr);
m_body_edges_tol.resize(n, 0.0);
for (size_t b = 0; b < n; ++b) {
const TopoDS_Shape& shape = (*m_solid_bodies)[b].shape;
const void* key = shape.IsNull() ? nullptr : shape.TShape().get();
const void* key = body_key(int(b));
if (key == m_body_edges_key[b] && key != nullptr)
continue;
m_body_edges_key[b] = key;
m_body_edges[b].clear();
if (key == nullptr)
continue;
// A thousandth of the body's size: round edges stay round at any zoom that shows the
// whole body, without sampling a large import into millions of segments.
Bnd_Box box;
BRepBndLib::Add(shape, box);
const double diag = box.IsVoid() ? 1.0 : std::sqrt(box.SquareExtent());
const TopoDS_Shape& shape = (*m_solid_bodies)[b].shape;
m_body_edges_tol[b] = display_edge_tol(shape);
try {
m_body_edges[b] = GeometryEngine::display_edges(shape, std::max(1e-3 * diag, 0.005));
m_body_edges[b] = GeometryEngine::display_edges(shape, m_body_edges_tol[b]);
} catch (const Standard_Failure&) {
m_body_edges[b].clear(); // an unsampleable edge costs its body the lines, nothing else
}
}
}
// The identity of a body's current shape. Face ids and sampled edges hold while it does.
const void* DesignSketchTool::body_key(int body) const
{
if (m_solid_bodies == nullptr || body < 0 || body >= int(m_solid_bodies->size()))
return nullptr;
const TopoDS_Shape& shape = (*m_solid_bodies)[body].shape;
return shape.IsNull() ? nullptr : shape.TShape().get();
}
// View-facing ribbons along a body's polylines (shape coordinates), `hw` either side of the line
// and moved by `pull` toward the eye, so a line wins the depth test against the faces meeting at
// it while a face in front of it still hides it.
void DesignSketchTool::append_ribbons(GLModel::Geometry& g, int body, const std::vector<std::vector<Vec3d>>& polylines,
const Vec3d& vd, const Vec3d& pull, double hw) const
{
g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
unsigned int base = unsigned(g.vertices_count());
for (const std::vector<Vec3d>& pl : polylines)
for (size_t s = 1; s < pl.size(); ++s) {
const Vec3d a = body_xform_pt(body, pl[s - 1]) + pull, c = body_xform_pt(body, pl[s]) + pull;
Vec3d dir = c - a; if (dir.norm() < 1e-9) continue; dir.normalize();
Vec3d off = dir.cross(vd);
if (off.norm() < 1e-9) continue; // edge seen end-on: a point, nothing to draw
off = off.normalized() * hw;
g.add_vertex((Vec3f)(a + off).cast<float>());
g.add_vertex((Vec3f)(c + off).cast<float>());
g.add_vertex((Vec3f)(c - off).cast<float>());
g.add_vertex((Vec3f)(a - off).cast<float>());
g.add_triangle(base, base + 1, base + 2);
g.add_triangle(base, base + 2, base + 3); base += 4;
}
}
// Ribbons tested against the scene's depth without writing it, and blended.
static void render_ribbons(GLModel& model, GLModel::Geometry&& g, const ColorRGBA& colour)
{
if (g.is_empty())
return;
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthFunc(GL_LEQUAL));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
model.reset();
model.init_from(std::move(g));
model.set_color(colour);
model.render();
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDepthFunc(GL_LESS));
glsafe(::glDisable(GL_BLEND));
glsafe(::glDisable(GL_DEPTH_TEST));
}
void DesignSketchTool::render_body_edges()
{
if (m_body_edges_hidden || m_solid_bodies == nullptr)
return;
using EPT = GLModel::Geometry::EPrimitiveType;
using EVL = GLModel::Geometry::EVertexLayout;
const Camera& cam = wxGetApp().plater()->get_camera();
const Vec3d vd = cam.get_dir_forward();
const double px = 1.0 / std::max(cam.get_zoom(), 1e-6);
const double hw = 1.0 * px; // ~2 px wide: at 1.5 the lines read as hairlines
// Pulled toward the eye by a few pixels, so the line wins the depth test against the two
// faces meeting at the edge while a face in front of it still hides it.
const Vec3d pull = -vd * (3.0 * px);
// Two passes: the edges of a body faded by body focus are fainter, like the body itself.
for (int pass = 0; pass < 2; ++pass) {
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
unsigned int base = 0;
GLModel::Geometry g;
for (int b = 0; b < int(m_body_edges.size()); ++b) {
if (m_solid_visible != nullptr && b < int(m_solid_visible->size()) && !(*m_solid_visible)[b])
continue;
@@ -3501,36 +3559,9 @@ void DesignSketchTool::render_body_edges()
&& b != m_pick_only_body;
if (faded != (pass == 1))
continue;
for (const std::vector<Vec3d>& pl : m_body_edges[b])
for (size_t s = 1; s < pl.size(); ++s) {
const Vec3d a = body_xform_pt(b, pl[s - 1]) + pull, c = body_xform_pt(b, pl[s]) + pull;
Vec3d dir = c - a; if (dir.norm() < 1e-9) continue; dir.normalize();
Vec3d off = dir.cross(vd);
if (off.norm() < 1e-9) continue; // edge seen end-on: a point, nothing to draw
off = off.normalized() * hw;
g.add_vertex((Vec3f)(a + off).cast<float>());
g.add_vertex((Vec3f)(c + off).cast<float>());
g.add_vertex((Vec3f)(c - off).cast<float>());
g.add_vertex((Vec3f)(a - off).cast<float>());
g.add_triangle(base, base + 1, base + 2);
g.add_triangle(base, base + 2, base + 3); base += 4;
}
append_ribbons(g, b, m_body_edges[b], vd, pull, hw);
}
if (base == 0)
continue;
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthFunc(GL_LEQUAL));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
m_body_edges_model.reset();
m_body_edges_model.init_from(std::move(g));
m_body_edges_model.set_color(ColorRGBA(0.08f, 0.09f, 0.11f, pass == 0 ? 0.85f : 0.25f));
m_body_edges_model.render();
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDepthFunc(GL_LESS));
glsafe(::glDisable(GL_BLEND));
glsafe(::glDisable(GL_DEPTH_TEST));
render_ribbons(m_body_edges_model, std::move(g), ColorRGBA(0.08f, 0.09f, 0.11f, pass == 0 ? 0.85f : 0.25f));
}
}
@@ -3596,7 +3627,121 @@ void DesignSketchTool::select_body(int body)
m_sel_edge_pts.clear();
m_sel_edges_more.clear();
m_sel_edges_more_pts.clear();
m_solid_sel = SolidSel::Whole; // render_solid_highlight tints just this body
m_solid_sel = SolidSel::Whole; // the canvas fills just this body, render_solid_highlight outlines it
}
void DesignSketchTool::set_highlight_faces(const std::vector<std::pair<int, int>>& faces)
{
std::map<int, std::vector<int>> by_body;
for (const auto& [b, f] : faces)
if (body_key(b) != nullptr)
by_body[b].push_back(f);
// Reuse each body's entry, so re-sending the same faces resamples nothing.
std::vector<FaceHighlight> next;
for (auto& [b, fs] : by_body) {
FaceHighlight h;
for (FaceHighlight& old : m_hl_faces)
if (old.body == b)
h = std::move(old);
cached_face_highlight(h, b, std::move(fs));
next.push_back(std::move(h));
}
m_hl_faces = std::move(next);
}
DesignSketchTool::FaceHighlight DesignSketchTool::make_face_highlight(int body, std::vector<int> faces) const
{
FaceHighlight h;
h.body = body;
std::sort(faces.begin(), faces.end());
faces.erase(std::unique(faces.begin(), faces.end()), faces.end());
h.faces = std::move(faces);
h.key = body_key(body);
if (h.key == nullptr || h.faces.empty())
return h;
// Sampled at the body edges' tolerance; a whole body reuses its body edges.
const TopoDS_Shape& shape = (*m_solid_bodies)[body].shape;
const std::vector<TopoDS_Face> all = GeometryEngine::faces_of(shape);
const bool cached = body < int(m_body_edges_key.size()) && m_body_edges_key[body] == h.key;
if (cached && h.faces.size() == all.size() && h.faces.front() == 0 && h.faces.back() == int(all.size()) - 1) {
h.edges = m_body_edges[body];
return h;
}
TopoDS_Compound picked;
BRep_Builder builder;
builder.MakeCompound(picked);
for (int f : h.faces)
if (f >= 0 && f < int(all.size()))
builder.Add(picked, all[f]);
try {
h.edges = GeometryEngine::display_edges(picked, cached ? m_body_edges_tol[body] : display_edge_tol(shape));
} catch (const Standard_Failure&) {
h.edges.clear(); // an edge that cannot be sampled costs the outline, never the fill
}
return h;
}
std::vector<std::pair<int, int>> DesignSketchTool::picked_faces() const
{
std::vector<std::pair<int, int>> out;
if (body_key(m_sel_body) == nullptr)
return out;
if (m_solid_sel == SolidSel::Face && m_sel_face >= 0)
out.emplace_back(m_sel_body, m_sel_face);
else if (m_solid_sel == SolidSel::Whole)
for (int f = 0, n = GeometryEngine::face_count((*m_solid_bodies)[m_sel_body].shape); f < n; ++f)
out.emplace_back(m_sel_body, f);
return out;
}
std::vector<std::pair<int, int>> DesignSketchTool::selected_faces() const
{
std::vector<std::pair<int, int>> out = picked_faces();
for (const FaceHighlight& h : m_hl_faces)
if (h.key != nullptr && h.key == body_key(h.body))
for (int f : h.faces)
out.emplace_back(h.body, f);
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
const DesignSketchTool::FaceHighlight& DesignSketchTool::cached_face_highlight(FaceHighlight& cache, int body,
std::vector<int> faces) const
{
std::sort(faces.begin(), faces.end());
faces.erase(std::unique(faces.begin(), faces.end()), faces.end());
if (cache.body != body || cache.faces != faces || cache.key != body_key(body))
cache = make_face_highlight(body, std::move(faces));
return cache;
}
// The edges of selected faces, cased: a dark band under a selection-coloured line, so the
// selection's outline reads on a body of any colour, the selection colour included. The faces
// themselves are filled by the canvas (DesignCanvas::rebuild_bodies). `quiet` is the hover's
// version: the line alone, thinner and fainter.
void DesignSketchTool::render_face_outline(const FaceHighlight& h, bool quiet)
{
if (m_body_edges_hidden || h.key == nullptr || h.key != body_key(h.body) || !body_pickable(h.body))
return; // hidden, or the body was rebuilt and these face ids are stale
const Camera& cam = wxGetApp().plater()->get_camera();
// View-facing ribbons like the body edges, pulled a pixel further toward the eye so they win
// over the dark body edges along the same curves.
const Vec3d vd = cam.get_dir_forward();
const double px = 1.0 / std::max(cam.get_zoom(), 1e-6);
const Vec3d pull = -vd * (4.0 * px);
auto draw = [&](double hw_px, const ColorRGBA& colour) {
GLModel::Geometry g;
append_ribbons(g, h.body, h.edges, vd, pull, hw_px * px);
render_ribbons(m_solid_edge_model, std::move(g), colour);
};
if (quiet) {
draw(1.0, design_selection_color(0.8f));
} else {
draw(2.25, ColorRGBA(0.05f, 0.06f, 0.08f, 1.0f));
draw(1.25, design_selection_color());
}
}
// Pick tracing. Selection failures on a real desktop have repeatedly turned out to be an
@@ -3918,53 +4063,19 @@ bool DesignSketchTool::update_solid_hover(GLCanvas3D& canvas, const wxMouseEvent
return true;
}
// One highlight, drawn from explicit arguments rather than from the selection members, so the
// committed selection and the hover pre-highlight cannot drift apart in how they look. alpha_mul
// scales every layer at once: the pre-highlight is the same shape in the same place, quieter.
void DesignSketchTool::render_solid_sel(SolidSel kind, int body, int face,
const std::vector<Vec3d>& edge_pts,
const Vec3d& vertex_pt,
const ColorRGBA& rgb, float alpha_mul)
// An edge or vertex highlight, drawn from explicit arguments rather than from the selection
// members, so the committed selection and the hover pre-highlight cannot drift apart in how they
// look. Faces and bodies are filled by the canvas and outlined by render_face_outline.
void DesignSketchTool::render_solid_sel(SolidSel kind, const std::vector<Vec3d>& edge_pts,
const Vec3d& vertex_pt, const ColorRGBA& rgb)
{
using EPT = GLModel::Geometry::EPrimitiveType;
using EVL = GLModel::Geometry::EVertexLayout;
// Opaque: the edge ribbon and the vertex square render with GL_BLEND OFF, so an alpha below 1
// here would be silently ignored. Those two are quietened by a MUTED rgb from the caller
// instead; alpha_mul only reaches the face fill, which is the one layer that is blended.
// here would be silently ignored.
const ColorRGBA cyan(rgb.r(), rgb.g(), rgb.b(), 1.0f);
// Whole tints the picked BODY (all its triangles, lighter alpha); Face tints just the
// picked face on that body. Both filter by `body` so other bodies stay untinted.
if ((kind == SolidSel::Face || kind == SolidSel::Whole)
&& m_solid_mesh != nullptr && m_solid_tri_body != nullptr && body >= 0) {
const bool face_only = (kind == SolidSel::Face);
const indexed_triangle_set& its = m_solid_mesh->its;
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
unsigned int base = 0;
for (size_t i = 0; i < its.indices.size(); ++i) {
if (i >= m_solid_tri_body->size() || (*m_solid_tri_body)[i] != body) continue;
if (face_only && (m_solid_tri_face == nullptr || i >= m_solid_tri_face->size()
|| (*m_solid_tri_face)[i] != face)) continue;
const auto& idx = its.indices[i];
for (int j = 0; j < 3; ++j) g.add_vertex(its.vertices[idx(j)]);
g.add_triangle(base, base + 1, base + 2); base += 3;
}
if (base > 0) {
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.0f, -2.0f));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
m_solid_face_model.reset();
m_solid_face_model.init_from(std::move(g));
m_solid_face_model.set_color(ColorRGBA(rgb.r(), rgb.g(), rgb.b(),
(face_only ? 0.40f : 0.22f) * alpha_mul));
m_solid_face_model.render();
glsafe(::glDisable(GL_BLEND));
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
glsafe(::glDisable(GL_DEPTH_TEST));
}
} else if (kind == SolidSel::Edge && edge_pts.size() >= 2) {
if (kind == SolidSel::Edge && edge_pts.size() >= 2) {
const Camera& cam = wxGetApp().plater()->get_camera();
const Vec3d vd = cam.get_dir_forward();
const double hw = 2.0 / std::max(cam.get_zoom(), 1e-6); // ~2 px ribbon half-width
@@ -4016,13 +4127,16 @@ void DesignSketchTool::render_solid_sel(SolidSel kind, int body, int face,
}
}
// Cyan overlay for the picked face / edge / vertex, plus the quieter pre-highlight of whatever
// the pointer is currently over. Whole-solid tint is the panel's job (set_body_highlight).
// The faces of the Feature tree's selected feature, the picked body / face / edge / vertex, and
// the quieter pre-highlight of whatever the pointer is currently over.
// Called from render() while no sketch session is active.
void DesignSketchTool::render_solid_highlight()
{
const ColorRGBA sel_cyan = design_selection_color();
for (const FaceHighlight& h : m_hl_faces)
render_face_outline(h, false);
// The pre-highlight goes FIRST so the committed selection paints over it where the two
// overlap — what you HAVE outranks what you would get. Suppressed entirely when they are the
// same thing: two coats of the same colour on the same face reads as a rendering fault, and
@@ -4032,17 +4146,27 @@ void DesignSketchTool::render_solid_highlight()
: m_pre.kind == SolidSel::Edge ? m_pre.edge == m_sel_edge
: m_pre.kind == SolidSel::Face ? m_pre.face == m_sel_face
: true);
if (m_pre.kind != SolidSel::None && !pre_is_sel)
// Desaturated toward white rather than a second hue: a distinct colour would read as a
// distinct KIND of selection, when it is the same selection one moment earlier.
render_solid_sel(m_pre.kind, m_pre.body, m_pre.face, m_pre.edge_pts, m_pre.vertex_pt,
design_selection_color(), 0.45f); // hover = the same colour, quieter
if (m_pre.kind == SolidSel::Face && !pre_is_sel)
// A hovered face gets its outline alone: a promise, not a selection.
render_face_outline(cached_face_highlight(m_pre_hl, m_pre.body, { m_pre.face }), true);
else if (m_pre.kind != SolidSel::None && !pre_is_sel)
// The selection's own colour: a second hue would read as a distinct KIND of selection,
// when it is the same selection one moment earlier.
render_solid_sel(m_pre.kind, m_pre.edge_pts, m_pre.vertex_pt, sel_cyan);
render_solid_sel(m_solid_sel, m_sel_body, m_sel_face, m_sel_edge_pts, m_sel_vertex_pt,
sel_cyan, 1.0f);
// A picked body or face looks like the Feature tree's faces: the canvas fills it, this
// outlines it.
if (const std::vector<std::pair<int, int>> picked = picked_faces(); !picked.empty()) {
std::vector<int> faces;
for (const auto& bf : picked)
faces.push_back(bf.second);
render_face_outline(cached_face_highlight(m_sel_hl, m_sel_body, std::move(faces)), false);
return;
}
render_solid_sel(m_solid_sel, m_sel_edge_pts, m_sel_vertex_pt, sel_cyan);
if (m_solid_sel == SolidSel::Edge)
for (const std::vector<Vec3d>& pts : m_sel_edges_more_pts)
render_solid_sel(SolidSel::Edge, m_sel_body, -1, pts, Vec3d::Zero(), sel_cyan, 1.0f);
render_solid_sel(SolidSel::Edge, pts, Vec3d::Zero(), sel_cyan);
}
// Datum/reference planes (Plane feature) have no solid; draw each as a translucent indigo
@@ -9133,7 +9257,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
}
// Nothing else to draw when no live sketch session is active — except the solid
// face/edge highlight overlay (whole-solid tint is handled by set_body_highlight).
// selection and the Feature tree's faces (render_solid_highlight).
if (!m_active) {
render_datum_planes();
render_mate_connectors();
+32 -5
View File
@@ -215,6 +215,12 @@ public:
// Select a whole body by index (from the Parts list) — Whole-level highlight, no face/edge.
// body < 0 or out of range clears the selection.
void select_body(int body);
// Outline these (body, face id) faces as selected: the faces the Feature tree's selected
// feature made. A body whose shape changes later drops out, its face ids being stale. The
// canvas fills them (DesignCanvas::rebuild_bodies).
void set_highlight_faces(const std::vector<std::pair<int, int>>& faces);
// Every face drawn as selected, sorted: the committed pick's and the still-valid ones above.
std::vector<std::pair<int, int>> selected_faces() const;
// Move-body gizmo (M5): translate a whole body with three world-axis drag arrows
// (X red / Y green / Z blue) anchored at the body centroid. Display-only — the host
@@ -1219,9 +1225,13 @@ private:
// whose shape changed (keyed by the TShape), since set_solid_pick runs on every recompute.
std::vector<std::vector<std::vector<Vec3d>>> m_body_edges;
std::vector<const void*> m_body_edges_key;
std::vector<double> m_body_edges_tol; // the chord tolerance they were sampled at
bool m_body_edges_hidden{false};
void refresh_body_edges();
void render_body_edges();
const void* body_key(int body) const; // the body's TShape, nullptr when there is none
void append_ribbons(GLModel::Geometry& g, int body, const std::vector<std::vector<Vec3d>>& polylines,
const Vec3d& vd, const Vec3d& pull, double hw) const;
bool body_pickable(int b) const; // false when the body is explicitly hidden
SolidSel m_solid_sel{SolidSel::None};
int m_sel_body{-1}; // which body the face/edge selection is on
@@ -1264,10 +1274,28 @@ private:
bool m_right_consumed{false}; // last RightDown was a gesture terminator, not a menu
bool m_escalate_repick{true}; // re-picking the same sub-element takes the whole body
void render_solid_highlight();
// The shared body of the above: one highlight from explicit arguments, so the committed
// selection and the hover pre-highlight cannot drift apart in how they look.
void render_solid_sel(SolidSel kind, int body, int face, const std::vector<Vec3d>& edge_pts,
const Vec3d& vertex_pt, const ColorRGBA& rgb, float alpha_mul);
// The above's edge and vertex highlight, from explicit arguments, so the committed selection
// and the hover pre-highlight cannot drift apart in how they look.
void render_solid_sel(SolidSel kind, const std::vector<Vec3d>& edge_pts, const Vec3d& vertex_pt,
const ColorRGBA& rgb);
// A set of selected faces of one body, with their edges sampled once, keyed by the body's
// TShape so a recompute that rebuilt the body retires it.
struct FaceHighlight {
int body{-1};
std::vector<int> faces; // sorted
const void* key{nullptr};
std::vector<std::vector<Vec3d>> edges; // in the body's shape coordinates
};
FaceHighlight make_face_highlight(int body, std::vector<int> faces) const;
// The faces the committed pick names: the one face of a face pick, every face of a picked
// body. Empty for an edge or vertex pick.
std::vector<std::pair<int, int>> picked_faces() const;
// `cache`, rebuilt only when it no longer holds these faces of this body's current shape.
const FaceHighlight& cached_face_highlight(FaceHighlight& cache, int body, std::vector<int> faces) const;
void render_face_outline(const FaceHighlight& h, bool quiet);
std::vector<FaceHighlight> m_hl_faces; // set_highlight_faces, one entry per body
FaceHighlight m_sel_hl; // the committed Face/Whole pick
FaceHighlight m_pre_hl; // the face under the pointer
void render_datum_planes(); // translucent rectangles for datum/reference planes
void render_view_helpers(); // world origin planes + axis triad (P / A toggles)
bool m_show_planes{false};
@@ -1284,7 +1312,6 @@ private:
std::vector<std::pair<Vec3d, Vec3d>> m_mate_links;
GLModel m_mc_stroke_model;
GLModel m_mc_fill_model; // the face treatment's shaded facets
GLModel m_solid_face_model;
GLModel m_solid_edge_model;
GLModel m_body_edges_model;
GLModel m_solid_vertex_model;
+165
View File
@@ -46,6 +46,7 @@
#include <Bnd_Box.hxx>
#include <BRepBndLib.hxx>
#include <BRepAdaptor_Curve.hxx>
#include <BRepAdaptor_Surface.hxx>
#include <TopExp_Explorer.hxx>
#include <TopoDS.hxx>
#include <BRepGProp.hxx>
@@ -9083,3 +9084,167 @@ TEST_CASE("revolve about a line of the sketch", "[CadDocument][revolve]")
CHECK(loaded.features[1].revolve_angle == Approx(270.));
}
}
// faces_made_by is what the Design tab highlights when a feature row is selected: the faces of
// the finished model that this feature made, not the whole body it belongs to.
static GeomAbs_SurfaceType made_face_type(const CadDocument& doc, const std::pair<int, int>& bf)
{
return BRepAdaptor_Surface(GeometryEngine::face_by_index(doc.bodies[bf.first].shape, bf.second)).GetType();
}
TEST_CASE("A fillet owns its round face, and the extrude keeps the faces it trimmed", "[CadDocument][highlight]")
{
CadDocument doc;
const int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
const int ex = doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
const int fi = doc.add_fillet(2.0, 0, "Fillet");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 1);
REQUIRE(GeometryEngine::faces_of(doc.bodies[0].shape).size() == 7);
const auto fillet = doc.faces_made_by(fi);
REQUIRE(fillet.size() == 1);
CHECK(fillet[0].first == 0);
CHECK(made_face_type(doc, fillet[0]) == GeomAbs_Cylinder);
const auto extrude = doc.faces_made_by(ex);
CHECK(extrude.size() == 6);
for (const auto& bf : extrude)
CHECK(made_face_type(doc, bf) == GeomAbs_Plane);
CHECK(doc.faces_made_by(sk).empty()); // a sketch leaves no face behind
// A hidden feature made nothing in the model on screen.
REQUIRE(doc.set_feature_enabled(fi, false));
CHECK(doc.faces_made_by(fi).empty());
}
TEST_CASE("A chamfer owns its bevel", "[CadDocument][highlight]")
{
CadDocument doc;
const int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
const int ex = doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
const int ch = doc.add_chamfer(2.0, 0, "Chamfer");
REQUIRE(doc.recompute());
REQUIRE(GeometryEngine::faces_of(doc.bodies[0].shape).size() == 7);
const auto bevel = doc.faces_made_by(ch);
REQUIRE(bevel.size() == 1);
CHECK(made_face_type(doc, bevel[0]) == GeomAbs_Plane);
const auto extrude = doc.faces_made_by(ex);
CHECK(extrude.size() == 6);
CHECK(std::find(extrude.begin(), extrude.end(), bevel[0]) == extrude.end());
}
TEST_CASE("A boss owns its wall and cap, not the face it stands on", "[CadDocument][highlight]")
{
CadDocument doc;
const int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 30, 20, 10, "Sketch");
const int ex = doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
SketchPlane top = SketchPlane::XY();
top.origin = Vec3d(0, 0, 10);
const int sk2 = doc.add_sketch_entities({circle_entity({0, 0}, 5.0)}, top, "Sketch2");
const int boss = doc.add_extrude(sk2, 8.0, false, BooleanMode::Add, "Boss");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 1);
const auto made = doc.faces_made_by(boss);
REQUIRE_FALSE(made.empty());
int walls = 0, caps = 0;
for (const auto& bf : made) {
const GeomAbs_SurfaceType t = made_face_type(doc, bf);
walls += t == GeomAbs_Cylinder;
// The cap is the plane at the boss's top, not the box's top face it stands on.
if (t == GeomAbs_Plane) {
++caps;
Bnd_Box bb;
BRepBndLib::Add(GeometryEngine::face_by_index(doc.bodies[0].shape, bf.second), bb);
double x0, y0, z0, x1, y1, z1;
bb.Get(x0, y0, z0, x1, y1, z1);
CHECK(z0 > 17.9);
}
}
CHECK(walls >= 1);
CHECK(caps == 1);
CHECK(doc.faces_made_by(ex).size() == 6); // the box keeps its top face, hole and all
}
TEST_CASE("A body stacked on another still owns the face they share", "[CadDocument][highlight]")
{
CadDocument doc;
const int sk0 = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch0");
doc.add_extrude(sk0, 10.0, false, BooleanMode::New, "Base");
SketchPlane top = SketchPlane::XY();
top.origin = Vec3d(0, 0, 10);
const int sk1 = doc.add_sketch(SketchShape::Rectangle, top, 10, 10, 5, "Sketch1");
const int ex1 = doc.add_extrude(sk1, 5.0, false, BooleanMode::New, "Block");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 2);
// The block's bottom lies in the base's top face, facing the other way: it is the block's.
const auto made = doc.faces_made_by(ex1);
CHECK(made.size() == 6);
for (const auto& bf : made)
CHECK(bf.first == 1);
}
TEST_CASE("A Boolean union, which makes no face of its own, answers with the body it changed", "[CadDocument][highlight]")
{
CadDocument doc;
const int sk0 = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch0");
doc.add_extrude(sk0, 10.0, false, BooleanMode::New, "Box0");
SketchPlane beside = SketchPlane::XY();
beside.origin = Vec3d(10, 5, 0);
const int sk1 = doc.add_sketch(SketchShape::Rectangle, beside, 20, 20, 10, "Sketch1");
doc.add_extrude(sk1, 10.0, false, BooleanMode::New, "Box1");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 2);
const int fuse = doc.add_boolean(BooleanMode::Add, 0, 1, false, 0.0, -1, -1, "Fuse");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 1);
const auto made = doc.faces_made_by(fuse);
CHECK(made.size() == GeometryEngine::faces_of(doc.bodies[0].shape).size());
}
TEST_CASE("A Boolean subtract owns the walls its tool cut", "[CadDocument][highlight]")
{
CadDocument doc;
const int sk0 = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch0");
doc.add_extrude(sk0, 10.0, false, BooleanMode::New, "Box0");
SketchPlane beside = SketchPlane::XY();
beside.origin = Vec3d(10, 5, 0);
const int sk1 = doc.add_sketch(SketchShape::Rectangle, beside, 20, 20, 10, "Sketch1");
doc.add_extrude(sk1, 10.0, false, BooleanMode::New, "Box1");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 2);
const int cut = doc.add_boolean(BooleanMode::Cut, 0, 1, false, 0.0, -1, -1, "Cut");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 1);
// The tool notches a corner out of the box: the two walls of the notch, the tool's faces
// turned inside out, are the subtract's; what is left of the box's own faces is not.
const auto made = doc.faces_made_by(cut);
CHECK(made.size() == 2);
for (const auto& bf : made)
CHECK(made_face_type(doc, bf) == GeomAbs_Plane);
}
TEST_CASE("A hole owns its bore", "[CadDocument][highlight]")
{
CadDocument doc;
const int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
const int ex = doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
const int hole = doc.add_hole(6.0, 20.0, true, 0.0, 0.0, SketchPlane::XY(), "Hole");
REQUIRE(doc.recompute());
const auto bore = doc.faces_made_by(hole);
REQUIRE_FALSE(bore.empty());
for (const auto& bf : bore)
CHECK(made_face_type(doc, bf) == GeomAbs_Cylinder);
CHECK(doc.faces_made_by(ex).size() == 6);
}