Merge branch 'main' into exturuder-tab-multi-switch

This commit is contained in:
SoftFever
2026-10-07 01:38:10 +08:00
committed by GitHub
101 changed files with 3121 additions and 655 deletions
+4
View File
@@ -370,6 +370,10 @@ void AppConfig::set_defaults()
// restores the conventional CAD representation for users who expect it (x0kd).
if (get("design_connector_face_glyph").empty())
set_bool("design_connector_face_glyph", true);
// Design tab: draw the printer bed and its plate grid (the Feature tree's Bed row).
if (get("design_show_bed").empty())
set_bool("design_show_bed", true);
#endif
//#ifdef SUPPORT_SHOW_HINTS
+10
View File
@@ -352,6 +352,16 @@ void FillSpiralInset::_fill_surface_single(const FillParams& params,
assert(params.use_arachne);
assert(this->print_config != nullptr && this->print_object_config != nullptr);
// Internal solid infill must not add Arachne's standalone thin walls: one of those walls can
// become the extra centre point after the spiral has finished. Top and bottom surfaces keep
// Arachne, including the centre plug handled by generate_spiral_insets().
if (params.extrusion_role == erSolidInfill) {
Polylines polylines;
this->_fill_surface_single(params, thickness_layers, direction, expolygon, polylines);
append(thick_polylines_out, to_thick_polylines(std::move(polylines), scaled<coord_t>(this->spacing)));
return;
}
// Only a solid surface is worth the variable width walls; a sparse one falls back to plain loops.
if (params.density <= 0.9999f || params.dont_adjust) {
Polylines polylines;
+144 -40
View File
@@ -61,6 +61,8 @@ public:
bool is_smaller_width_perimeter;
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth;
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
bool is_thin_wall_hole = false;
// Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector<PerimeterGeneratorLoop> children;
@@ -118,7 +120,7 @@ static bool detect_steep_overhang(const PrintRegionConfig *config,
}
static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perimeter_generator, const PerimeterGeneratorLoops &loops, ThickPolylines &thin_walls,
bool &steep_overhang_contour, bool &steep_overhang_hole, bool reverse_thin_wall_hole)
bool &steep_overhang_contour, bool &steep_overhang_hole)
{
// loops is an arrayref of ::Loop objects
// turn each one into an ExtrusionLoop object
@@ -285,24 +287,17 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
} else {
const PerimeterGeneratorLoop &loop = loops[idx.first];
assert(thin_walls.empty());
const bool reverse_children_thin_wall_hole = loops.size() == 1 && loop.is_contour && loop.children.size() == 1 &&
(!loop.children.front().is_contour) && loop.children.front().children.empty();
ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls, steep_overhang_contour,
steep_overhang_hole, reverse_children_thin_wall_hole);
ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls, steep_overhang_contour, steep_overhang_hole);
out.entities.reserve(out.entities.size() + children.entities.size() + 1);
ExtrusionLoop *eloop = static_cast<ExtrusionLoop*>(coll.entities[idx.first]);
coll.entities[idx.first] = nullptr;
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == (loop.is_contour || reverse_thin_wall_hole))
// Orca: holes run against the contour, except thin wall holes, which run with it.
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == (loop.is_contour || loop.is_thin_wall_hole))
eloop->make_counter_clockwise();
else
eloop->make_clockwise();
// Orca: Reverse print order for thin wall holes.
if (reverse_thin_wall_hole) {
std::reverse(out.entities.begin(), out.entities.end());
}
eloop->inset_idx = loop.depth;
if (loop.is_contour) {
out.append(std::move(children.entities));
@@ -759,6 +754,59 @@ static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &
}
}
// ORCA: only_one_wall_top - widest bead of the given walls.
static coord_t widest_bead(const std::vector<Arachne::VariableWidthLines> &walls)
{
coord_t widest = 0;
for (const Arachne::VariableWidthLines &group : walls)
for (const Arachne::ExtrusionLine &el : group)
for (const Arachne::ExtrusionJunction &j : el.junctions)
widest = std::max(widest, j.w);
return widest;
}
// ORCA: only_one_wall_top - length of the walls running further than tolerance from the reference walls, outside the
// excluded area.
static double length_off_reference(const std::vector<Arachne::VariableWidthLines> &walls, const Arachne::VariableWidthLines &reference,
const ExPolygons &excluded, coord_t tolerance)
{
auto append_centerlines = [](const Arachne::VariableWidthLines &lines, Polylines &out) {
for (const Arachne::ExtrusionLine &el : lines) {
if (el.junctions.size() < 2)
continue;
Polyline &centerline = out.emplace_back();
centerline.points.reserve(el.junctions.size());
for (const Arachne::ExtrusionJunction &j : el.junctions)
centerline.points.emplace_back(j.p);
}
};
Polylines wall_centerlines;
Polylines reference_centerlines;
for (const Arachne::VariableWidthLines &group : walls)
append_centerlines(group, wall_centerlines);
append_centerlines(reference, reference_centerlines);
Polylines off_reference = diff_pl(wall_centerlines, offset(reference_centerlines, float(tolerance)));
if (! excluded.empty())
off_reference = diff_pl(off_reference, excluded);
return total_length(off_reference);
}
// ORCA: only_one_wall_top - area covered by the given walls at their local widths.
static Polygons walls_footprint(const Arachne::VariableWidthLines &walls)
{
Polygons footprint;
for (const Arachne::ExtrusionLine &el : walls)
for (size_t i = 1; i < el.junctions.size(); ++ i) {
const Arachne::ExtrusionJunction &a = el.junctions[i - 1];
const Arachne::ExtrusionJunction &b = el.junctions[i];
const coord_t width = std::max(a.w, b.w);
if (width > 0)
append(footprint, offset(Polyline(a.p, b.p), float(width) / 2.f));
}
return union_(footprint);
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
ExPolygons &non_top_polygons, ExPolygons &fill_clip) const {
// other perimeters
@@ -1446,6 +1494,8 @@ void PerimeterGenerator::process_classic()
coord_t min_spacing = coord_t(perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE));
coord_t ext_min_spacing = coord_t(ext_perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE));
bool has_gap_fill = this->config->gap_infill_speed.get_at(get_extruder_index(*print_config, this->config->outer_wall_filament_id - 1)) > 0;
// ORCA: Use the smaller width as the lower bound to avoid overestimating safe overlap
const double gap_fill_min_width = 0.2 * std::min(perimeter_width, ext_perimeter_width) * (1 - INSET_OVERLAP_TOLERANCE);
// BBS: this flow is for smaller external perimeter for small area
coord_t ext_min_spacing_smaller = coord_t(ext_perimeter_spacing * (1 - SMALLER_EXT_INSET_OVERLAP_TOLERANCE));
@@ -1640,6 +1690,15 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets);
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
if (i == 0 && ! holes[0].empty()) {
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
for (PerimeterGeneratorLoop &hole : holes[0])
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
}
//BBS: refer to superslicer
//store surface for top infill if only_one_wall_top
if (i == 0 && i!=loop_number && only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
@@ -1766,7 +1825,7 @@ void PerimeterGenerator::process_classic()
steep_overhang_contour = true;
steep_overhang_hole = true;
}
ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole, false);
ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole);
// All walls are counter-clockwise initially, so we don't need to reorient it if that's what we want
if (config->overhang_reverse) {
reorient_perimeters(entities, steep_overhang_contour, steep_overhang_hole,
@@ -1893,8 +1952,7 @@ void PerimeterGenerator::process_classic()
// fill gaps
if (! gaps.empty()) { // collapse
// ORCA: Use the smaller width as the lower bound to avoid overestimating safe overlap
double min = 0.2 * std::min(perimeter_width, ext_perimeter_width) * (1 - INSET_OVERLAP_TOLERANCE);
double min = gap_fill_min_width;
double max = 2. * perimeter_spacing;
ExPolygons gaps_ex = diff_ex(
//FIXME offset2 would be enough and cheaper.
@@ -2546,44 +2604,90 @@ void PerimeterGenerator::process_arachne()
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Infill contour bounding box.
BoundingBox infill_contour_bbox = get_extents(infill_contour);
infill_contour_bbox.offset(SCALED_EPSILON);
coord_t perimeter_width = this->perimeter_flow.scaled_width();
// Get top ExPolygons from current infill contour.
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, infill_contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
// contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box.
BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON);
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty();
if (top.empty())
return top;
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset);
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons.
top_expolygons = diff_ex(top_expolygons, current_slices_bridges);
top = diff_ex(top, current_slices_bridges);
}
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
return intersection_ex(top, contour);
};
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
std::vector<Arachne::VariableWidthLines> full_perimeters;
Polygons full_inner_contour;
bool full_perimeters_generated = false;
auto generate_full_perimeters = [&]() {
if (full_perimeters_generated)
return;
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
full_perimeters = full_tool_paths.getToolPaths();
full_inner_contour = full_tool_paths.getInnerContour();
full_perimeters_generated = true;
};
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
// the width added, and only beads widened by more than twice the tolerance are looked for.
const coord_t outer_wall_tolerance = bead_width_0 / 10;
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
bool nominal_uncovered = false;
// Grown by an outer wall width to take in the outer walls bordering the top surface.
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
if (nominal_uncovered) {
generate_full_perimeters();
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
perimeters = { full_perimeters.front() };
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
}
}
}
bool uncovered = false;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
if (uncovered) {
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
@@ -2612,11 +2716,11 @@ void PerimeterGenerator::process_arachne()
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so we call Arachne again with parameters
// like when the single perimeter feature is disabled.
Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
perimeters = no_single_perimeter_tool_paths.getToolPaths();
infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour());
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
// feature is disabled.
generate_full_perimeters();
perimeters = std::move(full_perimeters);
infill_contour = union_ex(full_inner_contour);
}
}
//PS
+7
View File
@@ -310,6 +310,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
, force_native_color(false)
, force_neutral_color(false)
, force_sinking_contours(false)
, depth_bias(false)
, picking(false)
, tverts_range(0, size_t(-1))
{
@@ -1332,11 +1333,17 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
shader->set_uniform("projection_matrix", projection_matrix);
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
if (volume.first->depth_bias) {
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(1.0f, 1.0f));
}
//BBS: add outline related logic
if (volume.first->selected && shader_can_outline && GUI::wxGetApp().show_outline())
volume.first->render_with_outline(cnv_size);
else
volume.first->render();
if (volume.first->depth_bias)
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
#if ENABLE_ENVIRONMENT_MAP
if (use_environment_texture)
+4
View File
@@ -232,6 +232,10 @@ public:
bool force_neutral_color : 1;
// Whether or not to force rendering of sinking contours
bool force_sinking_contours : 1;
// Orca: draw this volume with a positive depth bias (glPolygonOffset, pushed away from the
// camera), so on a surface it shares with another volume the other volume wins the depth
// test instead of z-fighting it
bool depth_bias : 1;
// Is render for picking
bool picking : 1;
// slice error
+79 -48
View File
@@ -8,6 +8,7 @@
#include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/PartPlate.hpp" // the current plate's origin: the first view's offset
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLToolbar.hpp"
#include "slic3r/GUI/Event.hpp"
@@ -58,6 +59,16 @@ extern wxPopupWindow* wxCurrentPopupWindow;
namespace Slic3r {
namespace GUI {
// The Design tab's iso view is the CAD one: a true isometric from the front-right corner, which
// shows the navigator's Front, Right and Top faces. Camera's own "iso" looks from the front-left
// at 45 degrees and stays Prepare's and the thumbnails'.
static void select_iso_view(Camera& camera)
{
const Vec3d target = camera.get_target();
camera.look_at(target + camera.get_distance() * Vec3d(1., -1., 1.).normalized(), target, Vec3d::UnitZ());
camera.auto_type(Camera::EType::Perspective);
}
DesignCanvas::DesignCanvas(wxWindow* parent)
: wxPanel()
{
@@ -91,7 +102,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
m_canvas->enable_plate_chrome(false);
m_canvas->enable_labels(false);
m_canvas->enable_sinking_contours(false); // they would be sliced from the plater's meshes
m_canvas->set_axes_at_bed_center(true); // triad at bed centre = modeling origin
m_canvas->set_design_canvas(true); // home-position bed, triad and CAD grid at the modeling origin
m_canvas->set_design_sketch_tool(&m_sketch_tool);
m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) {
@@ -190,8 +201,13 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
refresh_bed();
// The view this canvas opens on. Built lazily, on the way into the Design tab, so this
// is the view the user is looking at right now.
// is the view the user is looking at right now — moved off the current plate onto the
// Design bed, which stays at the printer bed's home whichever plate is current — turned to
// the iso view Home returns to, keeping its target and zoom.
m_parked_camera = wxGetApp().plater()->get_camera();
if (PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate())
m_parked_camera.translate_world(-plate->get_origin());
select_iso_view(m_parked_camera);
// Before any of this class's own Binds below: wx calls dynamically bound handlers in
// reverse order of binding, and GLCanvas3D swallows several events without skipping them —
@@ -345,7 +361,7 @@ void DesignCanvas::repaint_now()
m_canvas_widget->Update(); // service the pending paint immediately (a modal popup owns the loop)
}
void DesignCanvas::reload(bool keep_view)
void DesignCanvas::reload()
{
m_canvas->reset_volumes();
@@ -378,21 +394,19 @@ void DesignCanvas::reload(bool keep_view)
v->set_color(c);
}
} else if (obj_idx == 1) {
// The ghost is normally a faint blue overlay on the visible body. In preview-only
// mode it IS the result (base bodies hidden), so render it opaque so it reads as a
// finished solid rather than a see-through hint.
// The ghost is the whole resulting model, normally drawn as a faint blue overlay on
// the visible bodies. Every face the feature leaves alone is in both, at the same
// depth, so the ghost is drawn with a depth bias: the bodies win on those faces instead
// of the two copies z-fighting, and the ghost shows only where the result reaches past
// the bodies. In preview-only mode it IS the result (base bodies hidden), so render it
// opaque so it reads as a finished solid rather than a see-through hint.
v->set_color(m_body_hidden ? ColorRGBA(0.40f, 0.82f, 1.0f, 1.0f) : ghost);
v->depth_bias = true;
}
}
if (!keep_view) {
if (m_first_frame && !m_model.objects.empty()) {
m_canvas->select_view("iso");
m_canvas->zoom_to_volumes();
m_first_frame = false;
}
}
// The camera stays where the user left it, even for the first body: Home and a double-click
// fit on demand.
m_canvas->set_as_dirty();
if (m_canvas_widget)
m_canvas_widget->Refresh();
@@ -406,18 +420,21 @@ void DesignCanvas::set_bodies(const std::vector<TriangleMesh>* body_meshes,
if (body_meshes == nullptr || body_meshes->empty()) { clear_mesh(); return; }
m_body_meshes = body_meshes;
m_sketch_tool.refresh_body_edges(); // of the bodies set_solid_pick() pointed the tool at
m_lit_faces = m_sketch_tool.selected_faces();
rebuild_bodies();
reload(!m_first_frame);
reload();
}
void DesignCanvas::clear_mesh()
{
m_body_meshes = nullptr;
m_volumes.clear();
// No solid, so no edge lines, pick or hover either, as after a rebuild that leaves no body.
m_sketch_tool.set_solid_pick(nullptr, nullptr, nullptr, nullptr);
if (!m_model.objects.empty()) {
m_model.delete_object((size_t)0);
reload(true);
reload();
}
}
@@ -431,14 +448,14 @@ void DesignCanvas::set_preview_mesh(const TriangleMesh& mesh)
obj->add_volume(mesh);
obj->add_instance();
reload(true);
reload();
}
void DesignCanvas::clear_preview()
{
if (m_model.objects.size() > 1) {
m_model.delete_object((size_t)1);
reload(true);
reload();
}
}
@@ -452,10 +469,23 @@ void DesignCanvas::fit_view()
}
}
bool DesignCanvas::zoom_to_box(BoundingBoxf3 box)
{
if (m_canvas == nullptr || !box.defined)
return false;
DesignSketchTool::pad_box(box);
m_canvas->zoom_to_box(box);
request_repaint();
return true;
}
void DesignCanvas::set_view(const std::string& view_name)
{
if (m_canvas) {
m_canvas->select_view(view_name);
if (view_name == "iso")
select_iso_view(wxGetApp().plater()->get_camera()); // the Design camera while the tab is shown
else
m_canvas->select_view(view_name);
m_canvas->zoom_to_volumes();
m_canvas->set_as_dirty();
if (m_canvas_widget)
@@ -470,13 +500,6 @@ void DesignCanvas::begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode
if (m_canvas_widget) m_canvas_widget->Refresh();
}
void DesignCanvas::set_sketch_plane(const SketchPlane& plane)
{
m_sketch_tool.set_plane(plane); // keeps the 2D entities; only the carrier plane changes
if (m_canvas) m_canvas->set_as_dirty();
if (m_canvas_widget) m_canvas_widget->Refresh();
}
void DesignCanvas::edit_sketch(const std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
const SketchPlane& plane)
@@ -550,6 +573,7 @@ void DesignCanvas::refresh_bed()
double printable_height = 100.0;
const auto* ph_opt = config->opt<ConfigOptionFloat>("printable_height");
if (ph_opt) printable_height = ph_opt->value;
// No position: the Design bed stays at the printer bed's home, whichever plate is current.
m_bed.set_shape(bed_shape_opt->values, printable_height, {}, {}, "", false); // mainline added extruder_areas/heights params
}
@@ -731,8 +755,6 @@ void DesignCanvas::clear_move_gizmo()
request_repaint();
}
bool DesignCanvas::moving_body() const { return m_sketch_tool.moving_body(); }
void DesignCanvas::set_on_body_move_changed(std::function<void(int, const Transform3d&)> cb)
{
m_sketch_tool.on_body_move_changed = std::move(cb);
@@ -1025,6 +1047,11 @@ void DesignCanvas::set_on_datum_base_picked(std::function<void(int)> cb)
m_sketch_tool.on_datum_base_picked = std::move(cb);
}
void DesignCanvas::set_selected_base(std::function<int()> cb)
{
m_sketch_tool.selected_base = std::move(cb);
}
void DesignCanvas::set_on_sketch_exit(std::function<void()> cb)
{
m_sketch_tool.on_exit = std::move(cb);
@@ -1047,15 +1074,18 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
return;
m_ctx_bound = true;
// Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event.
// It only consumes when it actually opens the offer; every other right-click still falls
// through to the polyline-chain end and the move gizmo, which were there first.
// Right-drag pans. Without remembering where the press landed, every pan ended by popping
// the offer over wherever the camera stopped — the menu appearing as the reward for moving
// the view. The offer is the release of a STATIONARY right-click (kCadRightClickDriftPx).
// It only consumes when it actually opens the offer.
// Right-drag may pan or orbit (Preferences > Control). Without remembering where the press
// landed, every such drag ended by popping the offer over wherever the camera stopped — the
// menu appearing as the reward for moving the view. A right-click is the release of a
// STATIONARY press (kCadRightClickDriftPx); only that reaches the sketch tool or the offer.
m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
m_ctx_press = e.GetPosition();
m_ctx_travelled = false;
e.Skip(); // the canvas still needs the press to seed the orbit
// Drop any press the tool still keeps (only a click's release takes it, so a pan's stays)
// before the canvas offers it this one, which ImGui may take instead.
m_sketch_tool.drop_right_click();
e.Skip(); // the canvas still needs the press to seed a pan or an orbit
});
m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
if (e.RightIsDown()) {
@@ -1066,12 +1096,13 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
});
m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) {
const wxPoint d = e.GetPosition() - m_ctx_press;
// Always read-and-clear, even when another guard already rules the offer out, or a
// terminator recorded under one condition would still be pending under the next.
const bool terminated = m_sketch_tool.take_right_consumed();
// Click, or navigation? A press that travelled orbited; one that did not, did not.
// Click, or navigation? A press that travelled panned or orbited; one that did not, did not.
const bool is_click = !m_ctx_travelled && std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx;
if (m_on_context_menu && !terminated && !inline_busy() && is_click) {
// Ending a chain or abandoning an anchor uses the click up.
const bool terminated = is_click && m_canvas && m_sketch_tool.take_right_click(*m_canvas);
if (terminated)
m_canvas->set_as_dirty(); // drawn by the canvas's own RightUp (e.Skip below) or at idle
else if (m_on_context_menu && !inline_busy() && is_click) {
// The menu belongs to what you POINTED AT — and pointing happened at the PRESS, not
// at the release, so the raycast uses the press position. Within a 3 px budget the
// two are the same pixel in practice; using the press is what makes that a
@@ -1167,9 +1198,6 @@ void DesignCanvas::set_readout(const std::string& text)
if (m_canvas) m_canvas->set_as_dirty(); // drawn by the next frame (the tool feeds this from one)
}
// Clear of the view cube and the two round view buttons, which own the bottom-left corner.
static constexpr float kStatusHudLeftInset = 190.f;
void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour)
{
if (text == m_status_hud_last && colour == m_status_hud_colour) return;
@@ -1204,8 +1232,11 @@ void DesignCanvas::render_hud()
ImGuiWrapper::pop_common_window_style();
};
if (!m_status_hud_last.IsEmpty()) {
// Past the view cube and the round view buttons, which own the bottom-left corner. Asked
// of the canvas, which lays them out: they follow the monitor's DPI on Windows, where `em`
// does not, so a fixed inset in `em` let them cover the start of the line at 150%.
// A sentence can be a sentence: it wraps to the room left of the readout chip.
const float left = kStatusHudLeftInset * em;
const float left = m_canvas->get_canvas_toolbar_right() + margin;
const ImVec4 col = m_status_hud_colour.IsOk()
? ImVec4(m_status_hud_colour.Red() / 255.f, m_status_hud_colour.Green() / 255.f,
m_status_hud_colour.Blue() / 255.f, 1.f)
@@ -1241,7 +1272,7 @@ void DesignCanvas::sync_selected_faces()
if (m_body_meshes == nullptr || m_model.objects.empty())
return;
rebuild_bodies();
reload(true);
reload();
});
}
@@ -1293,7 +1324,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
reload(); // recolours the body volumes
}
void DesignCanvas::set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl)
@@ -1306,7 +1337,7 @@ void DesignCanvas::set_body_translucent(bool on)
{
if (m_body_translucent == on) return;
m_body_translucent = on;
reload(true); // re-applies object-0 alpha so the solid fades for the fillet preview
reload(); // re-applies object-0 alpha so the solid fades for the fillet preview
}
void DesignCanvas::set_xray_focus(int body)
@@ -1314,7 +1345,7 @@ void DesignCanvas::set_xray_focus(int body)
if (m_xray_focus == body) return;
m_xray_focus = body;
m_sketch_tool.set_pick_only_body(body);
reload(true); // re-applies per-body alpha so the non-focused bodies fade
reload(); // re-applies per-body alpha so the non-focused bodies fade
}
void DesignCanvas::set_body_hidden(bool on)
@@ -1322,7 +1353,7 @@ void DesignCanvas::set_body_hidden(bool on)
if (m_body_hidden == on) return;
m_body_hidden = on;
m_sketch_tool.set_body_edges_hidden(on);
reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
reload(); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
}
bool DesignCanvas::delete_selected_sketch_entities()
+18 -6
View File
@@ -3,6 +3,8 @@
#include <vector>
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Color.hpp"
#include <utility>
#include <wx/colour.h>
@@ -48,16 +50,26 @@ public:
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
// `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.
// by address (a stable panel member) and read again whenever the selection changes. The
// bodies' edge lines are refreshed with them (DesignSketchTool::refresh_body_edges).
void set_bodies(const std::vector<TriangleMesh>* body_meshes,
const std::vector<bool>& visible = {});
void clear_mesh();
void clear_mesh(); // no bodies: drops their volumes and the tool's edge lines, pick and selection
void set_preview_mesh(const TriangleMesh& mesh);
void clear_preview();
void fit_view();
void set_view(const std::string& view_name);
// Frame `box` along the current view direction, as the canvas's Fit button frames a selection.
// False, the camera left alone, when there is nothing to frame: an undefined box.
bool zoom_to_box(BoundingBoxf3 box);
// Boxes to hand zoom_to_box: the selection and body faces (see DesignSketchTool).
BoundingBoxf3 selection_box() const { return m_sketch_tool.selection_box(); }
BoundingBoxf3 faces_box(std::vector<std::pair<int, int>> faces) const
{
return m_sketch_tool.faces_box(std::move(faces));
}
void begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode);
// Re-open a committed entity sketch for full in-canvas editing (load geometry +
@@ -66,7 +78,6 @@ public:
const std::vector<SketchEntityConstraintDef>& constraints,
const SketchPlane& plane);
void set_sketch_tool(DesignSketchTool::Mode mode);
void set_sketch_plane(const SketchPlane& plane); // re-plane the live sketch when a reference plane is clicked in 3D
void set_sketch_construction(bool c);
// Flip the sketch selection between construction and real geometry; returns the
// number of entities changed (0 = nothing selected, caller falls back to the mode).
@@ -83,6 +94,8 @@ public:
void finish_sketch();
bool is_sketching() const;
void refresh_bed(); // re-sync the bed to the current printer (call on tab activation)
// Centre of the Design bed: the printer bed at its home position, whichever plate is current.
Vec2d bed_center() const { return m_bed.build_volume().bed_center(); }
// The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no
// per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs
// show. Exactly one of the two views is live at a time, so entering and leaving are the
@@ -157,7 +170,6 @@ public:
void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform,
double body_radius);
void clear_move_gizmo();
bool moving_body() const;
void set_on_body_move_changed(std::function<void(int, const Transform3d&)> cb);
// Visual Fillet/Chamfer radius gizmo: when a solid edge is picked, anchor a radius arrow on
// it; drag/edit fire the radius callback. Returns false if no edge is currently picked.
@@ -234,6 +246,7 @@ public:
std::vector<std::string> labels = {}); // clickable labelled reference planes
void clear_base_pick();
void set_on_datum_base_picked(std::function<void(int)> cb);
void set_selected_base(std::function<int()> cb); // the reference plane drawn selected, or -1
void set_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
void set_on_sketch_exit_refused(std::function<void()> cb); // Esc declined: sketch has work
void set_on_sketch_notice(std::function<void(const std::string&, bool)> cb); // tool refusals/side effects
@@ -380,7 +393,7 @@ public:
void repaint_now();
private:
void reload(bool keep_view);
void reload();
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
@@ -415,7 +428,6 @@ private:
Camera m_parked_camera;
bool m_camera_swapped{false}; // guards a leave without an enter, and the reverse
Model m_model;
bool m_first_frame{true};
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
+3 -2
View File
@@ -102,7 +102,7 @@ static const bool kOfferRowFlat[] = {
};
static const OfferVerb kOfferVerbs[] = {
{"sketch", L_CONTEXT("Sketch", "Design"), 0, "Shift+S", "key:S+S", L("Click a face or a reference plane in the viewport, then a sketch tool"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Click a face or a reference plane, then pick a drawing tool")},
{"sketch", L_CONTEXT("Sketch", "Design"), 0, "Shift+S", "key:S+S", L("Select a flat face or a reference plane to sketch on"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Sketch on the selected flat face or plane, or click one next")},
{"extrude", L_CONTEXT("Extrude", "Design"), 1, "Shift+E", "key:S+E", L("Create a sketch, or pick a solid face, first"), 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", L("Extrude a sketch profile, or push/pull a picked face")},
{"revolve", L_CONTEXT("Revolve", "Design"), 1, "Shift+R", "key:S+R", L("Create a sketch profile to revolve first"), 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", L("Revolve a profile about an axis")},
{"sweep", L_CONTEXT("Sweep", "Design"), 1, "Shift+W", "key:S+W", L("Create a profile sketch to sweep first"), 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", L("Sweep a profile along a path")},
@@ -145,6 +145,7 @@ static const OfferVerb kOfferVerbs[] = {
{"rename", L_CONTEXT("Rename…", "Design"), 8, "F2", "btn:rename", L("Select a feature, or a body, to rename it"), 0x00004080u, 0, 0, false, false, nullptr, nullptr, L("Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)")},
{"delete_face", L_CONTEXT("Delete Face", "Design"), 7, nullptr, "fly:dressup#3", L("Delete Face needs a body — add or import one first"), 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", L("Remove faces from a body and heal the solid")},
{"colour", L_CONTEXT("Color", "Design"), 8, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", L("Set the selected body's display color")},
{"zoom_to", L_CONTEXT("Zoom to selection", "Design"), 8, nullptr, "btn:zoom_to", nullptr, 0x000041feu, 0, 0, false, false, nullptr, "design_zoom", L("Frame the selection in the view, keeping the view direction")},
{"delete", L_CONTEXT("Delete", "Design"), 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", L("Delete what is selected")},
{"delete_body", L_CONTEXT("Delete Body", "Design"), 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", L("Delete this whole body — removes the feature it was made from")},
{"sk_line_t", L_CONTEXT("Line", "Design"), 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, L("Line"), "design_line", L("Line — click start, then end")},
@@ -204,7 +205,7 @@ static const OfferVerb kOfferVerbs[] = {
{"sk_radius", L_CONTEXT("Radius / diameter…", "Design"), 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the radius of this arc, or the diameter of this circle")},
{"sk_angdist", L_CONTEXT("Angle / distance…", "Design"), 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the angle between two lines, or the distance between the two picks")},
};
static const int kOfferVerbCount = 92;
static const int kOfferVerbCount = 93;
}} // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+44 -8
View File
@@ -383,7 +383,16 @@ private:
void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card)
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
void update_reference_planes(); // the XY/XZ/YZ planes + axes: Origin row, plane choice, Plane tool
void toggle_origin(); // the Origin row's eye (and Ctrl+Shift+O): keep the reference planes up
void toggle_bed(); // the Bed row's eye (and Ctrl+Shift+B): draw the printer bed, or not
void refresh_pinned(); // rebuild the Origin and Bed rows (their eyes and label colours)
// Sketch: open a session on the picked face or plane, or with none picked, put the planes up
// and wait for one (m_choosing_sketch_plane).
void start_sketch();
void start_sketch_on_target(bool offer_tools); // a session on sketch_plane_from_selection()
void end_sketch_plane_choice(); // leave the plane choice without a sketch; no-op outside it
bool sketch_plane_pick_live() const; // may a pick made now still open the sketch?
CadDocument m_doc;
@@ -413,7 +422,6 @@ private:
ScalableButton* m_commit_btn{nullptr}; // main face: runs the current commit mode
DropDown* m_commit_drop{nullptr}; // commit-mode choices, owned via m_flyout_keepalive
wxSizer* m_tb_doc{nullptr}; // toolbar document/view actions (new, commit, export, section, place)
CheckBox* m_show_bed{nullptr}; // view option: draw the printer bed + plate grid, or not
wxSizer* m_box_move{nullptr}; // Move/Rotate numeric options (distance, axis, angle)
wxSizer* m_box_sketch{nullptr};
wxSizer* m_box_extrude{nullptr};
@@ -571,8 +579,20 @@ private:
// deliberately no dropdown for it. e1p.
int m_ref_plane{0};
// m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY"
// from "nobody has chosen anything yet". This does.
// from "nobody has chosen anything yet". This does. A selection like a picked face: the sketch
// opened on it uses it up, and Esc or a click on nothing lets go of it.
bool m_plane_picked{false};
// Sketch was pressed with nothing to sketch on: the reference planes are up and the next
// reference plane or flat face clicked opens the sketch on it. Still Feature mode — sketch
// mode is entered only with the session (start_sketch_on_target).
bool m_choosing_sketch_plane{false};
// The Feature tree's Origin row: keeps the reference planes and their axes up
// (update_reference_planes). A view preference (AppConfig "design_show_origin"), not part of
// the recipe.
bool m_show_origin{false};
// The Feature tree's Bed row: draws the printer bed and its plate grid. A view preference
// (AppConfig "design_show_bed"), not part of the recipe.
bool m_show_bed{true};
ComboBox* m_shape{nullptr};
ComboBox* m_mode{nullptr};
wxSpinCtrlDouble* m_width{nullptr};
@@ -913,12 +933,16 @@ private:
std::function<void(double)> m_value_cont; // deferred apply, run on Confirm
std::function<void()> m_value_cancel; // optional action when the card is cancelled
// Feature tree: one row per feature, in feature order, with a per-type icon and the row's
// own Edit / Show-hide / Delete icons. Callers use row indices via
// tree_selection()/set_tree_selection(); refresh_tree() rebuilds the rows.
// Feature tree: the Origin and Bed rows, a fixed block of view switches that never scrolls
// and selects nothing, then, in a frame of its own below them, one row per feature, in feature
// order, with a per-type icon and the row's own Zoom to selection / Edit / Show-hide / Delete
// icons. Callers use row indices via tree_selection()/set_tree_selection(); refresh_tree()
// rebuilds the rows.
DesignRowList* m_pinned{nullptr};
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.
// The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected, and
// what Zoom to selection frames for a row. Finding them replays the history, so they are kept
// per row and topology generation (feature_faces).
// 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...
@@ -927,7 +951,10 @@ private:
bool m_hl_pending{false};
void request_feature_highlight();
void update_feature_highlight();
// The faces feature `f` made, found again when the cached ones are another row's or topology's.
const std::vector<std::pair<int, int>>& feature_faces(int f);
bool deselect_rows(); // Esc / a click on nothing: drop the tree and Bodies rows
bool drop_plane_pick(); // ...and a picked reference plane; true if one was picked
// 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};
@@ -964,8 +991,17 @@ private:
void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform
void feed_bodies(); // push m_disp_* + visibility/xform to the viewport
void on_move_body(); // start the move gizmo on the selected body
bool body_move_pending() const { return m_move_body >= 0; } // the Move button's session is open
void end_body_move(bool keep); // leave it: keep the dragged pose, or put the body back
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
// Zoom to selection: frame one thing along the current view, as the canvas's Fit button frames
// a selection. A body is framed whole, hidden or not; a feature by its sketch or the faces it
// made; anything else the offer is opened on, by what the Fit button frames for it.
void zoom_to_body(int b);
void zoom_to_feature(int f);
void zoom_to_selection();
bool can_zoom_to_feature(int f) const; // a sketch, or a shown feature that makes faces
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1
+22 -12
View File
@@ -29,11 +29,19 @@ static constexpr int kCellDip = 20; // square action cell, hover chip included
static constexpr int kPadDip = 4; // row edges, and the gap before the action cells
static constexpr int kGapDip = 6; // type icon to label
DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
// wxVListBox defaults to wxBORDER_THEME; the sidebar's lists take a simple frame.
: wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE)
DesignRowList::DesignRowList(wxWindow* parent, int max_visible, bool selectable, long style)
// wxVListBox defaults to wxBORDER_THEME; `style` defaults to the simple frame the sidebar's
// lists take, and a list meant to sit unframed on its card passes wxBORDER_NONE.
: wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, style)
, m_max_visible(std::max(max_visible, 1))
, m_selectable(selectable)
{
// A non-selectable list takes no focus: from the mouse because it consumes the presses that
// would focus it, below, and from the keyboard through AcceptsFocus(). wxGTK reads that once,
// while the base class is being built, before the override exists, so it is told here.
if (!m_selectable)
SetCanFocus(false);
Bind(wxEVT_LISTBOX, [this](wxCommandEvent&) { if (on_select) on_select(); });
Bind(wxEVT_LISTBOX_DCLICK, [this](wxCommandEvent&) { if (on_activate) on_activate(); });
@@ -46,18 +54,20 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
m_pressed = Hit{};
e.Skip();
});
// A press on an action cell is remembered and skipped, so the list still selects the row.
// A press on an action cell is remembered and skipped, so the list still selects the row. A
// non-selectable list keeps every press from wxVListBox, which would select the row and take
// the focus.
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
m_pressed = h.cell >= 0 ? h : Hit{};
e.Skip();
if (m_selectable) e.Skip();
});
// The second press of a double-click on a cell is a press too, and is not skipped: the list
// would otherwise turn it into a row double-click, so a quick double toggle of the eye would
// also open the feature for editing.
Bind(wxEVT_LEFT_DCLICK, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
if (h.cell < 0) { m_pressed = Hit{}; e.Skip(); return; }
if (h.cell < 0) { m_pressed = Hit{}; if (m_selectable) e.Skip(); return; }
m_pressed = h;
});
Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
@@ -69,9 +79,9 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
const int id = m_rows[pressed.row].actions[pressed.cell].id;
// After the click has finished dispatching: the action may rebuild these rows. The row
// must still exist and still be the selected one, or the click is dropped rather than
// applied to whatever row took its place.
// applied to whatever row took its place. A non-selectable list has no selected row.
CallAfter([this, row = pressed.row, id] {
if (row < int(GetItemCount()) && row == GetSelection() && on_action)
if (row < int(GetItemCount()) && (!m_selectable || row == GetSelection()) && on_action)
on_action(row, id);
});
});
@@ -79,7 +89,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
// press suppresses the wxEVT_CONTEXT_MENU that follows it.
Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
if (h.row != wxNOT_FOUND) select(h.row);
if (m_selectable && h.row != wxNOT_FOUND) select(h.row);
e.Skip();
});
Bind(wxEVT_CONTEXT_MENU, [this](wxContextMenuEvent& e) {
@@ -95,7 +105,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
return;
// On MSW the menu comes with the button's release, and the pointer may have moved to
// another row since the press selected one: the menu is for the row it opens over.
select(row);
if (m_selectable) select(row);
if (on_menu) on_menu(row, screen);
});
// wxVListBox takes every wheel event, even with nothing to scroll, so a short list would stop
@@ -254,7 +264,7 @@ void DesignRowList::OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) con
wxColour bg;
if (IsSelected(n))
bg = StateColor::darkModeColorFor(wxColour("#BFE1DE"));
else if (int(n) == m_hover.row)
else if (m_selectable && int(n) == m_hover.row) // a non-selectable row lights only its icons
bg = StateColor::darkModeColorFor(wxColour("#E5F0EE"));
if (!bg.IsOk())
return; // the list already cleared to its background colour
@@ -314,7 +324,7 @@ DesignRowList::Hit DesignRowList::hit_test(const wxPoint& pt) const
const wxRect rect = GetItemRect(h.row);
const auto& acts = m_rows[h.row].actions;
for (size_t i = 0; i < acts.size(); ++i)
if (cell_rect(rect, acts.size(), i).Contains(pt)) {
if (!acts[i].icon.empty() && cell_rect(rect, acts.size(), i).Contains(pt)) {
h.cell = int(i);
break;
}
+16 -5
View File
@@ -29,12 +29,16 @@ namespace Slic3r { namespace GUI {
// Selection follows wxTreeCtrl's contract, which DesignPanel's mutual exclusion between the two
// lists relies on: on_select runs on every change, whether the user or select() made it, and
// selecting the row that is already selected changes nothing and notifies nobody.
//
// A list built non-selectable is a row of switches, not of objects: a click never selects a row,
// the pointer highlights only the action icon under it, and the list never takes the focus. Its
// action icons and its context menu are the only things that answer.
class DesignRowList : public wxVListBox
{
public:
struct Action {
int id; // the owner's code for it, handed back to on_action
std::string icon; // icon name, e.g. "design_eye"
std::string icon; // icon name, e.g. "design_eye"; empty for a blank cell that only holds a column
wxString tip;
};
struct Row {
@@ -50,7 +54,8 @@ public:
static constexpr const char* hover_chip = "#D4D4D4";
// The list is as tall as its rows, at least one and at most `max_visible`; past that it scrolls.
DesignRowList(wxWindow* parent, int max_visible);
// `style` is its frame: wxBORDER_NONE for a list that sits unframed on its card.
DesignRowList(wxWindow* parent, int max_visible, bool selectable = true, long style = wxBORDER_SIMPLE);
// Replace every row. Clears the selection without notifying and cancels a rename in progress;
// the owner re-selects the row it keeps, which notifies.
@@ -67,15 +72,20 @@ public:
std::function<void()> on_select; // the selection changed
std::function<void()> on_activate; // the selected row was double-clicked
// A row's action icon was clicked. Runs after the click has finished dispatching, and only
// while that row is still the selected one; the click itself selected it.
// A row's action icon was clicked. Runs after the click has finished dispatching, while the
// row still exists and — in a selectable list, where the click itself selected it — is still
// the selected one.
std::function<void(int row, int id)> on_action;
// Context menu on a row, at a screen position; the row under the pointer is selected first.
// Context menu on a row, at a screen position; the row under the pointer is selected first,
// unless the list is non-selectable. Pop the menu from inside the call: wxGTK sends it on the
// right press, and a menu deferred past that event closes on the button's release.
std::function<void(int row, const wxPoint& screen)> on_menu;
// The editor committed `name` for `row`, trimmed and never empty (an empty commit cancels).
// Runs after the editor's own events have finished, so the owner may rebuild the rows here.
std::function<void(int row, const wxString& name)> on_rename;
bool AcceptsFocus() const override { return m_selectable && wxVListBox::AcceptsFocus(); }
protected:
void OnDrawItem(wxDC& dc, const wxRect& rect, size_t n) const override;
void OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) const override;
@@ -103,6 +113,7 @@ private:
std::vector<Row> m_rows;
std::map<std::string, ScalableBitmap> m_icons;
int m_max_visible;
bool m_selectable;
int m_row_h{0}; // every row's height, from measure_row()
Hit m_hover;
Hit m_pressed; // action cell under the last left press
File diff suppressed because it is too large Load Diff
+80 -17
View File
@@ -2,26 +2,32 @@
#define slic3r_DesignSketchTool_hpp_
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Line.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/CAD/CadDocument.hpp" // CadBody for per-body solid picking
#include <TopoDS_Shape.hxx>
#include "libslic3r/CAD/SketchInference.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies
#include <Eigen/Core>
#include <array>
#include <cstddef>
#include <wx/event.h>
#include <functional>
#include <optional>
#include "libslic3r/Color.hpp"
#include <math.h>
#include <vector>
#include <string>
#include <utility>
class wxMouseEvent;
class wxPoint;
namespace Slic3r {
class TriangleMesh; // fwd (libslic3r) — solid-pick mesh, non-owning pointer
class GLVolumeCollection;
namespace GUI {
@@ -50,6 +56,36 @@ inline ColorRGBA design_idle_face_color()
{
return ColorRGBA(0.72f, 0.76f, 0.80f, 0.14f);
}
// A convex piece of the square drawn on planes[plane].
struct PlanePiece
{
int plane;
std::vector<Vec3d> corners;
};
// The squares of half-extent `half` centred on `planes`' origins, cut where they cross one another
// and ordered back to front for an eye at `eye` (perspective) or looking along `forward`
// (orthographic). Translucent planes that cross cannot be drawn in any per-plane order: each is
// partly in front of and partly behind the others. Drawn piece by piece in this order, each one
// tints only what is behind it.
std::vector<PlanePiece> planes_back_to_front(const std::vector<SketchPlane>& planes, double half,
const Vec3d& eye, const Vec3d& forward, bool perspective);
// The square a reference plane is drawn as, given as the frame at its centre, half-extent `half`.
// The base planes XY, XZ and YZ (`base` 0, 1, 2) sit in the octant (+X, -Y, +Z), the one all three
// names face, reference_square_gap(half) clear of the two axes bounding each, so the three never cross
// and the axes run between them. Any other base (a datum) stays centred on its own origin. The frame is the same
// plane moved within itself, so planes_back_to_front and pick_reference_square take it unchanged.
SketchPlane reference_square(const SketchPlane& plane, int base, double half);
inline double reference_square_gap(double half) { return 0.25 * half; }
// The corners of the box around a base square's label, in the square: inset from its (+x, +y) corner and
// written along the square's x axis with its y axis up, so it turns with the plane. The box the hit
// test takes for the label: round the strokes render_base_pick draws, from the same layout, with a
// margin to aim at.
std::array<Vec3d, 4> reference_label_box(const SketchPlane& square, double half, const std::string& text);
// The nearest of `squares` (half-extent `half`) the ray from `from` along `dir` crosses, or -1.
int pick_reference_square(const std::vector<SketchPlane>& squares, double half, const Vec3d& from, const Vec3d& dir);
class DesignSketchTool {
public:
enum class Mode { Select, Dimension, Polyline, Line, CornerRect, CenterRect, ObliqueRect,
@@ -137,7 +173,6 @@ public:
// Returns false when no session is live, so the caller can fall back to a new feature.
bool add_imported_regions(const std::vector<std::vector<std::vector<Vec2d>>>& regions);
void set_tool(Mode mode); // switch tool, keep accumulated entities
void set_plane(const SketchPlane& plane) { m_plane = plane; } // re-plane a live sketch (a reference plane was clicked mid-session); entities are 2D, re-lifted through the new plane
void set_construction(bool c) { m_construction = c; }
void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); }
void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; }
@@ -149,10 +184,10 @@ public:
// Right-click on a draw tool: true when an in-progress anchor was abandoned, false when
// there was nothing to abandon — and false is what lets the offer menu open. ghcz.
bool right_abandon();
// True if the LAST right-press was consumed as a gesture terminator (end a polyline chain,
// abandon an anchor, exit a tool). Read-and-clear: the canvas asks on the matching release to
// decide whether that right-click was the user's, in which case it opens the offer.
bool take_right_consumed() { const bool b = m_right_consumed; m_right_consumed = false; return b; }
// Replays and clears the right press on_mouse kept for the camera: true if the tool used it as
// a gesture terminator (end a polyline chain, abandon an anchor), so no offer opens.
bool take_right_click(GLCanvas3D& canvas);
void drop_right_click() { m_right_press.reset(); }
void render(GLCanvas3D& canvas);
// The in-canvas value field, drawn by render() before any early return. Owned by
// DesignCanvas; null until it sets it. Not a window — see SketchInlineEditor.hpp.
@@ -207,6 +242,9 @@ public:
// their edges do not float over the preview.
void set_body_edges_hidden(bool h) { m_body_edges_hidden = h; }
void clear_solid_selection();
// Resample the edges of each body whose shape changed and drop those of bodies that are gone.
// Also expires a pick or hover on a rebuilt body. The canvas calls it whenever it gets the bodies.
void refresh_body_edges();
bool has_solid_selection() const { return m_solid_sel != SolidSel::None; }
// Every picked edge when the selection is an edge set (Shift/Ctrl+click adds and removes
// edges of the same body): the earlier picks first, the last-clicked edge at the end.
@@ -221,6 +259,20 @@ public:
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;
// What the canvas's Fit button frames: the selection — faces (the Feature tree's included), a
// body, edges, a vertex, a sketch region, the live sketch's picked entities — else everything
// on show: the visible bodies, the preview of the feature being edited and every sketch.
// Undefined when the tab shows none of it. `volumes` are the canvas's: bodies and preview.
BoundingBoxf3 fit_box(const GLVolumeCollection& volumes) const;
// The selection fit_box frames, with no fallback: undefined when nothing is selected.
BoundingBoxf3 selection_box() const;
// World box of (body, face id) faces on the pick mesh, hidden bodies included.
BoundingBoxf3 faces_box(std::vector<std::pair<int, int>> faces) const;
// World box of sketch entities drawn on `plane`.
static BoundingBoxf3 sketch_box(const std::vector<SketchEntity>& entities, const SketchPlane& plane);
// Grow a box the camera is to frame to at least a small extent on every axis: a vertex has no
// size, nor has a sketch along its normal.
static void pad_box(BoundingBoxf3& box);
// 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
@@ -230,8 +282,10 @@ public:
void set_move_gizmo(int body, const Vec3d& pivot, const Transform3d& base_xform,
double body_radius = 0.0);
void clear_move_gizmo();
bool moving_body() const { return m_mv_active; }
int move_body_index() const { return m_mv_body; }
// Press, then drag, translate arrow `axis` (0..2 = X/Y/Z) with the mouse ray under the cursor.
void grab_move_arrow(int axis, const Linef3& ray);
void drag_move_arrow(const Linef3& ray);
std::function<void(int body, const Transform3d& xform)> on_body_move_changed;
// Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id).
std::function<void(int level, int body, int face, int edge)> on_solid_selection_changed;
@@ -316,6 +370,12 @@ public:
std::vector<std::string> labels = {});
void clear_base_pick();
std::function<void(int base)> on_datum_base_picked;
// The base drawn as selected, or -1. Asked once a frame rather than set, because what decides it
// (the panel's chosen sketch plane, a picked face, the Plane card's base) changes in many places.
std::function<int()> selected_base;
// The reference planes are drawn this frame, with their own half-axes and origin mark: the canvas
// leaves out the bed's axes triad, which would sit on top of them.
bool draws_reference_axes() const { return m_dbp_active && !m_active; }
// Visual Fillet/Chamfer gizmo. The Dressup tool is a DesignPanel docked card, so the sketch
// tool is NOT active during it; when a solid EDGE is picked the panel passes the body centroid
@@ -971,7 +1031,7 @@ private:
bool op_ready() const; // required entities picked -> arrow/ghost live
// Sample an entity into a 2D polyline for the overlay renderer.
std::vector<Vec2d> entity_polyline(const SketchEntity& e, bool& closed) const;
static std::vector<Vec2d> entity_polyline(const SketchEntity& e, bool& closed);
// Closed regions formed by the current (non-construction) entities: each a CCW-
// ordered boundary polygon on the plane. A circle is its own region; line/arc
@@ -1225,19 +1285,20 @@ private:
Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform
// The bodies' B-rep edges, drawn as dark lines over the solids so faces and features read
// apart. One polyline set per body in its own shape coordinates, resampled only for a body
// whose shape changed (keyed by the TShape), since set_solid_pick runs on every recompute.
// whose shape changed (see is_current_shape).
std::vector<std::vector<std::vector<Vec3d>>> m_body_edges;
std::vector<const void*> m_body_edges_key;
std::vector<TopoDS_Shape> m_body_edges_shape; // the shape each set was sampled from
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
const TopoDS_Shape* body_shape(int body) const; // nullptr when there is no such body or it has no shape
bool is_current_shape(int body, const TopoDS_Shape& sampled) const; // `sampled` is still the body's shape
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
TopoDS_Shape m_sel_shape; // that body's shape when picked: the ids index into it
int m_sel_face{-1};
int m_sel_edge{-1};
std::vector<Vec3d> m_sel_edge_pts;
@@ -1274,7 +1335,7 @@ private:
void hit_display_sketch(const DisplaySketch& d, const Vec2d& p, double tol,
int& edge_feat, int& edge_reg, int& edge_ent,
double& edge_d, int& face_feat, int& face_reg) const;
bool m_right_consumed{false}; // last RightDown was a gesture terminator, not a menu
std::optional<wxMouseEvent> m_right_press; // right press not yet known to be a click
bool m_escalate_repick{true}; // re-picking the same sub-element takes the whole body
void render_solid_highlight();
// The above's edge and vertex highlight, from explicit arguments, so the committed selection
@@ -1282,11 +1343,11 @@ private:
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.
// shape so a recompute that rebuilt the body retires it.
struct FaceHighlight {
int body{-1};
std::vector<int> faces; // sorted
const void* key{nullptr};
TopoDS_Shape shape;
std::vector<std::vector<Vec3d>> edges; // in the body's shape coordinates
};
FaceHighlight make_face_highlight(int body, std::vector<int> faces) const;
@@ -1385,7 +1446,9 @@ private:
std::vector<int> m_dbp_base;
std::vector<std::string> m_dbp_labels;
int m_dbp_hover{-1};
double dbp_half_extent() const; // bed-derived: reference planes are larger than the bed
double dbp_half_extent() const; // bed-derived square size
std::vector<SketchPlane> dbp_squares(double half) const; // reference_square of each entry
void render_reference_axes(const Vec3d& origin, double half);
void render_base_pick();
int hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
@@ -1403,6 +1466,7 @@ private:
int m_mv_drag{-1}; // 0..2 = X/Y/Z arrow, 3..5 = X/Y/Z ring, -1 none
double m_mv_radius{0.0}; // body bounding-sphere radius (mm); 0 = unknown
int m_mv_press_x{0}, m_mv_press_y{0};
double m_mv_grab_along{0.0}; // arrow grab point's distance from the anchor; NaN = unknown
Transform3d compose_move_xform() const; // T(offset)*T(pivot)*rot*T(-pivot)*base_xform
void ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const; // world axis + in-plane basis
void render_move_gizmo();
@@ -1410,7 +1474,6 @@ private:
double move_gizmo_arm(const Camera& cam) const;
bool hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
bool hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
void drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
void drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
bool arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const;
void open_move_editor(int axis);
+91 -26
View File
@@ -1875,7 +1875,7 @@ BoundingBoxf3 GLCanvas3D::volumes_bounding_box(bool current_plate_only) const
bool is_limit = m_canvas_type != ECanvasType::CanvasAssembleView;
if (is_limit) {
if (current_plate_only) {
expand_part_plate_list_box = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_bounding_box();
expand_part_plate_list_box = _current_plate_box();
} else {
auto plate_list_box = wxGetApp().plater()->get_partplate_list().get_bounding_box();
auto horizontal_radius = 0.5 * sqrt(std::pow(plate_list_box.min[0] - plate_list_box.max[0], 2) + std::pow(plate_list_box.min[1] - plate_list_box.max[1], 2));
@@ -2088,6 +2088,25 @@ float GLCanvas3D::get_collapse_toolbar_height() const
return collapse_side() != CollapseSide::None ? collapse_toolbar().get_height() : 0;
}
// The bottom-left corner: the 3D navigator, then the column of round canvas buttons beside it, in
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar and
// get_canvas_toolbar_right, so an overlay kept clear of the corner follows its layout.
static constexpr float NAVIGATOR_SIZE = 128.f;
static constexpr float CANVAS_TOOLBAR_MARGIN = 10.f; // off the canvas edge, when there is no navigator
static constexpr float CANVAS_TOOLBAR_PADDING = 2.f;
static constexpr float CANVAS_TOOLBAR_BUTTON = 36.f;
float GLCanvas3D::get_canvas_toolbar_right() const
{
// The toolbar scale, which on Windows follows the monitor's DPI where the ImGui style does not.
float sc = get_scale();
#ifdef WIN32
sc *= (float) get_dpi_for_window(wxGetApp().GetTopWindow()) / (float) DPI_DEFAULT;
#endif // WIN32
const float left = wxGetApp().show_3d_navigator() ? NAVIGATOR_SIZE : CANVAS_TOOLBAR_MARGIN;
return (left + 2.f * CANVAS_TOOLBAR_PADDING + CANVAS_TOOLBAR_BUTTON) * sc;
}
GLToolbar& GLCanvas3D::collapse_toolbar() const
{
return m_collapse_toolbar != nullptr ? *m_collapse_toolbar : wxGetApp().plater()->get_collapse_toolbar();
@@ -2395,7 +2414,7 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
}
else if (gizmo_type == GLGizmosManager::BrimEars && !camera.is_looking_downward())
show_grid = false;
if (m_axes_at_bed_center)
if (m_design_canvas)
// Design tab: the plate grid is generated from the plate's front-left corner, so it
// floats mid-cell under the modeling-origin triad. Suppress it here; a CAD grid centred
// on the origin is rendered in its place (see _render_cad_grid).
@@ -2406,12 +2425,15 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
if (m_canvas_type == ECanvasType::CanvasView3D) {
// m_show_bed gates the plate list too: hiding the bed but leaving its grid and outline
// floating would read as a rendering fault rather than a deliberate view option.
// Design tab: while its reference planes are up they draw their own axes from the modeling
// origin, where the bed's triad would otherwise sit on top of them.
if (show_bed)
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes);
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(),
m_show_world_axes && !(m_design_sketch_tool != nullptr && m_design_sketch_tool->draws_reference_axes()));
m_frame_profiler.mark("bed");
if (show_bed) //BBS: add outline logic
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
if (m_axes_at_bed_center && show_bed)
if (m_design_canvas && show_bed)
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid.
_render_cad_grid(camera.get_view_matrix(), camera.get_projection_matrix());
m_frame_profiler.mark("plates");
@@ -4311,7 +4333,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
if (evt.LeftDown() && m_canvas != nullptr)
m_canvas->SetFocus(); // grab keyboard focus so Delete/keys reach this canvas
if (m_design_sketch_tool->on_mouse(evt, *this)) {
// A Touchpad-style orbit or pan (a plain move with Alt or Shift held) is the camera's,
// whatever the tool would make of the move. Only a drag consults the button mappings.
const bool camera_move = evt.Moving() && (is_camera_rotate(evt, {}) || is_camera_pan(evt, {}));
if (!camera_move && m_design_sketch_tool->on_mouse(evt, *this)) {
m_dirty = true;
render(); // force an immediate redraw so the sketch overlay updates live
return;
@@ -6355,7 +6380,7 @@ void GLCanvas3D::_render_3d_navigator()
}
}
const float size = 128 * sc;
const float size = NAVIGATOR_SIZE * sc;
m_canvas_toolbar_pos[0] = size;
const auto result = ImGuizmo::ViewManipulate(cameraView, cameraProjection, ImGuizmo::OPERATION::ROTATE, ImGuizmo::MODE::WORLD, nullptr,
camDistance, ImVec2(viewManipulateLeft, viewManipulateTop - size), ImVec2(size, size),
@@ -7970,9 +7995,16 @@ void GLCanvas3D::_render_fps_overlay(int fps) const
const float margin = 10.0f * get_scale();
const ImVec2 display_size = ImGui::GetIO().DisplaySize;
ImVec2 pos(display_size.x - margin, margin);
// Last frame's size; zero until the overlay has been shown once.
const ImGuiWindow* self = ImGui::FindWindowByName("###fps_overlay");
const float left = pos.x - (self != nullptr ? self->Size.x : 0.0f);
// The Preview legend takes the top-right corner.
if (const ImGuiWindow* legend = ImGui::FindWindowByName("Legend"); m_canvas_type == ECanvasType::CanvasPreview && legend != nullptr && legend->Active)
pos = ImVec2(legend->Pos.x - margin, legend->Pos.y);
// The toolbar row can reach the corner on a narrow canvas; stack the overlay below it then.
else if (m_main_toolbar.is_enabled() &&
get_main_toolbar_offset() + m_main_toolbar.get_width() + m_separator_toolbar.get_width() + m_gizmos.get_scaled_total_width() + m_assemble_view_toolbar.get_width() > left)
pos.y = std::max(m_main_toolbar.get_height(), m_gizmos.get_scaled_total_height()) + margin;
ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 0.0f));
ImGui::SetNextWindowBgAlpha(0.35f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f * get_scale());
@@ -8251,7 +8283,7 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
*/
//bool show_texture = true;
//BBS set axes mode
if (m_axes_at_bed_center) {
if (m_design_canvas) {
// Design tab: triad at the bed centre = modeling origin (set every frame because
// set_shape/set_axes_mode otherwise reset it to the bed corner).
const Vec2d bc = m_bed.build_volume().bed_center();
@@ -8264,7 +8296,30 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
{
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
// Design tab: its bed stays at the printer bed's home whichever plate is current, so the
// current plate's exclude areas are moved from that plate onto it.
PartPlate* curr_plate = plate_list.get_curr_plate();
const Transform3d plate_view_matrix = m_design_canvas && curr_plate != nullptr ?
Transform3d(view_matrix * Geometry::translation_transform(-curr_plate->get_origin())) : view_matrix;
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
}
BoundingBoxf3 GLCanvas3D::_current_plate_box() const
{
// Design tab: its own bed stands in for the current plate (see _render_platelist).
const BuildVolume& build_volume = m_bed.build_volume();
if (m_design_canvas && build_volume.valid()) {
// Flat at z = 0 like the plate's own box. Merged, as PartPlate builds it: the min/max
// constructor leaves a flat box undefined.
const BoundingBoxf bb = build_volume.bounding_volume2d();
BoundingBoxf3 box;
box.merge(Vec3d(bb.min.x(), bb.min.y(), 0.));
box.merge(Vec3d(bb.max.x(), bb.max.y(), 0.));
return box;
}
PartPlate* curr_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
return curr_plate != nullptr ? curr_plate->get_bounding_box() : BoundingBoxf3();
}
// Design tab: CAD grid on the bed plane, drawn in place of the plate's corner-origin grid.
@@ -10038,10 +10093,10 @@ void GLCanvas3D::_render_canvas_toolbar()
sc *= (float) dpi / (float) DPI_DEFAULT;
#endif // WIN32
ImVec2 btn_size = ImVec2(36.f, 36.f) * sc;
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (10.f * sc), 10.f * sc);
ImVec2 btn_size = ImVec2(CANVAS_TOOLBAR_BUTTON, CANVAS_TOOLBAR_BUTTON) * sc;
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (CANVAS_TOOLBAR_MARGIN * sc), CANVAS_TOOLBAR_MARGIN * sc);
ImVec2 spacing = ImVec2(6.f, 6.f) * sc;
ImVec2 padding = ImVec2(2.f, 2.f) * sc;
ImVec2 padding = ImVec2(CANVAS_TOOLBAR_PADDING, CANVAS_TOOLBAR_PADDING) * sc;
Vec2i32 pos = {
m_canvas_toolbar_pos[0] + margin.x,
get_canvas_size().get_height() - margin.y
@@ -10099,14 +10154,25 @@ void GLCanvas3D::_render_canvas_toolbar()
ImTextureID z_hover_id = m_gizmos.get_icon_texture_id(m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_ZOOM_DARK_HOVER : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_ZOOM_HOVER);
if (ImGui::ImageButton3(z_normal_id, z_hover_id, btn_size)) {
select_view("plate");
if (m_selection.is_empty()) {
if (m_canvas_type == ECanvasType::CanvasAssembleView)
zoom_to_volumes();
else
zoom_to_bed();
} else {
zoom_to_selection();
#ifdef SLIC3R_CAD
// The Design tab selects and sketches outside the canvas's selection and volumes, so
// it names what to frame. Framed along the current view, which is often square to a
// sketch plane; an empty tab falls through to the bed.
const BoundingBoxf3 design_box = m_design_sketch_tool != nullptr ? m_design_sketch_tool->fit_box(m_volumes) : BoundingBoxf3();
if (design_box.defined)
_zoom_to_box(design_box);
else
#endif
{
select_view("plate");
if (m_selection.is_empty()) {
if (m_canvas_type == ECanvasType::CanvasAssembleView)
zoom_to_volumes();
else
zoom_to_bed();
} else {
zoom_to_selection();
}
}
} else if (ImGui::IsItemHovered()) {
auto tooltip_str_wx = _L("Fit camera to scene or selected object.");
@@ -11059,10 +11125,9 @@ std::optional<Vec3d> GLCanvas3D::get_camera_orbit_target(ECameraNavigationType n
{
// Orca: Centralize the pre-existing pivot rules so orbiting and pan fallback cannot
// choose different reference depths for the same canvas and active tool.
PartPlate* current_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
const BoundingBoxf3 plate_box = _current_plate_box();
if (navigation_type == ECameraNavigationType::Gesture)
return current_plate == nullptr ? std::nullopt :
std::make_optional(current_plate->get_bounding_box().center());
return plate_box.defined ? std::make_optional(plate_box.center()) : std::nullopt;
const GLGizmosManager::EType gizmo_type = m_gizmos.get_current_type();
const bool use_scene_target = m_canvas_type == ECanvasType::CanvasAssembleView ||
@@ -11082,15 +11147,15 @@ std::optional<Vec3d> GLCanvas3D::get_camera_orbit_target(ECameraNavigationType n
Vec3d target = Vec3d::Zero();
if (m_canvas_type == ECanvasType::CanvasPreview) {
if (current_plate != nullptr)
target = current_plate->get_bounding_box().center();
if (plate_box.defined)
target = plate_box.center();
} else if (!m_selection.is_empty()) {
target = m_selection.get_bounding_box().center();
} else {
// Orca: Match regular mouse orbit: objects on the active plate, then the plate itself.
BoundingBoxf3 bbox = volumes_bounding_box(true);
if (!bbox.defined && current_plate != nullptr)
bbox = current_plate->get_bounding_box();
if (!bbox.defined)
bbox = plate_box;
if (bbox.defined)
target = bbox.center();
}
+15 -6
View File
@@ -604,9 +604,11 @@ private:
GLToolbar* m_collapse_toolbar{nullptr};
std::function<CollapseSide()> m_collapse_side;
bool m_plate_chrome_enabled{true};
// Design tab: render the world-axis triad at the bed centre (= modeling origin) instead of
// the bed corner. Default false preserves the main editor's corner triad.
bool m_axes_at_bed_center{false};
// This canvas is the Design tab's. Its bed stays at the printer bed's home whichever plate is
// current, with the world-axis triad and a CAD grid at the bed centre (= modeling origin) in
// place of the corner triad and the plate grid, and the plate data it reads (exclude areas,
// the current plate's box) moved onto that bed. Default false leaves the editor tabs untouched.
bool m_design_canvas{false};
// Design tab: draw the printer bed and its plate grid at all. Default true, so the
// main editor is untouched; the Design tab lets the user hide it to model without a bed.
bool m_show_bed{true};
@@ -1025,13 +1027,15 @@ public:
// initialized, which loads the toolbar's background.
void set_collapse_toolbar(GLToolbar* toolbar, std::function<CollapseSide()> side);
void enable_plate_chrome(bool enable);
void set_axes_at_bed_center(bool b) { m_axes_at_bed_center = b; }
void set_design_canvas(bool b) { m_design_canvas = b; }
void set_show_bed(bool b) { m_show_bed = b; }
bool get_show_bed() const { return m_show_bed; }
void enable_sinking_contours(bool enable) { m_sinking_contours_enabled = enable; }
#ifdef SLIC3R_CAD
void set_design_sketch_tool(DesignSketchTool* tool) { m_design_sketch_tool = tool; }
DesignSketchTool* get_design_sketch_tool() const { return m_design_sketch_tool; }
// The Design tab frames what it selects itself (DesignCanvas::zoom_to_box), as the Fit button does.
void zoom_to_box(const BoundingBoxf3& box) { _zoom_to_box(box); }
#endif
void enable_dynamic_background(bool enable) { m_dynamic_background_enabled = enable; }
void enable_labels(bool enable) { m_labels.enable(enable); }
@@ -1059,6 +1063,9 @@ public:
bool is_collapse_toolbar_on_left() const;
float get_collapse_toolbar_width() const;
float get_collapse_toolbar_height() const;
// Right edge, in canvas pixels, of the bottom-left corner the 3D navigator and the round
// canvas buttons own. An overlay along the bottom edge starts past it.
float get_canvas_toolbar_right() const;
void update_volumes_colors_by_extruder();
@@ -1441,9 +1448,11 @@ private:
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add part plate related logic
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
// The current plate's box (XY, at z = 0); in the Design tab, its own bed's.
BoundingBoxf3 _current_plate_box() const;
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
// corner-origin grid when the axes sit at the bed centre (modeling origin). Rebuilds its
// GLModels lazily, only when the bed shape changed.
// corner-origin grid, centred on the modeling origin. Rebuilds its GLModels lazily, only
// when the bed shape changed.
void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add outline drawing logic
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
+4 -1
View File
@@ -1816,7 +1816,10 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
m_input_printer_name->GetTextCtrl()->SetLabelText(m_obj->get_dev_name());
std::string img_str = DevPrinterConfigUtil::get_printer_connect_help_img(m_obj->printer_type);
auto diagram_bmp = create_scaled_bitmap(img_str + "_en", this, 198);
if (img_str.empty()) { img_str = "input_access_code_x1"; }
std::string language = wxGetApp().app_config->get("language");
auto diagram_bmp = create_scaled_bitmap(img_str + (language == "zh_CN" ? "_cn" : "_en"), this, 198);
m_img_help->SetBitmap(diagram_bmp);
+1 -1
View File
@@ -2966,7 +2966,7 @@ void TabPrint::build()
optgroup->append_single_option_line("skin_infill_line_width", "strength_settings_patterns#locked-zag");
optgroup->append_single_option_line("skeleton_infill_line_width", "strength_settings_patterns#locked-zag");
optgroup->append_single_option_line("symmetric_infill_y_axis", "strength_settings_infill#symmetric-infill-y-axis");
optgroup->append_single_option_line("infill_complete_top", "strength_settings_infill#infill-complete-top");
optgroup->append_single_option_line("infill_complete_top", "strength_settings_patterns#fill-pattern-tops");
optgroup->append_single_option_line("infill_shift_step", "strength_settings_patterns#cross-hatch");
optgroup->append_single_option_line("lateral_lattice_angle_1", "strength_settings_patterns#lateral-lattice");
optgroup->append_single_option_line("lateral_lattice_angle_2", "strength_settings_patterns#lateral-lattice");
+7
View File
@@ -456,6 +456,13 @@ wxBitmap create_scaled_bitmap( const std::string& bmp_name_in,
const vector<std::string>& array_new_color/* = vector<std::string>*/)//used for semi transparent material)
{
static Slic3r::GUI::BitmapCache cache;
// An empty name means the caller's icon lookup failed
if (bmp_name_in.empty() || bmp_name_in == ".png") {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": empty bitmap name";
return wxNullBitmap;
}
if (bitmap2) {
return create_scaled_bitmap2(bmp_name_in, cache, win, px_cnt, grayscale, resize, array_new_color);
}