mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user