Merge upstream/main into belt-printer

Brings belt-printer up to main eb5b9a77b9. One conflict: main translates
the pressure advance test name (#16142) on the line after belt's guard that
keeps PA Line and PA Pattern off belt printers; both are kept.
This commit is contained in:
harrierpigeon
2026-10-05 13:40:41 -05:00
269 changed files with 213986 additions and 42084 deletions
File diff suppressed because it is too large Load Diff
+147 -7
View File
@@ -78,7 +78,7 @@ struct CadFeature {
// the OCCT shape verbatim — it is adopted as a base body in route_feature (no parametric
// recipe). Downstream face/edge features (fillet/chamfer/cut/shell/...) act on it like any
// other body. TopoDS_Shape is a cheap handle, so copying it through recompute/checkpoint
// snapshots is cheap. In-session only for now (no BRep serialization yet).
// snapshots is cheap. Saved with the recipe as a BRep string (see save()/load()).
TopoDS_Shape imported_solid;
// Non-destructive placement transform for imported_regions (Text/SVG),
@@ -116,6 +116,25 @@ struct CadFeature {
double dressup_size{1.0}; // fillet radius or chamfer distance
FaceGroup face_group{FaceGroup::All};
int dressup_edge{-1}; // global edge id for edge-targeted fillet/chamfer; -1 = use face_group
// Several picked edges dressed in ONE operation, all resolved against the same body, so
// the ids cannot drift the way they do across a chain of single-edge features. When set,
// dressup_edge holds the first of them: a build that predates the list still dresses that
// one edge instead of falling back to the whole face group.
std::vector<int> dressup_edges;
// The edges this dress-up targets: the list, else the single edge, else none (face group).
// Text feature: a Sketch whose imported_regions were vectorised from this string in this font
// (a WxFontUtils descriptor, bold/italic included) at this cap height in mm. The regions are
// what gets built — they are saved too, so the project opens on a machine without the font —
// and these three are what an edit reopens the Text dialog with. Empty = not a text feature.
std::string text_string;
std::string text_font;
double text_height{0.0};
bool is_text() const { return !text_string.empty(); }
std::vector<int> dressup_edge_ids() const {
if (!dressup_edges.empty()) return dressup_edges;
if (dressup_edge >= 0) return { dressup_edge };
return {};
}
// Hole params (positioned circular cut into the current body)
double hole_diameter{5};
@@ -134,12 +153,21 @@ struct CadFeature {
std::string hole_standard; // provenance only, e.g. "M6" / "1/4-20"; not used by geometry
// Thread params (helical thread about the plane normal at a positioned point)
double thread_radius{5}; // nominal cylinder radius
double thread_radius{5}; // MAJOR (nominal) radius when thread_major_nominal,
// else the legacy reference radius (see below)
double thread_pitch{2}; // axial advance per turn
double thread_height{10}; // total axial length
double thread_depth{1}; // radial crest depth of the thread profile
bool thread_internal{false}; // false = external threaded rod (New body);
// true = tapped bore cut into the current body
double thread_depth{1}; // radial depth of the thread profile
bool thread_internal{false}; // false = external: the thread goes on the target body
// (or on a standalone rod when there is none);
// true = tapped bore cut into the target body
// How thread_radius is read. true (every thread made since): it is the nominal MAJOR
// radius, for both kinds — an internal thread bores to R - depth and grooves out to R, an
// external one is a rod of radius R with its groove cut in to R - depth, so an M6 is 6 mm
// across its crests either way. false (older recipes, kept bit-for-bit): the ridge was
// added OUTSIDE R, so an internal thread given the minor diameter came out undersized and
// an external one given the major diameter came out oversized.
bool thread_major_nominal{false};
double thread_x{0}; // axis position on the plane (u/x axis)
double thread_y{0}; // axis position on the plane (v/y axis)
@@ -156,6 +184,9 @@ struct CadFeature {
// target_body. revolve_axis: 0 = plane X axis, 1 = plane Y axis.
double revolve_angle{360}; // sweep angle in degrees (1..360)
int revolve_axis{0}; // 0 = plane X, 1 = plane Y
// A Line of the profile sketch to revolve about instead (index into its entities: a
// centerline, usually construction, or an edge of the profile itself); -1 = revolve_axis.
int revolve_axis_entity{-1};
// Sweep: profile carried by sketch_ref / entities (like Extrude); the spine is a
// second Sketch referenced by sweep_path_ref (an open or closed wire). Reuses
@@ -178,6 +209,12 @@ struct CadFeature {
double pattern_spacing{20}; // linear step (mm)
int pattern_dir{0}; // linear direction: 0 = plane X, 1 = plane Y
double pattern_angle{360}; // circular total angle (degrees)
// How a circular pattern spreads its copies over pattern_angle. true (every pattern made
// since): a full turn is split into `count` equal steps, anything less is spanned end to end,
// first copy at 0 and last at pattern_angle — the way a pattern along a curve spans its
// curve. false (older recipes, kept as they were built): always angle / count, so 90° with 3
// copies stopped at 60°.
bool pattern_inclusive{false};
// Pattern along a curve: when pattern_curve_sketch >= 0 this mode takes precedence over
// linear/circular. Copies are placed at equal-parameter points along entity
@@ -190,6 +227,16 @@ struct CadFeature {
// BEFORE geometry runs. Empty (the common case) means the feature uses its literal fields.
std::map<std::string, std::string> expr;
// Transient, never serialized. A body is referred to above by its INDEX, and an index is
// only meaningful against the history it was taken in: delete, reorder or disable a
// feature that makes a body and every later index points at a different body. So recompute
// records, for each body field, the identity of the body it resolved to — the feature that
// made it, and which of that feature's bodies it is — and when the history changes the
// fields are re-pointed from those identities (see CadDocument::recompute).
struct BodyId { int src{-1}; int ord{0}; };
std::vector<BodyId> body_ref_ids;
bool body_refs_pending{false};
// Datum/reference plane: a derived SketchPlane the document offers as a selectable
// sketch plane (no solid). plane_base selects the reference (0=XY,1=XZ,2=YZ, or 3+N
// = the Nth earlier datum plane); plane_offset shifts along the base normal;
@@ -361,10 +408,62 @@ struct CadFeature {
pattern_curve_sketch, pattern_curve_entity,
expr,
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
coordsys_face_kind, coordsys_face_edges);
coordsys_face_kind, coordsys_face_edges,
thread_major_nominal, pattern_inclusive,
dressup_edges,
text_string, text_font, text_height,
revolve_axis_entity);
}
template<class Archive>
void load(Archive& ar) {
std::string brep;
ar(type, name, enabled, shape, plane, width, height, radius,
profile, entities, constraints, entity_constraints, imported_regions,
import_offset, import_scale_x, import_scale_y, import_on_face, import_face_body,
sketch_ref, distance, symmetric, mode, extrude_end, distance2, taper_deg, flip,
up_to_face, extrude_src_face, up_to_point, target_body,
dressup_size, face_group, dressup_edge,
hole_diameter, hole_depth, hole_through, hole_x, hole_y,
thread_radius, thread_pitch, thread_height, thread_depth, thread_internal, thread_x, thread_y,
shell_thickness, shell_face,
draft_face, draft_angle,
revolve_angle, revolve_axis,
sweep_path_ref, loft_profile_refs, loft_ruled,
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
plane_base, plane_offset, plane_angle_tilt, plane_axis,
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
brep,
plane_type, plane_face_body, plane_face, plane_face2_body, plane_face2,
plane_edge_body, plane_edge, plane_edge2_body, plane_edge2, plane_u_size, plane_v_size,
mirror_keep_original,
axis_type, axis_p1, axis_p2, axis_body, axis_face, axis_edge, axis_plane_a, axis_plane_b,
coordsys_type, coordsys_point, coordsys_body, coordsys_face, coordsys_edge, coordsys_x_hint,
helix_radius, helix_pitch, helix_height, helix_left_handed, helix_taper_deg,
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
thicken_face, thicken_thickness, thicken_flip,
cut_face_body, cut_face,
project_source_body, project_edges, project_face,
delete_faces,
hole_style, hole_cbore_diameter, hole_cbore_depth,
hole_csink_diameter, hole_csink_angle, hole_standard,
rib_sketch_ref, rib_entity, rib_thickness, rib_depth,
pattern_curve_sketch, pattern_curve_entity,
expr,
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
coordsys_face_kind, coordsys_face_edges,
thread_major_nominal, pattern_inclusive,
dressup_edges,
text_string, text_font, text_height,
revolve_axis_entity);
imported_solid = brep_from_string(brep);
}
// The pre-framing (v4) layout, FROZEN. A v4 recipe is one flat stream with no per-feature
// length, so it can only be read with exactly the field list it was written with; reading it
// through load() above breaks the moment a field is appended there (every field added since
// v5 is appended ONLY above, never here).
template<class Archive>
void load_flat_v4(Archive& ar) {
std::string brep;
ar(type, name, enabled, shape, plane, width, height, radius,
profile, entities, constraints, entity_constraints, imported_regions,
@@ -403,6 +502,7 @@ struct CadFeature {
coordsys_face_kind, coordsys_face_edges);
imported_solid = brep_from_string(brep);
}
};
// Serialize a TopoDS_Shape to/from a BRep string for cereal persistence.
@@ -440,6 +540,13 @@ public:
// Named document variables: name -> expression. Evaluated topologically each recompute();
// an expression may reference other variables. Feature `expr` bindings resolve against these.
std::map<std::string, std::string> variables;
// Can a feature expression drive this numeric field? The single allow-list the evaluator
// uses, so a caller can refuse a name before it breaks the next recompute.
static bool is_bindable_field(const std::string& field);
// Does a feature of this type leave a body behind? Sketches, datums, the helix curve and
// Project (which emits sketch entities) do not. The one answer recompute(), the rollback
// rule and the GUI's preview all use.
static bool produces_body(CadFeatureType t);
// Multi-body result of the last replay. A "New" extrude appends a body; other ops
// mutate a target body. Empty after a failed/empty recompute.
std::vector<CadBody> bodies;
@@ -459,8 +566,16 @@ public:
// Modeling origin: the world point the default XY/XZ/YZ planes pass through. The GUI sets this
// to the bed centre so sketches/datums land in the middle of the bed (not the bed corner =
// world 0). Not serialized — the GUI re-applies it from the live bed on every tab show.
// world 0) — for a NEW document. It is saved with the recipe: sketches bake it into their
// planes while datum planes add it when they are resolved, so a project reopened on another
// printer must keep the origin it was made with or its datums move and its sketches do not.
Vec3d modeling_origin{Vec3d::Zero()};
bool origin_from_recipe{false}; // modeling_origin came from the loaded project
// Weld sketch endpoints within kSketchJoinTol (the "Auto-close sketch loops" preference, taken
// when the document is started). A property of the DOCUMENT, saved with it: as a machine-wide
// preference it made one project rebuild into a closed solid on one computer and an open
// loop on another. recompute() pushes it into the kernel.
bool auto_close_loops{true};
// Tessellation quality, matched to Orca's OWN STEP importer (Format/STEP.hpp defaults:
// linear 0.003, angular 0.5 rad) so a body modelled here reaches the screen at the same
@@ -513,8 +628,10 @@ public:
BooleanMode mode, const std::string& name);
int add_fillet(double radius, FaceGroup faces, const std::string& name);
int add_fillet(double radius, int edge_id, const std::string& name);
int add_fillet(double radius, const std::vector<int>& edge_ids, const std::string& name);
int add_chamfer(double distance, FaceGroup faces, const std::string& name);
int add_chamfer(double distance, int edge_id, const std::string& name);
int add_chamfer(double distance, const std::vector<int>& edge_ids, const std::string& name);
int add_hole(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane,
const std::string& name);
@@ -707,6 +824,9 @@ public:
// an Extrude, its sketch_ref are preserved from the original).
bool remove_feature(int index);
bool move_feature(int index, int delta);
// Show or hide a feature. Transactional like the others, and it keeps body and datum-plane
// references pointing at the same objects, which a bare `enabled` flip does not.
bool set_feature_enabled(int index, bool enabled);
bool replace_feature(int index, const CadFeature& edited);
// replace_sketch_extrude: a box is two linked features (Sketch + Extrude);
// overwrite both slots from one `edited` candidate (sketch params ->
@@ -718,6 +838,9 @@ public:
// tessellate the result into out_mesh, WITHOUT modifying features/body/
// display_mesh. Returns false (with err set) if the candidate is invalid.
// Used by the Design tab to show a translucent ghost before Confirm.
// The solid body the closed profile of sketch `sketch_ref` lies on or touches, or -1. Drives
// the Extrude default: a profile drawn on a body joins it, one in free space is a new body.
int body_touching_sketch(int sketch_ref) const;
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh, std::string& err) const;
// Same, but also returns the per-body meshes (in `bodies` order; the candidate may append
// one), so the GUI can apply its display-only per-body Move transforms to the ghost and keep
@@ -725,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
@@ -741,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,
+84 -45
View File
@@ -1,6 +1,7 @@
#include "libslic3r/CAD/GeometryEngine.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/I18N.hpp"
#include <BRepMesh_IncrementalMesh.hxx>
#include <BRep_Tool.hxx>
@@ -73,19 +74,19 @@ std::vector<TopoDS_Shape> GeometryEngine::read_step_solids(const std::string& pa
try {
STEPControl_Reader reader;
if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) {
err = "cannot read STEP file";
err = _u8L("cannot read STEP file");
return out;
}
reader.TransferRoots();
const TopoDS_Shape shape = reader.OneShape();
if (shape.IsNull()) { err = "STEP file has no geometry"; return out; }
if (shape.IsNull()) { err = _u8L("STEP file has no geometry"); return out; }
// One body per top-level solid; fall back to the whole shape (shells/faces) if none.
for (TopExp_Explorer ex(shape, TopAbs_SOLID); ex.More(); ex.Next())
out.push_back(ex.Current());
if (out.empty())
out.push_back(shape);
} catch (const Standard_Failure& e) {
err = *e.what() ? e.what() : "OCCT failed to read STEP";
err = *e.what() ? e.what() : _u8L("OCCT failed to read STEP");
out.clear();
}
return out;
@@ -115,7 +116,7 @@ TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
if (tolerance <= 0.0)
throw std::runtime_error("mesh_to_brep: tolerance must be > 0");
if (its.indices.empty())
throw std::runtime_error("mesh_to_brep: mesh has no triangles");
throw std::runtime_error(_u8L("mesh_to_brep: mesh has no triangles"));
// 1. Tolerance-quantized vertex dedup. A merged vertex keeps the exact coordinates of the
// first input occurrence — vertices are grouped by a cell, never snapped onto its grid.
@@ -150,8 +151,8 @@ TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
}
stats.kept_tris = int(tris.size());
if (tris.empty())
throw std::runtime_error("mesh_to_brep: every triangle was rejected as degenerate "
"(try a smaller tolerance)");
throw std::runtime_error(_u8L("mesh_to_brep: every triangle was rejected as degenerate "
"(try a smaller tolerance)"));
// 3. One face per triangle, sharing vertices and edges through the caches.
std::vector<TopoDS_Vertex> vertex_cache(verts.size());
@@ -294,21 +295,21 @@ FaceGroup GeometryEngine::classify_face(const TopoDS_Face& face, const TopoDS_Sh
std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid, FaceGroup target)
{
// Each edge ONCE. An explorer visits a shared edge from both of its faces, so walking one
// handed every edge to the fillet twice; the de-duplicated map is also what edge_by_index
// numbers edges by.
std::vector<TopoDS_Edge> result;
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> edgeFaceMap;
TopExp::MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap);
if (target == FaceGroup::All) {
for (TopExp_Explorer exp(solid, TopAbs_EDGE); exp.More(); exp.Next())
result.push_back(TopoDS::Edge(exp.Current()));
for (int i = 1; i <= edgeFaceMap.Extent(); ++i)
result.push_back(TopoDS::Edge(edgeFaceMap.FindKey(i)));
return result;
}
// Build edge-to-face map once
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> edgeFaceMap;
TopExp::MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap);
for (TopExp_Explorer edgeExp(solid, TopAbs_EDGE); edgeExp.More(); edgeExp.Next()) {
const TopoDS_Edge& edge = TopoDS::Edge(edgeExp.Current());
if (!edgeFaceMap.Contains(edge)) continue;
const NCollection_List<TopoDS_Shape>& faces = edgeFaceMap.FindFromKey(edge);
for (int ei = 1; ei <= edgeFaceMap.Extent(); ++ei) {
const TopoDS_Edge& edge = TopoDS::Edge(edgeFaceMap.FindKey(ei));
const NCollection_List<TopoDS_Shape>& faces = edgeFaceMap.FindFromIndex(ei);
bool include = false;
for (auto it = faces.begin(); it != faces.end(); ++it) {
@@ -344,12 +345,14 @@ std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid
// ---- Fillet/Chamfer ----
// All four dress-up entry points fail the same way — with a reason — instead of two of them
// handing the solid back unchanged, which recompute then reported as a success.
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces)
{
if (radius <= 0.001) return solid;
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) return solid;
if (edges.empty()) throw std::runtime_error("no edges to fillet in that group");
BRepFilletAPI_MakeFillet fillet(solid);
for (const auto& edge : edges)
@@ -359,53 +362,67 @@ TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radi
// A too-large radius (e.g. >= half the smallest spanned dimension) makes the
// operation degenerate; OCCT leaves IsDone() false. Report it instead of
// silently returning the unfilleted solid (which reads as a false success).
if (!fillet.IsDone()) throw std::runtime_error("fillet radius too large for this geometry");
if (!fillet.IsDone()) throw std::runtime_error(_u8L("fillet radius too large for this geometry"));
return fillet.Shape();
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, FaceGroup faces)
{
if (distance <= 0.001) return solid;
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) return solid;
if (edges.empty()) throw std::runtime_error("no edges to chamfer in that group");
BRepFilletAPI_MakeChamfer chamfer(solid);
for (const auto& edge : edges)
chamfer.Add(distance, edge); // symmetric chamfer
chamfer.Build();
if (!chamfer.IsDone()) throw std::runtime_error("chamfer distance too large for this geometry");
if (!chamfer.IsDone()) throw std::runtime_error(_u8L("chamfer distance too large for this geometry"));
return chamfer.Shape();
}
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, int edge_id)
{
if (radius <= 0.001) return solid;
TopoDS_Edge edge = edge_by_index(solid, edge_id);
if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id");
BRepFilletAPI_MakeFillet mk(solid);
mk.Add(radius, edge);
mk.Build();
if (!mk.IsDone()) throw std::runtime_error("apply_fillet: OCCT fillet failed");
return mk.Shape();
return apply_fillet(solid, radius, std::vector<int>{ edge_id });
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id)
{
if (distance <= 0.001) return solid;
return apply_chamfer(solid, distance, std::vector<int>{ edge_id });
}
TopoDS_Edge edge = edge_by_index(solid, edge_id);
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, const std::vector<int>& edge_ids)
{
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
if (edge_ids.empty()) throw std::runtime_error("apply_fillet: no edge picked");
BRepFilletAPI_MakeChamfer mk(solid);
mk.Add(distance, edge);
BRepFilletAPI_MakeFillet mk(solid);
for (int id : edge_ids) {
TopoDS_Edge edge = edge_by_index(solid, id);
if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id");
mk.Add(radius, edge);
}
mk.Build();
if (!mk.IsDone()) throw std::runtime_error("apply_chamfer: OCCT chamfer failed");
if (!mk.IsDone()) throw std::runtime_error(_u8L("apply_fillet: OCCT fillet failed"));
return mk.Shape();
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, const std::vector<int>& edge_ids)
{
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
if (edge_ids.empty()) throw std::runtime_error("apply_chamfer: no edge picked");
BRepFilletAPI_MakeChamfer mk(solid);
for (int id : edge_ids) {
TopoDS_Edge edge = edge_by_index(solid, id);
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
mk.Add(distance, edge);
}
mk.Build();
if (!mk.IsDone()) throw std::runtime_error(_u8L("apply_chamfer: OCCT chamfer failed"));
return mk.Shape();
}
@@ -531,12 +548,13 @@ GeometryEngine::MassProps GeometryEngine::mass_properties(const TopoDS_Shape& sh
std::string GeometryEngine::primitive_name(PrimitiveType type)
{
switch (type) {
case PrimitiveType::Box: return "Box";
case PrimitiveType::Cylinder: return "Cylinder";
case PrimitiveType::Sphere: return "Sphere";
case PrimitiveType::Cone: return "Cone";
case PrimitiveType::Torus: return "Torus";
default: return "Unknown";
// TRN Default name of an object created from the box primitive shape.
case PrimitiveType::Box: return _u8L("Box");
case PrimitiveType::Cylinder: return _u8L("Cylinder");
case PrimitiveType::Sphere: return _u8L("Sphere");
case PrimitiveType::Cone: return _u8L("Cone");
case PrimitiveType::Torus: return _u8L("Torus");
default: return _u8L("Unknown");
}
}
@@ -614,6 +632,27 @@ std::vector<Vec3d> GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, do
return pts;
}
std::vector<std::vector<Vec3d>> GeometryEngine::display_edges(const TopoDS_Shape& shape, double chord_tol)
{
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> faces_of_edge;
TopExp::MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, faces_of_edge);
std::vector<std::vector<Vec3d>> out;
for (int i = 1; i <= faces_of_edge.Extent(); ++i) {
const TopoDS_Edge& edge = TopoDS::Edge(faces_of_edge.FindKey(i));
if (BRep_Tool::Degenerated(edge))
continue;
bool seam = false;
for (NCollection_List<TopoDS_Shape>::Iterator it(faces_of_edge.FindFromIndex(i)); it.More() && !seam; it.Next())
seam = BRep_Tool::IsClosed(edge, TopoDS::Face(it.Value()));
if (seam)
continue;
std::vector<Vec3d> pts = sample_edge_world(edge, chord_tol);
if (pts.size() >= 2)
out.push_back(std::move(pts));
}
return out;
}
Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face)
{
GProp_GProps props;
+13 -3
View File
@@ -39,8 +39,9 @@ struct PrimitiveParams {
double dressup_radius{1.0}; // fillet radius
double dressup_chamfer_dist{1.0}; // chamfer distance (symmetric)
// Mesh quality
double linear_deflection{0.01};
// Mesh quality — the Design tab's own density (CadDocument::linear_deflection), so a
// primitive and the same body modelled in the Design tab reach the screen alike.
double linear_deflection{0.003};
double angular_deflection{0.5};
template<class Archive>
@@ -122,9 +123,14 @@ public:
FaceGroup faces = FaceGroup::All);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
int edge_id);
// Several edges in one operation, all ids resolved against `solid`.
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
const std::vector<int>& edge_ids);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
const std::vector<int>& edge_ids);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
double linear_deflection = 0.01,
double linear_deflection = 0.003,
double angular_deflection = 0.5);
static std::string primitive_name(PrimitiveType type);
@@ -146,6 +152,10 @@ public:
static Vec3d face_normal_world(const TopoDS_Face& face);
// Sample an edge into a world-space polyline (>=2 pts) for pick-distance + highlight.
static std::vector<Vec3d> sample_edge_world(const TopoDS_Edge& edge, double chord_tol = 0.05);
// The edges a viewer draws over a body, each as a polyline: every edge of the shape once,
// without degenerate edges (a cone apex) and without the seam of a closed surface (the line
// down a cylinder's side), which is where OCCT closes the parameter space, not a real edge.
static std::vector<std::vector<Vec3d>> display_edges(const TopoDS_Shape& shape, double chord_tol);
// 0-based edge index into TopExp::MapShapes(shape, TopAbs_EDGE, map).
static int edge_count(const TopoDS_Shape& shape);
static TopoDS_Edge edge_by_index(const TopoDS_Shape& shape, int index);
+291 -89
View File
@@ -1,6 +1,7 @@
#include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/I18N.hpp"
#include <Standard_Handle.hxx>
#include <GeomAbs_SurfaceType.hxx>
@@ -19,6 +20,8 @@
#include <BRepBuilderAPI_MakeVertex.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepAdaptor_Curve.hxx>
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <BRepClass_FaceClassifier.hxx>
@@ -56,6 +59,7 @@
#include <TopoDS_Wire.hxx>
#include <GeomAPI_IntCS.hxx>
#include <map>
#include <atomic>
#include <math.h>
#include <tuple>
#include <stdexcept>
@@ -66,24 +70,18 @@ class Geom_TrimmedCurve;
namespace Slic3r {
// Single source of truth for the weld tolerance the viewport and the kernel share.
// Defaults ON so headless/kernel-only callers keep welding; the GUI pushes the
// "auto_close_sketch_loops" preference in via set_sketch_auto_close().
static bool s_auto_close = true;
// Single source of truth for the weld tolerance the viewport and the kernel share. Defaults ON
// so headless/kernel-only callers keep welding. It is the DOCUMENT's setting
// (CadDocument::auto_close_loops, saved with the recipe), pushed in by CadDocument::recompute:
// the same project must rebuild into the same solid on every machine. Atomic because the GUI
// rebuilds on a worker thread while the viewport reads it.
static std::atomic<bool> s_auto_close{true};
double sketch_join_tol() { return s_auto_close ? kSketchJoinTol : 0.0; }
void set_sketch_auto_close(bool on) { s_auto_close = on; }
double sketch_join_tol() { return s_auto_close.load() ? kSketchJoinTol : 0.0; }
void set_sketch_auto_close(bool on) { s_auto_close.store(on); }
// ---- SketchPlane ----
gp_Pln SketchPlane::to_occt() const
{
gp_Pnt o(origin.x(), origin.y(), origin.z());
gp_Dir n(normal.x(), normal.y(), normal.z());
gp_Dir x(x_axis.x(), x_axis.y(), x_axis.z());
return gp_Pln(gp_Ax3(o, n, x));
}
SketchPlane SketchPlane::from_face(const TopoDS_Face& face)
{
SketchPlane sp;
@@ -138,30 +136,10 @@ Vec3d SketchPlane::to_world(const Vec2d& pt) const
// ---- SketchProfile ----
bool SketchProfile::is_closed(double tolerance) const
{
if (points.size() < 3) return false;
return (points.front() - points.back()).norm() < tolerance;
}
bool SketchProfile::try_close(double tolerance)
{
if (is_closed(tolerance)) {
closed = true;
return true;
}
if (points.size() < 2) return false;
if ((points.front() - points.back()).norm() < tolerance) {
closed = true;
return true;
}
return false;
}
TopoDS_Wire SketchProfile::to_occt_wire(const SketchPlane& plane) const
{
if (points.size() < 2)
throw std::runtime_error("Profile has fewer than 2 points");
throw std::runtime_error(_u8L("Profile has fewer than 2 points"));
BRepBuilderAPI_MakeWire builder;
for (size_t i = 0; i < points.size(); ++i) {
@@ -173,7 +151,7 @@ TopoDS_Wire SketchProfile::to_occt_wire(const SketchPlane& plane) const
}
builder.Build();
if (!builder.IsDone())
throw std::runtime_error("Failed to build wire from profile");
throw std::runtime_error(_u8L("Failed to build wire from profile"));
return builder.Wire();
}
@@ -181,9 +159,9 @@ TopoDS_Wire SketchProfile::to_occt_wire(const SketchPlane& plane) const
TopoDS_Shape SketchEngine::make_prism(const TopoDS_Shape& base, const gp_Vec& vec)
{
if (vec.Magnitude() < 1e-9) throw std::runtime_error("extrude depth is zero");
if (vec.Magnitude() < 1e-9) throw std::runtime_error(_u8L("extrude depth is zero"));
BRepPrimAPI_MakePrism prism(base, vec);
if (!prism.IsDone()) throw std::runtime_error("extrude failed");
if (!prism.IsDone()) throw std::runtime_error(_u8L("extrude failed"));
return prism.Shape();
}
@@ -192,7 +170,7 @@ static TopoDS_Shape extrude_face_internal(const TopoDS_Face& face, const gp_Dir&
if (symmetric) {
gp_Vec halfVec = gp_Vec(dir) * (length / 2.0);
BRepAlgoAPI_Fuse fuse(SketchEngine::make_prism(face, halfVec), SketchEngine::make_prism(face, -halfVec));
if (!fuse.IsDone()) throw std::runtime_error("Fuse failed");
if (!fuse.IsDone()) throw std::runtime_error(_u8L("Fuse failed"));
return fuse.Shape();
}
return SketchEngine::make_prism(face, gp_Vec(dir) * length);
@@ -202,7 +180,7 @@ TopoDS_Shape SketchEngine::make_extrude(const TopoDS_Wire& wire, const SketchPla
double length, bool symmetric, double taper_deg)
{
BRepBuilderAPI_MakeFace fm(wire);
if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire");
if (!fm.IsDone()) throw std::runtime_error(_u8L("Failed to make face from wire"));
return make_extrude(fm.Face(), plane, length, symmetric, taper_deg);
}
@@ -217,7 +195,7 @@ TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Wire& wire, const
double up, double down)
{
BRepBuilderAPI_MakeFace fm(wire);
if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire");
if (!fm.IsDone()) throw std::runtime_error(_u8L("Failed to make face from wire"));
return make_extrude_two_sided(fm.Face(), plane, up, down);
}
@@ -229,7 +207,7 @@ TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Face& face, const
if (d < 1e-9) return make_prism(face, gp_Vec(dir) * u);
if (u < 1e-9) return make_prism(face, gp_Vec(dir) * -d);
BRepAlgoAPI_Fuse fuse(make_prism(face, gp_Vec(dir) * u), make_prism(face, gp_Vec(dir) * -d));
if (!fuse.IsDone()) throw std::runtime_error("two-sided extrude fuse failed");
if (!fuse.IsDone()) throw std::runtime_error(_u8L("two-sided extrude fuse failed"));
return fuse.Shape();
}
@@ -239,7 +217,7 @@ TopoDS_Shape SketchEngine::make_extrude_taper(const TopoDS_Wire& wire, const Ske
gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z());
auto straight = [&]() -> TopoDS_Shape {
BRepBuilderAPI_MakeFace fm(wire);
if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire");
if (!fm.IsDone()) throw std::runtime_error(_u8L("Failed to make face from wire"));
return make_prism(fm.Face(), gp_Vec(dir) * length);
};
if (std::abs(taper_deg) >= 89.0 || std::abs(length) < 1e-9) return straight();
@@ -285,14 +263,16 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
const SketchPlane& plane, double length, bool symmetric)
{
// The loop below skips regions that fail, so a zero depth is rejected before it.
if (std::abs(length) < 1e-9) throw std::runtime_error("extrude depth is zero");
if (std::abs(length) < 1e-9) throw std::runtime_error(_u8L("extrude depth is zero"));
// Drop consecutive coincident points and the closing duplicate. FreeType /
// SVG flattening routinely emits repeated points which would build a
// degenerate OCCT edge and make the wire builder throw — sanitising keeps a
// single bad glyph from killing the whole extrude.
auto clean = [](const std::vector<Vec2d>& pts) {
const double eps2 = 1e-12; // ~1e-6 mm
// The sketch's own joint tolerance: points closer than this are one point everywhere
// else in the sketcher, so they must not become a sub-micron edge here either.
const double eps2 = kSketchJoinTol * kSketchJoinTol;
std::vector<Vec2d> out;
out.reserve(pts.size());
for (const Vec2d& p : pts)
@@ -320,6 +300,10 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
};
gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z());
// Faces are built ON the sketch plane, as wires_to_face does: a surface inferred from the
// outer wire need not share the sketch normal, and then the extrude direction and the hole
// classification disagree with it.
const gp_Pln pln(gp_Pnt(plane.origin.x(), plane.origin.y(), plane.origin.z()), dir);
// Accumulate each region's solid into a compound rather than boolean-fusing:
// glyphs are independent profiles, so a compound avoids every boolean-failure
@@ -340,7 +324,10 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
// classify outer vs holes by area/containment and set correct wire
// orientations. This is winding-independent, so holed glyphs extrude
// with a solid body and empty counters regardless of source winding.
BRepBuilderAPI_MakeFace fm(outer);
// Probe on the inferred surface first: naming a plane makes MakeFace accept a wire
// that bounds nothing, and this check is what skips such a contour.
if (!BRepBuilderAPI_MakeFace(outer).IsDone()) continue;
BRepBuilderAPI_MakeFace fm(pln, outer);
if (!fm.IsDone()) continue;
for (size_t h = 1; h < region.size(); ++h) {
TopoDS_Wire hole = contour_wire(region[h]);
@@ -362,31 +349,48 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
}
}
if (count == 0) throw std::runtime_error("imported regions produced no extrudable geometry");
if (count == 0) throw std::runtime_error(_u8L("imported regions produced no extrudable geometry"));
return count == 1 ? last : TopoDS_Shape(comp); // avoid a compound-of-one
}
TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane,
double angle_deg, int axis_sel)
TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const gp_Ax1& axis_in, double angle_deg)
{
BRepBuilderAPI_MakeFace faceMaker(wire);
if (!faceMaker.IsDone())
throw std::runtime_error("Failed to make face from wire");
throw std::runtime_error(_u8L("Failed to make face from wire"));
TopoDS_Face face = faceMaker.Face();
// Revolution axis lies in the sketch plane through its origin: X (0) or Y (1).
const Vec3d& adir = (axis_sel == 1) ? plane.y_axis : plane.x_axis;
gp_Pnt o(plane.origin.x(), plane.origin.y(), plane.origin.z());
gp_Dir xd(adir.x(), adir.y(), adir.z());
gp_Ax1 axis(o, xd);
// A profile on both sides of the axis sweeps through itself; MakeRevol then fails with no
// reason, or builds an invalid solid. Sample every edge and name the cause instead.
{
const gp_Pnt o = axis_in.Location();
const gp_Dir d = axis_in.Direction();
bool pos = false, neg = false;
gp_Vec side_ref;
for (TopExp_Explorer ex(wire, TopAbs_EDGE); ex.More(); ex.Next()) {
BRepAdaptor_Curve c(TopoDS::Edge(ex.Current()));
for (int i = 0; i <= 16; ++i) {
const gp_Pnt p = c.Value(c.FirstParameter() + (c.LastParameter() - c.FirstParameter()) * i / 16.0);
const gp_Vec off = gp_Vec(o, p) - gp_Vec(d) * gp_Vec(o, p).Dot(gp_Vec(d)); // from the axis
if (off.Magnitude() < 1e-6)
continue;
if (side_ref.Magnitude() == 0.0) { side_ref = off; pos = true; continue; }
(off.Dot(side_ref) > 0.0 ? pos : neg) = true;
}
}
if (pos && neg)
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
}
gp_Ax1 axis = axis_in;
double angle_rad = angle_deg * M_PI / 180.0;
// A negative angle is expressed as a positive sweep about the reversed axis,
// since BRepPrimAPI_MakeRevol expects an angle in (0, 2*pi].
if (angle_rad < 0) { axis.Reverse(); angle_rad = -angle_rad; }
BRepPrimAPI_MakeRevol rev(face, axis, angle_rad);
if (!rev.IsDone())
throw std::runtime_error("Failed to revolve");
throw std::runtime_error(_u8L("Failed to revolve"));
if (!BRepCheck_Analyzer(rev.Shape()).IsValid())
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
return rev.Shape();
}
@@ -394,29 +398,29 @@ TopoDS_Shape SketchEngine::make_sweep(const TopoDS_Wire& profile, const TopoDS_W
{
BRepBuilderAPI_MakeFace faceMaker(profile);
if (!faceMaker.IsDone())
throw std::runtime_error("Failed to make face from sweep profile");
throw std::runtime_error(_u8L("Failed to make face from sweep profile"));
TopoDS_Face face = faceMaker.Face();
BRepOffsetAPI_MakePipe pipe(path, face);
pipe.Build();
if (!pipe.IsDone())
throw std::runtime_error("Failed to sweep profile along path");
throw std::runtime_error(_u8L("Failed to sweep profile along path"));
return pipe.Shape();
}
TopoDS_Shape SketchEngine::make_loft(const std::vector<TopoDS_Wire>& profiles, bool ruled)
{
if (profiles.size() < 2)
throw std::runtime_error("loft needs at least 2 profiles");
throw std::runtime_error(_u8L("loft needs at least 2 profiles"));
BRepOffsetAPI_ThruSections loft(true /*solid*/, ruled);
for (const TopoDS_Wire& w : profiles) {
if (w.IsNull()) throw std::runtime_error("loft: null profile wire");
loft.AddWire(w);
}
loft.Build();
if (!loft.IsDone()) throw std::runtime_error("loft failed");
if (!loft.IsDone()) throw std::runtime_error(_u8L("loft failed"));
TopoDS_Shape s = loft.Shape();
if (s.IsNull()) throw std::runtime_error("loft produced no solid");
if (s.IsNull()) throw std::runtime_error(_u8L("loft produced no solid"));
return s;
}
@@ -424,16 +428,16 @@ TopoDS_Shape SketchEngine::make_loft(const std::vector<TopoDS_Wire>& profiles, b
TopoDS_Shape SketchEngine::make_loft_surface(const std::vector<TopoDS_Wire>& profiles, bool ruled)
{
if (profiles.size() < 2)
throw std::runtime_error("loft needs at least 2 profiles");
throw std::runtime_error(_u8L("loft needs at least 2 profiles"));
BRepOffsetAPI_ThruSections loft(false /*shell, no end caps*/, ruled);
for (const TopoDS_Wire& w : profiles) {
if (w.IsNull()) throw std::runtime_error("loft: null profile wire");
loft.AddWire(w);
}
loft.Build();
if (!loft.IsDone()) throw std::runtime_error("loft failed");
if (!loft.IsDone()) throw std::runtime_error(_u8L("loft failed"));
TopoDS_Shape s = loft.Shape();
if (s.IsNull()) throw std::runtime_error("loft produced no shape");
if (s.IsNull()) throw std::runtime_error(_u8L("loft produced no shape"));
return s;
}
@@ -441,11 +445,11 @@ TopoDS_Shape SketchEngine::make_pocket(const TopoDS_Wire& wire, const SketchPlan
const TopoDS_Shape& target, double depth)
{
BRepBuilderAPI_MakeFace fm(wire);
if (!fm.IsDone()) throw std::runtime_error("Pocket face failed");
if (!fm.IsDone()) throw std::runtime_error(_u8L("Pocket face failed"));
TopoDS_Shape tool = extrude_face_internal(fm.Face(),
gp_Dir(plane.normal.x(), plane.normal.y(), plane.normal.z()), depth + 1.0, false);
BRepAlgoAPI_Cut cut(target, tool);
if (!cut.IsDone()) throw std::runtime_error("Pocket cut failed");
if (!cut.IsDone()) throw std::runtime_error(_u8L("Pocket cut failed"));
return cut.Shape();
}
@@ -581,7 +585,12 @@ std::vector<TopoDS_Wire> SketchEngine::entities_to_wires(const std::vector<Sketc
auto make_elips = [&](const SketchEntity& c) -> gp_Elips {
Vec3d c3 = plane.to_world(c.center);
gp_Pnt center(c3.x(), c3.y(), c3.z());
gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z());
// The frame's normal is x_axis x y_axis, not plane.normal: OCCT takes the ellipse's Y
// as N x X, and the parametric angles were measured in the sketch's own (x, y). The two
// agree on XY and YZ; the XZ base plane stores normal = +Y while x x y = -Y, so there
// every elliptical arc came out mirrored against what the sketch showed.
const Vec3d nz = plane.x_axis.cross(plane.y_axis);
gp_Dir n(nz.x(), nz.y(), nz.z());
Vec2d maj2(std::cos(c.rotation), std::sin(c.rotation));
Vec3d x3 = plane.to_world(c.center + maj2) - c3;
gp_Dir xdir(x3.x(), x3.y(), x3.z());
@@ -705,6 +714,9 @@ std::vector<TopoDS_Wire> SketchEngine::entities_to_wires(const std::vector<Sketc
if (c.radius <= 1e-9 || c.rminor <= 1e-9) return {};
e = BRepBuilderAPI_MakeEdge(make_elips(c)).Edge();
} else {
// Same guard as the ellipse above: a zero radius would reach OCCT and throw from
// .Edge() instead of reporting a sketch that cannot be built.
if (c.radius <= 1e-9) return {};
Vec3d c3 = plane.to_world(c.center);
gp_Pnt center(c3.x(), c3.y(), c3.z());
gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z());
@@ -903,10 +915,22 @@ std::vector<Vec2d> sketch_open_ends(const std::vector<SketchEntity>& entities,
return out;
}
// A closed wire can still fail to bound a region: a loop that crosses itself, or one that
// doubles back along itself (a cusp, e.g. an arc leaving a line tangent to it but in the
// opposite direction). MakeFace reports success on both and the prism built from the face is
// an invalid solid with no caps. Refuse it here, with the reason, instead of shipping that.
static TopoDS_Face checked_profile_face(const TopoDS_Face& f)
{
BRepCheck_Analyzer an(f);
if (!an.IsValid())
throw std::runtime_error("the profile crosses or folds back on itself, so it does not bound one region");
return f;
}
TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
const SketchPlane& plane)
{
if (wires.empty()) throw std::runtime_error("sketch has no closed loop");
if (wires.empty()) throw std::runtime_error(_u8L("sketch has no closed loop"));
// The ASSEMBLED face below is built on the SKETCH's own plane rather than on a surface OCCT
// infers from the outer wire. The inferred plane has no reason to share the sketch's normal,
@@ -921,8 +945,8 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
if (wires.size() == 1) {
BRepBuilderAPI_MakeFace fm(wires[0]);
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
return fm.Face();
if (!fm.IsDone()) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
return checked_profile_face(fm.Face());
}
// Two or more loops: build a face per wire and let the largest area be the outer
@@ -933,7 +957,7 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
areas.reserve(wires.size());
for (const TopoDS_Wire& w : wires) {
BRepBuilderAPI_MakeFace fm(w);
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
if (!fm.IsDone()) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
faces.push_back(fm.Face());
GProp_GProps props;
BRepGProp::SurfaceProperties(faces.back(), props);
@@ -959,10 +983,10 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
got = true;
break;
}
if (!got) throw std::runtime_error("sketch loop does not bound a face");
if (!got) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
BRepClass_FaceClassifier fc(faces[outer], p, 1e-7);
if (fc.State() != TopAbs_IN)
throw std::runtime_error("sketch has two disjoint regions; put each in its own sketch");
throw std::runtime_error(_u8L("sketch has two disjoint regions; put each in its own sketch"));
// Add the hole loop AS-IS and let ShapeFix_Face sort the orientations out below.
// Reversing it here only works when the sketch happened to wind both loops the same
// way: a circle drawn clockwise inside a counter-clockwise rectangle comes out matching
@@ -972,13 +996,13 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
// 147520 — a body larger than its own bounding box, which is the signature of it.
fm.Add(wires[i]);
}
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
if (!fm.IsDone()) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
// Winding-independent classification of outer vs holes — the same idiom make_extrude_regions
// already uses for imported glyphs, which is why holed TEXT extruded correctly all along
// while a holed SKETCH did not.
ShapeFix_Face sff(fm.Face());
sff.FixOrientation();
return sff.Face();
return checked_profile_face(sff.Face());
}
std::vector<SketchEntity> SketchEngine::mirror_entities(
@@ -1050,8 +1074,11 @@ std::vector<SketchEntity> SketchEngine::mirror_entities(
} else {
m.p0 = reflect(e.p0);
m.p1 = reflect(e.p1);
// Reflection reverses orientation: recompute parametric angles in
// the reflected frame, original end -> new start (CCW sense kept).
// Reflection reverses orientation. Like the Arc branch above, this pass keeps
// each angle with ITS point (start with p0) and lets the sweep run clockwise;
// the reversal pass below then swaps points and angles together, giving a CCW
// arc whose start is p0. Pairing them crosswise here, as this used to, was
// undone by that same swap and produced the complementary arc.
auto param = [&](const Vec2d& P) {
const Vec2d d = P - m.center;
const double cu = std::cos(m.rotation), su = std::sin(m.rotation);
@@ -1059,8 +1086,9 @@ std::vector<SketchEntity> SketchEngine::mirror_entities(
const double v = -d.x() * su + d.y() * cu;
return std::atan2(v / std::max(e.rminor, 1e-9), u / std::max(e.radius, 1e-9));
};
m.start_angle = param(m.p1);
m.end_angle = param(m.p0);
m.start_angle = param(m.p0);
m.end_angle = param(m.p1);
while (m.end_angle >= m.start_angle) m.end_angle -= 2.0 * M_PI;
}
break;
}
@@ -1120,9 +1148,14 @@ std::vector<SketchEntity> SketchEngine::mirror_entities(
// get their last-to-first seam repaired too, which is what makes the result closed again.
namespace {
constexpr double kOffJoinEps = 1e-6;
bool off_same(const Vec2d& a, const Vec2d& b) { return (a - b).squaredNorm() < kOffJoinEps * kOffJoinEps; }
// Two ends are the same point when the WIRE BUILDER would weld them: a loop the viewport and
// the kernel treat as closed must offset as one closed chain, not as separate open pieces with
// unrepaired seams. The floor keeps exact coincidence meaningful with auto-close switched off.
bool off_same(const Vec2d& a, const Vec2d& b)
{
const double tol = std::max(sketch_join_tol(), 1e-6);
return (a - b).squaredNorm() <= tol * tol;
}
// Does this entity type take part in chaining (i.e. does it have two ends)?
bool off_is_open_curve(const SketchEntity& e)
@@ -1228,7 +1261,10 @@ bool off_one(const SketchEntity& e, double d, SketchEntity& out)
return true;
}
case SketchEntity::Type::Circle: {
const double r = e.radius + d;
// A circle runs CCW (it is what a 360° CCW arc chain closes into), so the rule below
// applies to it too: +d is the left side, the inside, and the radius SHRINKS. It used to
// grow, so a full circle and the same outline drawn as arcs offset opposite ways.
const double r = e.radius - d;
if (r <= 1e-9) return false;
out = e; out.radius = r; out.p0 = out.center;
return true;
@@ -1467,6 +1503,9 @@ std::vector<SketchEntity> SketchEngine::transform_entities(
out.reserve(src.size());
const double ca = std::cos(angle), sa = std::sin(angle);
const double rs = std::abs(scale); // radii are unsigned magnitudes
// A negative scale is |scale| plus a half turn about the pivot: points get that from xf()
// below, and angle-valued fields (arc ends, ellipse axis) need the same extra pi.
const double turn = angle + (scale < 0.0 ? M_PI : 0.0);
// Affine map: translate pivot to origin, scale, rotate, then translate by `move`.
auto xf = [&](const Vec2d& p) -> Vec2d {
const Vec2d d = scale * (p - pivot);
@@ -1489,8 +1528,8 @@ std::vector<SketchEntity> SketchEngine::transform_entities(
case SketchEntity::Type::Arc: {
m.center = xf(e.center);
m.radius = e.radius * rs;
m.start_angle = e.start_angle + angle;
m.end_angle = e.end_angle + angle; // rigid sweep, shifted by rotation
m.start_angle = e.start_angle + turn;
m.end_angle = e.end_angle + turn; // rigid sweep, shifted by rotation
m.p0 = m.center + m.radius * Vec2d(std::cos(m.start_angle), std::sin(m.start_angle));
m.p1 = m.center + m.radius * Vec2d(std::cos(m.end_angle), std::sin(m.end_angle));
break;
@@ -1500,7 +1539,7 @@ std::vector<SketchEntity> SketchEngine::transform_entities(
m.center = xf(e.center);
m.radius = e.radius * rs;
m.rminor = e.rminor * rs;
m.rotation = e.rotation + angle; // major axis rotates with the body
m.rotation = e.rotation + turn; // major axis rotates with the body
if (e.type == SketchEntity::Type::Ellipse) {
m.p0 = m.center;
} else {
@@ -1847,6 +1886,15 @@ static double wrap_2pi(double x)
return x;
}
// An arc's endpoints are stored twice: as angles and as p0/p1. Everything downstream reads
// p0/p1 (the wire builder welds on them, the solver seeds from them, snapping uses them), so
// any edit of the angles must write them back or the arc's ends silently stay where they were.
static void sync_arc_ends(SketchEntity& e)
{
e.p0 = e.center + e.radius * Vec2d(std::cos(e.start_angle), std::sin(e.start_angle));
e.p1 = e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle));
}
bool SketchEngine::trim_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
const Vec2d& pick)
{
@@ -1881,6 +1929,7 @@ bool SketchEngine::trim_entity(SketchEntity& e, const std::vector<SketchEntity>&
if (uc == -std::numeric_limits<double>::max()) return false;
e.end_angle = e.start_angle + uc * sweep; // drop (uc, 1]
}
sync_arc_ends(e);
return true;
}
@@ -1912,6 +1961,7 @@ bool SketchEngine::trim_entity(SketchEntity& e, const std::vector<SketchEntity>&
e.type = SketchEntity::Type::Arc;
e.start_angle = hi;
e.end_angle = lo + 2.0 * M_PI;
sync_arc_ends(e); // p0 was the circle's centre (circle convention); now the arc's start
return true;
}
@@ -1984,6 +2034,7 @@ bool SketchEngine::extend_entity(SketchEntity& e, const std::vector<SketchEntity
if (best == std::numeric_limits<double>::max()) return false;
if (extend_end) e.end_angle += sgn * best;
else e.start_angle -= sgn * best;
sync_arc_ends(e);
return true;
}
@@ -2024,7 +2075,9 @@ bool SketchEngine::extend_entity(SketchEntity& e, const std::vector<SketchEntity
}
// Bridge: cubic Bézier with G1 continuity at both ends.
// Poles = {Pa, Pa + Ta*d/3, Pb - Tb*d/3, Pb}, where d = |Pb - Pa|.
// Poles = {Pa, Pa + Ta*d/3, Pb + Tb*d/3, Pb}, where d = |Pb - Pa| and Ta/Tb are the OUTWARD
// tangents at the two ends (the direction you would leave each entity in). The curve leaves
// Pa along Ta and arrives at Pb along -Tb, i.e. continuing INTO b.
SketchEntity SketchEngine::make_bridge(const SketchEntity& a, int a_end,
const SketchEntity& b, int b_end)
{
@@ -2070,7 +2123,9 @@ SketchEntity SketchEngine::make_bridge(const SketchEntity& a, int a_end,
SketchEntity e;
e.type = SketchEntity::Type::BSpline;
e.construction = false;
e.ctrl = { Pa, Pa + Ta * k, Pb - Tb * k, Pb };
// Pb + Tb*k, not minus: with Tb outward, minus put the last inner pole on b's side, so the
// curve overshot Pb (and against a line arrived with a cusp instead of continuing into it).
e.ctrl = { Pa, Pa + Ta * k, Pb + Tb * k, Pb };
e.p0 = e.ctrl.front();
e.p1 = e.ctrl.back();
return e;
@@ -2098,6 +2153,153 @@ int sketch_entity_ends(const SketchEntity& e, std::pair<SketchPointRole, Vec2d>
return 0;
}
namespace {
// An arc's signed angular offset of `ang` from its start, within its sweep: true when the angle
// lies on the arc (with `tol` radians of slack at either end).
bool angle_on_arc(const SketchEntity& e, double ang, double tol)
{
const double TWO_PI = 2.0 * M_PI;
const double sweep = e.end_angle - e.start_angle;
double d = (sweep >= 0.0) ? ang - e.start_angle : e.start_angle - ang;
d = std::fmod(d, TWO_PI);
if (d < 0.0) d += TWO_PI;
if (d > TWO_PI - tol) d -= TWO_PI; // just before the start counts as the start
return d >= -tol && d <= std::abs(sweep) + tol;
}
// Every point where two lines/arcs meet (tangent contact included). Exact, no sampling.
void entity_intersections(const SketchEntity& A, const SketchEntity& B, std::vector<Vec2d>& out)
{
using T = SketchEntity::Type;
const double eps = 1e-9;
auto on_seg = [](const SketchEntity& L, const Vec2d& p) {
const Vec2d d = L.p1 - L.p0;
const double l2 = d.squaredNorm();
if (l2 < 1e-24) return false;
const double t = (p - L.p0).dot(d) / l2;
return t >= -1e-9 && t <= 1.0 + 1e-9;
};
if (A.type == T::Line && B.type == T::Line) {
const Vec2d r = A.p1 - A.p0, q = B.p1 - B.p0, w = B.p0 - A.p0;
const double den = r.x() * q.y() - r.y() * q.x();
if (std::abs(den) < eps * r.norm() * q.norm()) {
// Parallel. Collinear overlap is a crossing wherever it is: report the overlap's
// nearest end, which the caller then tests against the shared joints.
if (std::abs(r.x() * w.y() - r.y() * w.x()) > 1e-9 * std::max(1.0, r.norm())) return;
for (const Vec2d& p : { B.p0, B.p1 }) if (on_seg(A, p)) out.push_back(p);
for (const Vec2d& p : { A.p0, A.p1 }) if (on_seg(B, p)) out.push_back(p);
return;
}
const double t = (w.x() * q.y() - w.y() * q.x()) / den;
const double u = (w.x() * r.y() - w.y() * r.x()) / den;
if (t >= -1e-9 && t <= 1.0 + 1e-9 && u >= -1e-9 && u <= 1.0 + 1e-9) out.push_back(A.p0 + t * r);
return;
}
if (A.type == T::Arc && B.type == T::Line) { entity_intersections(B, A, out); return; }
if (A.type == T::Line && B.type == T::Arc) {
const Vec2d d = A.p1 - A.p0, f = A.p0 - B.center;
const double a = d.squaredNorm(), b = 2.0 * f.dot(d), c = f.squaredNorm() - B.radius * B.radius;
if (a < 1e-24) return;
double disc = b * b - 4.0 * a * c;
if (disc < -1e-9 * a * B.radius * B.radius) return;
disc = std::sqrt(std::max(0.0, disc));
for (double t : { (-b - disc) / (2.0 * a), (-b + disc) / (2.0 * a) }) {
if (t < -1e-9 || t > 1.0 + 1e-9) continue;
const Vec2d p = A.p0 + t * d;
if (angle_on_arc(B, std::atan2(p.y() - B.center.y(), p.x() - B.center.x()), 1e-9))
out.push_back(p);
}
return;
}
if (A.type == T::Arc && B.type == T::Arc) {
const Vec2d dc = B.center - A.center;
const double dd = dc.norm();
if (dd < 1e-12) {
// Concentric: they overlap only on the same circle, and then everywhere they share.
if (std::abs(A.radius - B.radius) > 1e-9) return;
for (const Vec2d& p : { B.p0, B.p1 })
if (angle_on_arc(A, std::atan2(p.y() - A.center.y(), p.x() - A.center.x()), 1e-9)) out.push_back(p);
for (const Vec2d& p : { A.p0, A.p1 })
if (angle_on_arc(B, std::atan2(p.y() - B.center.y(), p.x() - B.center.x()), 1e-9)) out.push_back(p);
return;
}
if (dd > A.radius + B.radius + 1e-9 || dd < std::abs(A.radius - B.radius) - 1e-9) return;
const double x = (dd * dd + A.radius * A.radius - B.radius * B.radius) / (2.0 * dd);
const double h = std::sqrt(std::max(0.0, A.radius * A.radius - x * x));
const Vec2d u = dc / dd, m = A.center + x * u, n(-u.y(), u.x());
for (const Vec2d& p : { Vec2d(m + h * n), Vec2d(m - h * n) }) {
if (angle_on_arc(A, std::atan2(p.y() - A.center.y(), p.x() - A.center.x()), 1e-9) &&
angle_on_arc(B, std::atan2(p.y() - B.center.y(), p.x() - B.center.x()), 1e-9))
out.push_back(p);
if (h < 1e-12) break;
}
}
}
} // namespace
bool sketch_loop_defect(const std::vector<SketchEntity>& ents, const std::vector<int>& order, Vec2d& at)
{
using T = SketchEntity::Type;
const size_t n = order.size();
if (n < 2) return false;
for (int ei : order)
if (ei < 0 || ei >= int(ents.size()) || (ents[ei].type != T::Line && ents[ei].type != T::Arc))
return false;
const double tol = std::max(1e-6, sketch_join_tol());
// Traversal direction of each entity: the end it shares with the NEXT one is where it
// finishes. start[k]/end[k] are the traversal's ends.
std::vector<Vec2d> start(n), end(n);
std::vector<bool> rev(n, false);
for (size_t k = 0; k < n; ++k) {
const SketchEntity& e = ents[order[k]];
const SketchEntity& nx = ents[order[(k + 1) % n]];
const double to_next_p1 = std::min((e.p1 - nx.p0).norm(), (e.p1 - nx.p1).norm());
const double to_next_p0 = std::min((e.p0 - nx.p0).norm(), (e.p0 - nx.p1).norm());
rev[k] = to_next_p0 < to_next_p1;
start[k] = rev[k] ? e.p1 : e.p0;
end[k] = rev[k] ? e.p0 : e.p1;
}
auto tangent = [&](size_t k, bool at_end) {
const SketchEntity& e = ents[order[k]];
Vec2d t;
if (e.type == T::Line) {
t = e.p1 - e.p0;
} else {
const double a = at_end ? (rev[k] ? e.start_angle : e.end_angle)
: (rev[k] ? e.end_angle : e.start_angle);
const double s = (e.end_angle >= e.start_angle) ? 1.0 : -1.0;
t = s * Vec2d(-std::sin(a), std::cos(a));
}
if (rev[k]) t = -t;
const double l = t.norm();
return l > 1e-15 ? Vec2d(t / l) : Vec2d(0, 0);
};
// Cusps: the curve arriving at a joint and the one leaving it point opposite ways.
for (size_t k = 0; k < n; ++k) {
const size_t j = (k + 1) % n;
if (tangent(k, true).dot(tangent(j, false)) < -0.9999) { at = end[k]; return true; }
}
// Crossings: any contact between two entities away from the joints they share.
for (size_t i = 0; i < n; ++i)
for (size_t j = i + 1; j < n; ++j) {
std::vector<Vec2d> hits;
entity_intersections(ents[order[i]], ents[order[j]], hits);
const bool next = (j == i + 1), prev = (i == 0 && j == n - 1);
for (const Vec2d& p : hits) {
bool joint = false;
if (next && (p - end[i]).norm() < tol) joint = true;
if (prev && (p - start[i]).norm() < tol) joint = true;
if (!joint) { at = p; return true; }
}
}
return false;
}
bool sketch_closest_ends(const SketchEntity& A, const SketchEntity& B,
SketchPointRole& ra, SketchPointRole& rb, Vec2d& pa, Vec2d& pb)
{
+23 -15
View File
@@ -6,6 +6,7 @@
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <gp_Pln.hxx>
#include <gp_Ax1.hxx>
#include <gp_Ax3.hxx>
#include <gp_Vec.hxx>
#include <TopoDS_Wire.hxx>
@@ -53,9 +54,11 @@ struct SketchPlane {
Vec3d x_axis{1,0,0};
Vec3d y_axis{0,1,0};
gp_Pln to_occt() const;
static SketchPlane from_face(const TopoDS_Face& face);
static SketchPlane XY() { return {}; }
// NOTE: XZ's stored normal (+Y) is the OPPOSITE of x_axis x y_axis (-Y). Kept as it is —
// extrude directions and saved recipes depend on it — so anything that needs the frame's
// own handedness takes x_axis.cross(y_axis) instead of `normal` (see make_elips).
static SketchPlane XZ() { return {{0,0,0}, {0,1,0}, {1,0,0}, {0,0,1}}; }
static SketchPlane YZ() { return {{0,0,0}, {1,0,0}, {0,1,0}, {0,0,1}}; }
@@ -70,8 +73,6 @@ struct SketchProfile {
std::vector<Vec2d> points;
bool closed{false};
bool is_closed(double tolerance = 0.5) const;
bool try_close(double tolerance = 0.5);
void clear() { points.clear(); closed = false; }
TopoDS_Wire to_occt_wire(const SketchPlane& plane) const;
@@ -91,9 +92,9 @@ inline constexpr double kSketchJoinTol = 1e-3; // mm
// Effective sketch joint tolerance. ONE value for the viewport (region_loops /
// loop_report / connected_loop) and the kernel (entities_to_wires): if these ever
// disagree again, the viewport shades a region closed that the kernel refuses to
// build, which is how a sketch got extruded into the wrong solid. The GUI pushes
// the "auto_close_sketch_loops" preference in via set_sketch_auto_close(); the
// kernel defaults to ON so headless/kernel-only callers keep welding.
// build, which is how a sketch got extruded into the wrong solid. The document's
// own setting (CadDocument::auto_close_loops) is pushed in via set_sketch_auto_close();
// the kernel defaults to ON so headless/kernel-only callers keep welding.
double sketch_join_tol();
void set_sketch_auto_close(bool on);
@@ -109,8 +110,8 @@ enum class SketchConstraintType {
// inserting anywhere but the end reinterprets every constraint in every saved recipe.
EqualRadius,
Collinear,
DistanceX, // |dx| between two points, projected onto the sketch X axis
DistanceY, // |dy| between two points, projected onto the sketch Y axis
DistanceX, // signed dx between two points (eb - ea), projected onto the sketch X axis
DistanceY, // signed dy between two points (eb - ea), projected onto the sketch Y axis
SymmetricAboutY, // mirror across the sketch's vertical axis (x = 0); axis is implicit
SymmetricAboutX // mirror across the sketch's horizontal axis (y = 0); axis is implicit
};
@@ -165,6 +166,15 @@ int sketch_entity_ends(const SketchEntity& e, std::pair<SketchPointRole, Vec2d>
bool sketch_closest_ends(const SketchEntity& A, const SketchEntity& B,
SketchPointRole& ra, SketchPointRole& rb, Vec2d& pa, Vec2d& pb);
// Where a CLOSED loop fails to bound one region, although every joint meets: two of its
// entities touch somewhere other than a joint they share (the loop crosses itself), or a joint
// where the curve turns straight back along itself (a cusp: an arc leaving a line tangent to it
// but heading the other way). Either makes a face OCCT accepts and then builds an invalid solid
// from. `order` lists the loop's entity indices in traversal order, as the chainer found them.
// Lines and arcs are judged exactly; a loop holding any other kind is not judged (false).
// On true, `at` is the offending point in sketch coordinates.
bool sketch_loop_defect(const std::vector<SketchEntity>& ents, const std::vector<int>& order, Vec2d& at);
// Why an entity-constraint pick is refused. The caller maps a reason to a localized string;
// the planner itself stays translation-free.
enum class ConstraintReject {
@@ -256,12 +266,10 @@ public:
const std::vector<std::vector<std::vector<Vec2d>>>& regions,
const SketchPlane& plane, double length, bool symmetric = false);
// Revolve a planar profile wire about an axis lying in the sketch plane and
// passing through the plane origin: axis_sel 0 = plane X axis, 1 = plane Y axis.
// A negative angle_deg sweeps the opposite direction (Flip). The profile must
// lie to one side of the axis (Onshape rule); a straddling profile self-intersects.
static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane,
double angle_deg = 360.0, int axis_sel = 0);
// Revolve the closed profile wire about `axis` (world) by angle_deg; a negative angle sweeps
// the other way (Flip). The profile must lie to one side of the axis (Onshape rule): one that
// straddles it sweeps through itself, and that is refused rather than returned broken.
static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const gp_Ax1& axis, double angle_deg = 360.0);
// Sweep a planar profile wire along a path (spine) wire. The profile is turned
// into a face and swept with BRepOffsetAPI_MakePipe, which keeps the profile
@@ -317,7 +325,7 @@ public:
// offset together and their seams repaired (miter join), so a closed profile comes back
// closed and can still be extruded; per-entity offsetting cannot do that. Sign convention:
// +d moves each curve to the LEFT of its direction of travel, which for a CCW closed loop
// is inward. Ellipses and splines are not offset (a parallel of either is not the same
// is inward; a full circle counts as CCW, so +d shrinks it. Ellipses and splines are not offset (a parallel of either is not the same
// kind of curve) and are dropped from the result.
static std::vector<SketchEntity> offset_entities(
const std::vector<SketchEntity>& src, double d);
+11 -5
View File
@@ -75,12 +75,18 @@ ImportRegions text_to_regions(const std::string& utf8, double size_mm,
return {};
const std::string path = font_path.empty() ? default_font_path() : font_path;
std::unique_ptr<Emboss::FontFile> ff = Emboss::create_font_file(path.c_str());
if (!ff)
return {};
Emboss::FontFileWithCache fwc(std::move(ff));
if (!fwc.has_value())
return text_to_regions(utf8, size_mm,
std::shared_ptr<const Emboss::FontFile>(Emboss::create_font_file(path.c_str())));
}
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::shared_ptr<const Emboss::FontFile>& ff)
{
if (utf8.empty() || size_mm <= 0.0 || !ff)
return {};
Emboss::FontFileWithCache fwc;
fwc.font_file = ff;
fwc.cache = std::make_shared<Emboss::Glyphs>();
FontProp prop(static_cast<float>(size_mm)); // per_glyph=false
HealedExPolygons healed = Emboss::text2shapes(fwc, utf8.c_str(), prop);
+8
View File
@@ -3,11 +3,14 @@
#include "libslic3r/Point.hpp" // Vec2d
#include <memory>
#include <string>
#include <vector>
namespace Slic3r {
namespace Emboss { struct FontFile; }
// A rigid imported region: contour[0] = outer loop, contour[1..] = holes;
// points in plane (u,v) millimetres. The nested vector type matches
// CadFeature::imported_regions exactly, so results assign directly.
@@ -19,6 +22,11 @@ using ImportRegions = std::vector<ImportRegion>;
// font (resources/fonts). Returns an empty vector on any failure.
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::string& font_path = std::string());
// Same, from an already loaded font — the GUI resolves a system font (face, bold, italic)
// to one and keeps it while the user types. Returns an empty vector when the font is null or
// yields no shape.
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::shared_ptr<const Emboss::FontFile>& font);
// Parse an SVG file's filled paths into regions (mm), centred on the origin.
// `scale` multiplies the authored size (1.0 = as authored). Returns an empty
+21 -6
View File
@@ -80,6 +80,18 @@ InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0,
Vec2d(e.center.x() + e.radius * std::cos(am),
e.center.y() + e.radius * std::sin(am)));
// Nearest point on the arc itself (PointOnObject candidate), as for a line or a
// circle — the solver now takes a point on an arc's rim.
const Vec2d v = query - e.center;
const double n = v.norm();
const double sweep = e.end_angle - e.start_angle;
if (n > 1e-9 && e.radius > 1e-9 && std::abs(sweep) > 1e-9) {
double u = (std::atan2(v.y(), v.x()) - e.start_angle) / sweep;
while (u < 0.0) u += 2.0 * M_PI / std::abs(sweep);
if (u > 0.02 && u < 0.98)
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::Center,
e.center + v * (e.radius / n));
}
break;
}
case SketchEntity::Type::Circle: {
@@ -147,6 +159,10 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
{
std::vector<SketchEntityConstraintDef> out;
if (new_ei <= 0 || new_ei >= int(entities.size())) return out;
// Ends count as meeting at the same tolerance the wire builder welds them at; floor keeps
// exact coincidence meaningful when auto-close is switched off.
const double weld = std::max(sketch_join_tol(), 1e-7);
auto joined = [weld](const Vec2d& a, const Vec2d& b) { return (a - b).squaredNorm() <= weld * weld; };
// AT MOST ONE constraint per rule per new entity, not one per PAIR. Without this the
// function is quadratic in the sketch: a drawing with 200 equal holes yields ~20000
@@ -176,10 +192,9 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
if (n_line && o_line) {
// R1 — parallel / perpendicular, restricted to CONNECTED lines. Connection is
// what keeps this from firing on every distant line that is roughly parallel.
const bool connected = (n.p0 - o.p0).squaredNorm() <= 1e-14 ||
(n.p0 - o.p1).squaredNorm() <= 1e-14 ||
(n.p1 - o.p0).squaredNorm() <= 1e-14 ||
(n.p1 - o.p1).squaredNorm() <= 1e-14;
// "Connected" is the wire builder's weld, so what the viewport shows joined is.
const bool connected = joined(n.p0, o.p0) || joined(n.p0, o.p1) ||
joined(n.p1, o.p0) || joined(n.p1, o.p1);
if (!connected) continue;
const double ang = unsigned_angle(n.p1 - n.p0, o.p1 - o.p0);
const double par_err = std::min(ang, M_PI - ang);
@@ -208,7 +223,7 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
if (cv.type == SketchEntity::Type::Arc) {
const Vec2d ce[2] = { cv.p0, cv.p1 };
for (int m = 0; m < 2; ++m) {
if ((le[k] - ce[m]).squaredNorm() > 1e-14) continue;
if (!joined(le[k], ce[m])) continue;
const Vec2d r = ce[m] - cv.center;
if (r.squaredNorm() < 1e-18) continue;
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
@@ -216,7 +231,7 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
}
} else { // Circle: shared point is a line endpoint on the rim.
const Vec2d r = le[k] - cv.center;
if (std::abs(r.norm() - cv.radius) > 1e-7) continue;
if (std::abs(r.norm() - cv.radius) > weld) continue;
if (r.squaredNorm() < 1e-18) continue;
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
}
+6 -2
View File
@@ -38,8 +38,12 @@ InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
// within `ang_tol_rad` of an axis, returns Horizontal or Vertical (the constraint to
// auto-emit on the committed segment); std::nullopt otherwise. Degenerate (near-zero
// length) segments return nullopt.
// One angular tolerance for every "is this relation already true?" inference, so a pair of
// lines that reads as horizontal to one rule cannot read as not-quite-parallel to the other.
inline constexpr double kSketchInferAngleTol = 3.0 * M_PI / 180.0;
std::optional<SketchConstraintType>
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = 3.0 * M_PI / 180.0);
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = kSketchInferAngleTol);
// Relational constraints to auto-emit for a newly drawn entity `new_ei` against the
// entities already in the sketch. Pure, no GUI/GL dependencies, unit-testable.
@@ -49,7 +53,7 @@ infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad
// relation that is visibly there. Returns an empty vector when nothing qualifies.
std::vector<SketchEntityConstraintDef>
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
double ang_tol_rad = 2.0 * M_PI / 180.0,
double ang_tol_rad = kSketchInferAngleTol,
double len_tol_frac = 0.01);
} // namespace Slic3r
+143 -18
View File
@@ -4,6 +4,7 @@
#include <math.h>
#include <slvs.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <functional>
@@ -46,12 +47,15 @@ struct Build {
{ return E(Slvs_MakePoint2d(++eh, g, wp, P(g, u), P(g, v))); }
// Generic constraint (entityC unused by Slvs_MakeConstraint — set it manually below).
// `other` / `other2` pick the END point (point[2]) of entity A / B instead of its start
// (point[1]) for the constraints that read an arc's endpoint (the tangencies).
void C(int type, double val, Slvs_hEntity ptA, Slvs_hEntity ptB,
Slvs_hEntity eA, Slvs_hEntity eB, Slvs_hEntity eC = 0, int other = 0)
Slvs_hEntity eA, Slvs_hEntity eB, Slvs_hEntity eC = 0, int other = 0, int other2 = 0)
{
Slvs_Constraint c = Slvs_MakeConstraint(++ch, G_SK, type, wp, val, ptA, ptB, eA, eB);
c.entityC = eC;
c.other = other;
c.other2 = other2;
cons.push_back(c);
}
};
@@ -113,9 +117,15 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
case SketchEntity::Type::Circle: {
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = s.center; // p0 mirrors centre for circles
s.rparam = b.P(G_SK, e.radius > 1e-9 ? e.radius : 1.0);
Slvs_hEntity dist = b.E(Slvs_MakeDistance(++b.eh, G_SK, b.wp, s.rparam));
s.prim = b.E(Slvs_MakeCircle(++b.eh, G_SK, b.wp, s.center, b.normal, dist));
// A zero-radius circle is degenerate everywhere else (the wire builder, offset and
// inference all reject it). It used to be seeded with radius 1 here and written
// back, so any unrelated solve silently turned it into a 1 mm circle. It gets no
// primitive: its centre still solves, anything needing the rim is skipped.
if (e.radius > 1e-9) {
s.rparam = b.P(G_SK, e.radius);
Slvs_hEntity dist = b.E(Slvs_MakeDistance(++b.eh, G_SK, b.wp, s.rparam));
s.prim = b.E(Slvs_MakeCircle(++b.eh, G_SK, b.wp, s.center, b.normal, dist));
}
break;
}
case SketchEntity::Type::Arc:
@@ -175,6 +185,20 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
switch (r) { case Role::P0: return e.p0; case Role::P1: return e.p1; case Role::Center: return e.center; }
return e.p0;
};
// Which side of line `li` the point (ei, r) is on, in slvs' sign convention for the
// in-workplane PT_LINE_DISTANCE: with d = point[0] - point[1] and a = point[0], the signed
// distance has the sign of d x (p - a) (pinned by the tests "a point below the line stays
// below it" and "tangent line to circle, line below it"). +1 when on the line.
auto side_of = [&](int ei, Role r, int li) -> double {
Vec2d a, bb;
if (li == kSketchRefAxisX) { a = Vec2d(1, 0); bb = Vec2d(0, 0); }
else if (li == kSketchRefAxisY) { a = Vec2d(0, 1); bb = Vec2d(0, 0); }
else if (valid(li)) { a = entities[li].p0; bb = entities[li].p1; }
else return 1.0;
const Vec2d d = a - bb, ap = coordOf(ei, r) - a;
const double cr = d.x() * ap.y() - d.y() * ap.x();
return cr < 0.0 ? -1.0 : 1.0;
};
// A fixed reference point at (x,y) — used to pin coordinates (Fix / LockX / LockY).
auto fixedRef = [&](double x, double y) -> Slvs_hEntity { return b.pt2d(G_FIXED, x, y); };
@@ -212,7 +236,7 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
case CT::Collinear:
ref_ok = primOf(c.ea) && primOf(c.eb); break;
}
if (!ref_ok) continue;
if (!ref_ok) { out.skipped.push_back(int(&c - constraints.data())); continue; }
switch (c.type) {
case CT::Coincident:
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
@@ -287,9 +311,36 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
case CT::Tangent: {
const bool aCurve = valid(c.ea) && entities[c.ea].type != SketchEntity::Type::Line;
const bool bCurve = valid(c.eb) && entities[c.eb].type != SketchEntity::Type::Line;
if (aCurve && bCurve)
b.C(SLVS_C_CURVE_CURVE_TANGENT, 0, 0, 0, primOf(c.ea), primOf(c.eb));
else {
const bool aCircle = aCurve && entities[c.ea].type == SketchEntity::Type::Circle;
const bool bCircle = bCurve && entities[c.eb].type == SketchEntity::Type::Circle;
if (aCurve && bCurve && (aCircle || bCircle)) {
// CURVE_CURVE_TANGENT reads each curve's ENDPOINT, which a full circle does not
// have: slvs asserts and takes the process down (the same trap the circle-line
// case below documents). Two round curves are tangent exactly when their
// centres are r1 + r2 apart (outside each other) or |r1 - r2| apart (one inside
// the other); keep whichever the sketch is closer to now. Radii are captured as
// constants, with the same caveat as the circle-line case.
const SketchEntity& A = entities[c.ea];
const SketchEntity& B = entities[c.eb];
const double d = (A.center - B.center).norm();
const double ext = A.radius + B.radius;
const double in = std::abs(A.radius - B.radius);
b.C(SLVS_C_PT_PT_DISTANCE, std::abs(d - ext) <= std::abs(d - in) ? ext : in,
ptOf(c.ea, Role::Center), ptOf(c.eb, Role::Center), 0, 0);
} else if (aCurve && bCurve) {
// Tangent where the two arcs MEET: pick, for each arc, the endpoint closest to
// the other arc's nearest endpoint. Always binding the start point (the old
// behaviour) made a tangency at an arc's end act on its start instead.
const SketchEntity& A = entities[c.ea];
const SketchEntity& B = entities[c.eb];
int oa = 0, ob = 0; double best = 1e300;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j) {
const double dd = ((i ? A.p1 : A.p0) - (j ? B.p1 : B.p0)).squaredNorm();
if (dd < best) { best = dd; oa = i; ob = j; }
}
b.C(SLVS_C_CURVE_CURVE_TANGENT, 0, 0, 0, primOf(c.ea), primOf(c.eb), 0, oa, ob);
} else {
const int ci = aCurve ? c.ea : c.eb; // the curve
const int li = aCurve ? c.eb : c.ea; // the line
if (valid(ci) && entities[ci].type == SketchEntity::Type::Circle) {
@@ -310,24 +361,47 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
// not being changed by another constraint in the same solve; if some other
// constraint drives the radius, re-solving restores tangency. Tying them
// would need an auxiliary point constrained onto both circle and line.
b.C(SLVS_C_PT_LINE_DISTANCE, entities[ci].radius,
b.C(SLVS_C_PT_LINE_DISTANCE, side_of(ci, Role::Center, li) * entities[ci].radius,
ptOf(ci, Role::Center), 0, primOf(li), 0);
} else {
b.C(SLVS_C_ARC_LINE_TANGENT, 0, 0, 0, primOf(ci), primOf(li));
// The arc endpoint that touches the line is the one the tangency is at. The
// constraint used to bind the START point always, so a fillet — its start on
// one leg, its end on the other — forced both legs parallel.
int other = 0;
if (valid(ci) && valid(li)) {
const SketchEntity& A = entities[ci];
const SketchEntity& L = entities[li];
auto seg_dist = [&](const Vec2d& p) {
const Vec2d d = L.p1 - L.p0;
const double t = d.squaredNorm() > 1e-18
? std::clamp((p - L.p0).dot(d) / d.squaredNorm(), 0.0, 1.0) : 0.0;
return (L.p0 + t * d - p).norm();
};
other = seg_dist(A.p1) < seg_dist(A.p0) ? 1 : 0;
}
b.C(SLVS_C_ARC_LINE_TANGENT, 0, 0, 0, primOf(ci), primOf(li), 0, other);
}
}
break;
}
case CT::PointOnLine:
// slvs' in-workplane point-line distance is SIGNED. A negative stored value is an
// explicit side; a positive one (what the UI stores) keeps the side the point is on
// now. Passing |value| forced every point to the positive side, flipping any that
// sat on the other one across the line.
if (std::abs(c.value) < 1e-9)
b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
else
b.C(SLVS_C_PT_LINE_DISTANCE, std::abs(c.value), ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
b.C(SLVS_C_PT_LINE_DISTANCE,
c.value < 0.0 ? c.value : side_of(c.ea, c.ra, c.eb) * c.value,
ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
break;
case CT::PointOnObject:
// Point (ea,ra) lies on entity edge eb: a circle rim -> PT_ON_CIRCLE,
// otherwise the segment line -> PT_ON_LINE.
if (valid(c.eb) && entities[c.eb].type == SketchEntity::Type::Circle)
// Point (ea,ra) lies on entity edge eb: a circle or arc rim -> PT_ON_CIRCLE (slvs
// takes both), otherwise the segment line -> PT_ON_LINE. An arc used to fall to
// PT_ON_LINE, which is not an equation about an arc at all.
if (valid(c.eb) && (entities[c.eb].type == SketchEntity::Type::Circle ||
entities[c.eb].type == SketchEntity::Type::Arc))
b.C(SLVS_C_PT_ON_CIRCLE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
else
b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
@@ -438,6 +512,29 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
e.start_angle = ns;
e.end_angle = ns + sweep;
e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm());
} else if (e.type == SketchEntity::Type::EllipseArc && s.center && e.radius > 1e-9 && e.rminor > 1e-9) {
// The solver moves the centre and the two ends as free points (it has no conic), so
// after a solve they need not agree with the stored angles, and the wire builder then
// finds the arc's ends away from its vertices and drops the whole sketch. Re-derive
// the parametric angles from the solved ends and put the ends back ON the ellipse.
const double cr = std::cos(e.rotation), sr = std::sin(e.rotation);
auto param = [&](const Vec2d& p) {
const Vec2d d = p - e.center;
const double x = d.x() * cr + d.y() * sr, y = -d.x() * sr + d.y() * cr;
return std::atan2(y / e.rminor, x / e.radius);
};
auto at = [&](double t) {
const double x = e.radius * std::cos(t), y = e.rminor * std::sin(t);
return Vec2d(e.center.x() + x * cr - y * sr, e.center.y() + x * sr + y * cr);
};
const double old_sweep = e.end_angle - e.start_angle;
double t0 = param(e.p0), t1 = param(e.p1);
if (old_sweep >= 0.0) { while (t1 <= t0) t1 += 2.0 * M_PI; }
else { while (t1 >= t0) t1 -= 2.0 * M_PI; }
e.start_angle = t0;
e.end_angle = t1;
e.p0 = at(t0);
e.p1 = at(t1);
}
}
@@ -465,6 +562,21 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
// that fits today keeps its exact current behaviour, including its reported degrees of freedom.
// A genuinely over-constrained sketch still fails: the conflict lives inside one component and
// that component still rejects it.
// Degrees of freedom an entity has on its own, as the whole-system solve counts them.
static int natural_dof(const SketchEntity& e)
{
switch (e.type) {
case SketchEntity::Type::Line: return 4;
case SketchEntity::Type::Point: return 2;
case SketchEntity::Type::Circle: return e.radius > 1e-9 ? 3 : 2;
case SketchEntity::Type::Arc: return 5; // centre + two ends, ends equidistant
case SketchEntity::Type::Ellipse: return 2; // only the centre is a solver point
case SketchEntity::Type::EllipseArc: return 6; // centre + two ends
case SketchEntity::Type::BSpline: return 2 * int(e.ctrl.size());
}
return 0;
}
static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, Role dragged_role)
@@ -483,12 +595,18 @@ static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
};
for (const auto& c : constraints) { unite(c.ea, c.eb); unite(c.ea, c.ec); }
// Group the constraints by the component they belong to.
// Group the constraints by the component they belong to: that of the first ENTITY they
// reference. ea can be a sketch reference (origin / axis, negative) while eb is the entity,
// and such a constraint was dropped outright.
std::map<int, std::vector<int>> groups;
std::vector<bool> constrained(n, false);
for (size_t i = 0; i < constraints.size(); ++i) {
const int a = constraints[i].ea;
if (a < 0 || a >= n) continue;
const SketchEntityConstraintDef& c = constraints[i];
const int a = (c.ea >= 0 && c.ea < n) ? c.ea : (c.eb >= 0 && c.eb < n) ? c.eb
: (c.ec >= 0 && c.ec < n) ? c.ec : -1;
if (a < 0) continue;
groups[find(a)].push_back(int(i));
for (int e : { c.ea, c.eb, c.ec }) if (e >= 0 && e < n) constrained[e] = true;
}
SketchSolveResult out;
@@ -514,7 +632,8 @@ static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
subc.reserve(cidx.size());
for (int ci : cidx) {
SketchEntityConstraintDef d = constraints[ci];
auto map1 = [&](int& e) { e = (e >= 0 && local.count(e)) ? local[e] : -1; };
// Sketch references (origin, axes: negative sentinels) are global and pass through.
auto map1 = [&](int& e) { if (e >= 0) e = local.count(e) ? local[e] : -1; };
map1(d.ea); map1(d.eb); map1(d.ec);
subc.push_back(d);
}
@@ -527,8 +646,14 @@ static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
if (bi >= 0 && bi < int(cidx.size())) out.bad.push_back(cidx[bi]);
}
if (r.dof > 0) out.dof += r.dof;
for (int si : r.skipped)
if (si >= 0 && si < int(cidx.size())) out.skipped.push_back(cidx[si]);
solved.emplace_back(std::move(ents), std::move(sub));
}
// Entities no constraint touches are in no group but still have their freedoms; the
// whole-system solve counts them, so this path must too or the two report different dof.
for (int i = 0; i < n; ++i)
if (!constrained[i]) out.dof += natural_dof(entities[i]);
if (!out.ok) return out;
for (auto& [ents, sub] : solved)
for (size_t k = 0; k < ents.size(); ++k) entities[ents[k]] = sub[k];
+4
View File
@@ -19,6 +19,10 @@ struct SketchSolveResult {
int dof{-1}; // remaining degrees of freedom (>0 under-constrained)
int result{0}; // raw SLVS_RESULT_* code
std::vector<int> bad; // indices (into `constraints`) of conflicting constraints
// Indices of constraints that were NOT applied because an entity they reference has no
// solver representation for the role they need (an ellipse rim, a spline curve, a
// zero-radius circle...). A solve can be ok with some skipped; callers should say so.
std::vector<int> skipped;
};
// Solve `constraints` over `entities` in place (writes solved coordinates back into the
+2
View File
@@ -24,6 +24,8 @@ struct ThreadSpec {
double thread_depth_mm() const { return 0.6134 * pitch_mm; }
// Internal/tapped minor (tap-drill) diameter for the same nominal thread.
double minor_diameter_mm() const { return major_diameter_mm - 1.0825 * pitch_mm; }
// Radial depth of the internal (tapped) thread, minor to major: (D - D1) / 2.
double internal_depth_mm() const { return 0.5 * (major_diameter_mm - minor_diameter_mm()); }
bool imperial() const { return series == Series::UNC || series == Series::UNF; }
};
+8 -1
View File
@@ -2016,7 +2016,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (boost::algorithm::iequals(name, ORCA_CAD_RECIPE_FILE)
|| boost::algorithm::iequals(name, LEGACY_CAD_RECIPE_FILE)) {
// Restore the editable CAD recipe (optional; absent in non-CAD projects).
if (stat.m_uncomp_size > 0) {
// The current name wins over the legacy one whichever the archive lists
// first, and the size the archive claims is capped before it is allocated.
constexpr mz_uint64 kMaxCadRecipe = mz_uint64(1) << 30; // 1 GiB
const bool legacy = boost::algorithm::iequals(name, LEGACY_CAD_RECIPE_FILE);
if (stat.m_uncomp_size > kMaxCadRecipe) {
BOOST_LOG_TRIVIAL(error) << "3MF: CAD recipe of " << stat.m_uncomp_size
<< " bytes exceeds the limit; not loaded";
} else if (stat.m_uncomp_size > 0 && !(legacy && !model.cad_recipe.empty())) {
std::string buf((size_t)stat.m_uncomp_size, '\0');
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, buf.data(), buf.size(), 0))
model.cad_recipe = std::move(buf);
+6 -6
View File
@@ -5391,7 +5391,7 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionEnum<InputShaperType>(InputShaperType::Default));
def = this->add("input_shaping_freq_x", coFloat);
def->label = L("X");
def->label = L_CONTEXT("X", "Axis");
def->tooltip = L("Resonant frequency for the X axis input shaper.\nZero will use the firmware frequency.\nTo disable input shaping, use the Disable type.\nRRF: X and Y values are equal.");
def->sidetext = L("Hz"); // Hertz, CIS languages need translation
def->min = 0;
@@ -5400,7 +5400,7 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionFloat(0));
def = this->add("input_shaping_freq_y", coFloat);
def->label = L("Y");
def->label = L_CONTEXT("Y", "Axis");
def->tooltip = L("Resonant frequency for the Y axis input shaper.\nZero will use the firmware frequency.\nTo disable input shaping, use the Disable type.");
def->sidetext = L("Hz"); // Hertz, CIS languages need translation
def->min = 0;
@@ -5409,7 +5409,7 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionFloat(0));
def = this->add("input_shaping_damp_x", coFloat);
def->label = L("X");
def->label = L_CONTEXT("X", "Axis");
def->tooltip = L("Damping ratio for the X axis input shaper.\nZero will use the firmware damping ratio.\nTo disable input shaping, use the Disable type.\nRRF: X and Y values are equal.");
def->min = 0;
def->max = 1;
@@ -5417,7 +5417,7 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionFloat(0.1));
def = this->add("input_shaping_damp_y", coFloat);
def->label = L("Y");
def->label = L_CONTEXT("Y", "Axis");
def->tooltip = L("Damping ratio for the Y axis input shaper.\nZero will use the firmware damping ratio.\nTo disable input shaping, use the Disable type.");
def->min = 0;
def->max = 1;
@@ -8921,14 +8921,14 @@ void PrintConfigDef::init_sla_params()
def = this->add("display_pixels_x", coInt);
//def->full_label = L("");
def->label = ("X");
def->label = L_CONTEXT("X", "Axis");
//def->tooltip = L("");
def->min = 100;
def->set_default_value(new ConfigOptionInt(2560));
def = this->add("display_pixels_y", coInt);
//def->full_label = L("");
def->label = ("Y");
def->label = L_CONTEXT("Y", "Axis");
//def->tooltip = L("");
def->min = 100;
def->set_default_value(new ConfigOptionInt(1440));
+6
View File
@@ -147,6 +147,8 @@ set(SLIC3R_GUI_SOURCES
GUI/WebPanel.hpp
GUI/Widgets/WebHosting.cpp
GUI/Widgets/WebHosting.hpp
GUI/AuiMgr.cpp
GUI/AuiMgr.hpp
GUI/AuiPaneLayout.cpp
GUI/AuiPaneLayout.hpp
GUI/DragCanvas.cpp
@@ -835,6 +837,10 @@ if (SLIC3R_CAD)
list(APPEND SLIC3R_GUI_SOURCES
GUI/CAD/DesignPanel.cpp
GUI/CAD/DesignPanel.hpp
GUI/CAD/DesignRowList.cpp
GUI/CAD/DesignRowList.hpp
GUI/CAD/DesignTextDialog.cpp
GUI/CAD/DesignTextDialog.hpp
GUI/CAD/DesignCanvas.cpp
GUI/CAD/DesignCanvas.hpp
GUI/CAD/DesignSketchTool.cpp
+3 -3
View File
@@ -1228,11 +1228,11 @@ void AMSDryCtrWin::update_normal_description(DevAms* dev_ams)
for (const auto& lim : ams_limits) {
if (dev_ams->GetAmsType() == lim.type) {
if (temp_val > lim.max_temp) {
wxString msg = wxString::Format(_L("%s maximum drying temperature is %d°C."), wxString(lim.name), lim.max_temp);
wxString msg = wxString::Format(_L("%s maximum drying temperature is %d\u2103." /* °C */), wxString(lim.name), lim.max_temp);
warning_text += msg + "\n";
can_enable_button = false;
} else if (temp_val < lim.min_temp) {
wxString msg = wxString::Format(_L("%s minimum drying temperature is %d°C."), wxString(lim.name), lim.min_temp);
wxString msg = wxString::Format(_L("%s minimum drying temperature is %d\u2103." /* °C */), wxString(lim.name), lim.min_temp);
warning_text += msg + "\n";
can_enable_button = false;
}
@@ -1260,7 +1260,7 @@ void AMSDryCtrWin::update_normal_description(DevAms* dev_ams)
auto limit_temperature = preset.value().filament_dev_ams_drying_heat_distortion_temperature;
if (temp_val > limit_temperature) {
warning_text += _L("The temperature shall not exceed the filament's heat distortion temperature") + "(" +
wxString::Format(wxT("%d"), static_cast<int>(limit_temperature)) + wxString::FromUTF8("°C)\n");
wxString::Format(wxT("%d"), static_cast<int>(limit_temperature)) + wxString::FromUTF8(u8"\u2103" /* °C */) + ")\n";
can_enable_button = false;
}
}
+3 -2
View File
@@ -264,7 +264,8 @@ AboutDialog::AboutDialog()
vesizer->Add(0, 0, 1, wxEXPAND, FromDIP(5));
auto version_string = std::string(SoftFever_VERSION); // _L("Orca Slicer ") + " " + std::string(SoftFever_VERSION);
wxStaticText* version = new wxStaticText(this, wxID_ANY, version_string.c_str(), wxDefaultPosition, wxDefaultSize);
wxStaticText* credits_string = new wxStaticText(this, wxID_ANY, wxString::Format("Build %s", build_commit_label), wxDefaultPosition, wxDefaultSize);
// TRN %s is the commit the application was built from
wxStaticText* credits_string = new wxStaticText(this, wxID_ANY, wxString::Format(_L("Build %s"), build_commit_label), wxDefaultPosition, wxDefaultSize);
credits_string->SetFont(_build_string_font);
wxFont version_font = GetFont();
version_font = version_font.Scaled(1.85f); // SetPointSize(20) not works on macOS because it uses a 72 PPI reference
@@ -330,7 +331,7 @@ AboutDialog::AboutDialog()
copyright_hor_sizer->Add(copyright_ver_sizer, 0, wxLEFT, FromDIP(20));
wxStaticText *html_text = new wxStaticText(this, wxID_ANY, "Copyright(C) 2026 OrcaSlicer Pte Ltd All Rights Reserved", wxDefaultPosition, wxDefaultSize);
wxStaticText *html_text = new wxStaticText(this, wxID_ANY, _L("Copyright(C) 2026 OrcaSlicer Pte Ltd All Rights Reserved"), wxDefaultPosition, wxDefaultSize);
html_text->SetForegroundColour(wxColour(107, 107, 107));
copyright_ver_sizer->Add(html_text, 0, wxALL , 0);
+7 -6
View File
@@ -329,7 +329,8 @@ void AmsMapingPopup::update_mapping_items(MachineObject* obj, const std::vector<
if (m_show_type == ShowType::LEFT_AND_RIGHT_DYNAMIC) {
auto sizer_mapping_list = new wxBoxSizer(wxHORIZONTAL);
const auto& shown_name = td_opt->ams_id == VIRTUAL_TRAY_MAIN_ID ? "Ext-R" : "Ext-L";
// TRN Short labels of the external spool of the right and left nozzle
const auto& shown_name = td_opt->ams_id == VIRTUAL_TRAY_MAIN_ID ? _L("Ext-R") : _L("Ext-L");
auto ams_mapping_item_container = new MappingContainer(m_right_marea_panel, shown_name, 1);
ams_mapping_item_container->SetName(m_right_marea_panel->GetName());
ams_mapping_item_container->SetSizer(sizer_mapping_list);
@@ -506,7 +507,7 @@ void AmsMapingPopup::update_ams_tips(MachineObject* obj)
}
if (obj && obj->GetFilaSwitch()->IsInstalled()) {
const auto& msg = _L("External spools is not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it.");
const auto& msg = _L("External spools are not supported since Filament Track Switch has been installed. If you want to use an external spool, please uninstall it.");
m_ams_tips_msg_panel->AddMessage(msg, "#FF6F00", "");
}
@@ -588,7 +589,7 @@ void AmsMapingPopup::add_ams_mapping(std::vector<TrayData> tray_data,
// slots Ext-R / Ext-L inline in the two-nozzle left+right views (matches that overload's result).
if ((m_show_type == ShowType::LEFT_AND_RIGHT || m_show_type == ShowType::LEFT_AND_RIGHT_DYNAMIC)
&& (tray_data[i].id == VIRTUAL_TRAY_MAIN_ID || tray_data[i].id == VIRTUAL_TRAY_DEPUTY_ID)) {
m_mapping_item->set_tray_index(tray_data[i].id == VIRTUAL_TRAY_MAIN_ID ? wxString("Ext-R") : wxString("Ext-L"));
m_mapping_item->set_tray_index(tray_data[i].id == VIRTUAL_TRAY_MAIN_ID ? _L("Ext-R") : _L("Ext-L"));
} else {
m_mapping_item->set_tray_index(wxGetApp().transition_tridid(tray_data[i].id));
}
@@ -607,7 +608,7 @@ void AmsMapingPopup::add_ams_mapping(std::vector<TrayData> tray_data,
can_pick_the_item = !devPrinterUtil::IsVirtualSlot(m_mapping_item->m_ams_id);
if (!can_pick_the_item) {
item_tooltip_msg = _L(
"External spools is not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it.");
"External spools are not supported since Filament Track Switch has been installed. If you want to use an external spool, please uninstall it.");
}
}
}
@@ -624,7 +625,7 @@ void AmsMapingPopup::add_ams_mapping(std::vector<TrayData> tray_data,
can_pick_the_item = !devPrinterUtil::IsVirtualSlot(m_mapping_item->m_ams_id);
if (!can_pick_the_item) {
item_tooltip_msg = _L(
"External spools is not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it.");
"External spools are not supported since Filament Track Switch has been installed. If you want to use an external spool, please uninstall it.");
}
} else if (m_show_type != ShowType::RIGHT && m_show_type != ShowType::LEFT_AND_RIGHT) {
can_pick_the_item = false;
@@ -718,7 +719,7 @@ void AmsMapingPopup::add_ext_ams_mapping(TrayData tray_data, MappingItem* item)
});
}
item->set_tray_index("Ext");
item->set_tray_index(_L("Ext"));
}
} // namespace Slic3r::GUI
+84
View File
@@ -0,0 +1,84 @@
#include "AuiMgr.hpp"
#include "GUI_App.hpp"
#ifdef __WXGTK__
#include "LinuxDisplayBackend.hpp"
#endif
#include <wx/aui/dockart.h>
#include <wx/aui/floatpane.h>
#include <wx/aui/framemanager.h>
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
namespace Slic3r { namespace GUI {
namespace {
// TODO: listen on dark ui change
class FloatFrame : public wxAuiFloatingFrame
{
public:
FloatFrame(wxWindow* parent, wxAuiManager* ownerMgr, const wxAuiPaneInfo& pane) : wxAuiFloatingFrame(parent, ownerMgr, pane)
{
wxGetApp().UpdateFrameDarkUI(this);
}
};
} // namespace
void AuiMgr::init(wxWindow* window)
{
SetManagedWindow(window);
SetDockSizeConstraint(1, 1);
#ifdef __WXGTK__
// Floating and its docking hints need global pointer positions and window moves, which Wayland does not provide.
if (is_running_on_wayland())
SetFlags(GetFlags() & ~wxAUI_MGR_ALLOW_FLOATING);
#endif
GetArtProvider()->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, 18);
GetArtProvider()->SetMetric(wxAUI_DOCKART_GRADIENT_TYPE, wxAUI_GRADIENT_NONE);
apply_color_mode();
}
void AuiMgr::apply_color_mode()
{
const bool is_dark = wxGetApp().dark_mode();
const wxColour sash_color = is_dark ? wxColour(38, 46, 48) : wxColour(206, 206, 206);
GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, sash_color);
GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE);
GetArtProvider()->SetColour(wxAUI_DOCKART_SASH_COLOUR, sash_color);
GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165));
}
void AuiMgr::track_docked_size(wxWindow* window)
{
window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) {
wxAuiPaneInfo& pane = GetPane(window);
if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) {
const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM;
pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(),
horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight());
}
evt.Skip();
});
}
wxAuiPaneInfo AuiMgr::sidebar_pane_info()
{
return wxAuiPaneInfo()
.Name("sidebar")
.Left()
.CloseButton(false)
.TopDockable(false)
.BottomDockable(false)
.BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()));
}
wxAuiFloatingFrame* AuiMgr::CreateFloatingFrame(wxWindow* parent, const wxAuiPaneInfo& pane)
{
return new FloatFrame(parent, this, pane);
}
}} // namespace Slic3r::GUI
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <wx/aui/framemanager.h>
namespace Slic3r { namespace GUI {
// The wxAuiManager behind Orca's docked sidebars: Orca's caption and sash art in both themes,
// floating frames that follow the app theme, and no floating on Wayland.
class AuiMgr : public wxAuiManager
{
public:
// Manages `window`, whose own wxEVT_SIZE handlers must Skip().
void init(wxWindow* window);
// Captions, sashes and borders in the current theme.
void apply_color_mode();
// Keeps the docked size of `window`'s pane in its best_size, which re-docking and a saved layout
// use: wxAUI does not record a dragged sash there.
void track_docked_size(wxWindow* window);
// A tab's sidebar pane: docked left by default, movable to the right or floating, no close button.
static wxAuiPaneInfo sidebar_pane_info();
wxAuiFloatingFrame* CreateFloatingFrame(wxWindow* parent, const wxAuiPaneInfo& pane) override;
};
}} // namespace Slic3r::GUI
+1
View File
@@ -1152,6 +1152,7 @@ void AuxiliaryPanel::update_all_cover()
m_sizer_description->Add(m_text_description, 0, wxALIGN_TOP | wxRIGHT, FromDIP(10));
m_input_description = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxSize(FromDIP(450), FromDIP(300)), wxTE_MULTILINE | wxTE_PROCESS_ENTER);
m_input_description->SetBackgroundColour(*wxWHITE);
m_input_description->SetFont(::Label::Body_14);
m_sizer_description->Add(m_input_description, 0, wxALIGN_CENTER, 0);
+1 -1
View File
@@ -20,7 +20,7 @@ namespace GUI {
AuxiliaryDialog::AuxiliaryDialog(wxWindow * parent)
: DPIDialog(parent, wxID_ANY, _L("Auxiliaryies"), wxDefaultPosition,
: DPIDialog(parent, wxID_ANY, _L("Auxiliaries"), wxDefaultPosition,
wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
m_aux_list = new AuxiliaryList(this);
+16
View File
@@ -24,6 +24,9 @@
#include "wxExtensions.hpp"
#include "Plater.hpp"
#include "MainFrame.hpp"
#ifdef SLIC3R_CAD
#include "CAD/DesignPanel.hpp"
#endif
#include "WebViewDialog.hpp"
#include "PartPlate.hpp"
@@ -425,6 +428,9 @@ void BBLTopbar::OnSaveProject(wxAuiToolBarEvent& event)
void BBLTopbar::OnUndo(wxAuiToolBarEvent& event)
{
MainFrame* main_frame = dynamic_cast<MainFrame*>(m_frame);
#ifdef SLIC3R_CAD
if (DesignPanel* design = main_frame->shown_design_panel()) { design->menu_undo_redo(false); return; }
#endif
Plater* plater = main_frame->plater();
plater->undo();
}
@@ -432,6 +438,9 @@ void BBLTopbar::OnUndo(wxAuiToolBarEvent& event)
void BBLTopbar::OnRedo(wxAuiToolBarEvent& event)
{
MainFrame* main_frame = dynamic_cast<MainFrame*>(m_frame);
#ifdef SLIC3R_CAD
if (DesignPanel* design = main_frame->shown_design_panel()) { design->menu_undo_redo(true); return; }
#endif
Plater* plater = main_frame->plater();
plater->redo();
}
@@ -444,6 +453,13 @@ void BBLTopbar::EnableUndoRedoItems()
Refresh();
}
void BBLTopbar::EnableUndoRedo(bool undo, bool redo)
{
this->EnableTool(m_undo_item->GetId(), undo);
this->EnableTool(m_redo_item->GetId(), redo);
Refresh();
}
void BBLTopbar::DisableUndoRedoItems()
{
this->EnableTool(m_undo_item->GetId(), false);
+1
View File
@@ -77,6 +77,7 @@ public:
void EnableUndoRedoItems();
void DisableUndoRedoItems();
void EnableUndoRedo(bool undo, bool redo);
void SaveNormalRect();
+247 -282
View File
@@ -8,6 +8,9 @@
#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/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLToolbar.hpp"
#include "slic3r/GUI/Event.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "slic3r/GUI/3DScene.hpp"
@@ -15,9 +18,11 @@
#include "libslic3r/Config.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <imgui.h>
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
@@ -34,6 +39,7 @@
#include "libslic3r/BoundingBox.hpp"
#include <wx/glcanvas.h>
#include <wx/panel.h>
#include <wx/setup.h>
#include <wx/stopwatch.h> // wxGetLocalTimeMillis: the right-click vs right-hold budget
#include <wx/sizer.h>
#include <wx/frame.h>
@@ -44,6 +50,11 @@
#include <wx/window.h>
#include "libslic3r/PrintConfig.hpp"
#if defined(__WXMSW__) && wxUSE_POPUPWIN
#include <wx/popupwin.h>
extern wxPopupWindow* wxCurrentPopupWindow;
#endif
namespace Slic3r {
namespace GUI {
@@ -66,12 +77,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// cannot be null. Borrow the plater's, as the editor canvases do.
m_canvas->set_process(&wxGetApp().plater()->background_process());
m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D);
// CAD navigation, this canvas only: left-drag sweeps a selection rubber band, so orbit
// moves to middle-drag and pan to right-drag. Design is a different modality from
// Prepare/Preview and every CAD the user already knows maps the mouse this way; the other
// tabs are untouched.
m_canvas->set_cad_navigation(true);
m_canvas->set_studio_lighting(true); // see GLCanvas3D::m_studio_lighting and phong.fs
m_canvas->enable_picking(false); // viewport face/edge picking is custom (TODO)
m_canvas->enable_moving(false);
@@ -84,6 +90,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
m_canvas->enable_collapse_toolbar(false);
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_sketch_tool(&m_sketch_tool);
@@ -155,6 +162,8 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
[this, commit](double v) {
m_sketch_tool.set_inline_busy(false);
if (commit) commit(v);
// A refused value re-opens the same field (SketchInlineEditor::refuse).
if (m_inline_editor && m_inline_editor->is_open()) m_sketch_tool.set_inline_busy(true);
request_repaint();
},
[this, cancel]() {
@@ -172,76 +181,11 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
if (m_inline_editor) m_inline_editor->commit();
};
// Bottom-right viewport HUD: a borderless, non-focusable float label showing the active
// tool's current values. Top-level (a child widget is hidden by the GL surface, same as
// the inline editor). Fed every frame by the tool's on_readout; empty text hides it.
// NON-FOCUSABLE IS THE LOAD-BEARING WORD, and a wxFrame is not: see the header. The chip
// outlives the gesture that drew it, and while it held the X input focus every sketch
// shortcut was swallowed until the user clicked the canvas. Same window class as the status
// chip below for the same reason. Do not "simplify" it back to a wxFrame.
{
wxWindow* top = wxGetTopLevelParent(m_canvas_widget);
m_hud = new wxPopupWindow(top, wxBORDER_NONE);
m_hud->SetBackgroundColour(wxColour(28, 30, 34));
m_hud_label = new wxStaticText(m_hud, wxID_ANY, wxEmptyString);
m_hud_label->SetForegroundColour(wxColour(0x46, 0xE0, 0xC8)); // teal, reads on dark bed
wxFont f = m_hud_label->GetFont(); f.MakeBold(); m_hud_label->SetFont(f);
auto* hs = new wxBoxSizer(wxHORIZONTAL);
hs->Add(m_hud_label, 0, wxALL, 6);
m_hud->SetSizerAndFit(hs);
m_hud->Hide();
}
m_sketch_tool.on_readout = [this](const std::string& s) { set_readout(s); };
// Bottom-LEFT twin, carrying the status line. Top-level for the same reason as the readout
// (a child widget is hidden by the GL surface) but a wxPopupWindow rather than a wxFrame,
// because a popup cannot take keyboard focus. The readout gets away with a frame only
// because it appears mid-gesture and the next input is the mouse; this one is up
// permanently and is re-raised on every status change. As a frame it took the WM's focus
// each time and the canvas stopped receiving keys at all — every sketch shortcut silently
// dead, which reads as a broken tool. Do not "simplify" it back to a wxFrame.
// Its colour is set per message — the panel decides whether a line is neutral or an error.
{
wxWindow* top = wxGetTopLevelParent(m_canvas_widget);
m_status_hud = new wxPopupWindow(top, wxBORDER_NONE);
m_status_hud->SetBackgroundColour(wxColour(28, 30, 34));
m_status_hud_label = new wxStaticText(m_status_hud, wxID_ANY, wxEmptyString);
auto* ss = new wxBoxSizer(wxHORIZONTAL);
// The line never wraps — there is a whole window's width down here — so the chip is
// ONE LINE tall. Spacers rather than a wxALL border because the two axes want
// different numbers: roomy at the sides so it reads as a label, and just enough top
// and bottom to clear the descenders. Zero vertical clips the glyphs; 6 (what the
// readout chip uses) makes it look like a two-line box.
ss->AddSpacer(10);
ss->Add(m_status_hud_label, 0, wxTOP | wxBOTTOM, 3);
ss->AddSpacer(10);
m_status_hud->SetSizerAndFit(ss);
m_status_hud->Hide();
}
// A floating frame does not follow its parent, so the anchor has to be recomputed whenever
// the canvas changes size (that bind is below bind_event_handlers(), for the reason given
// there). The readout HUD gets away without this because it is transient; the status line is
// on screen almost permanently and would visibly detach.
// ...and it does not follow the WINDOW either. A popup is override-redirect: the window
// manager does not own it, so minimising the app leaves the chip sitting on the bare desktop
// (seen on the rig: whole screen black, chip still there), and it stacks above other
// applications rather than behind them. IsShownOnScreen does not catch this — an iconised
// frame still counts as shown — so the frame has to say so itself. Deactivating the app is
// the same case one step weaker: the chip belongs to a viewport the user is no longer
// looking at. Showing it back is safe because a popup cannot take focus, so neither event
// can be re-triggered by our own Show().
// Members rather than lambdas so unbind_canvas_event_handlers() can Unbind them: these sit on
// a frame that OUTLIVES this canvas, and a lambda cannot be unbound.
if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) {
top->Bind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this);
top->Bind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this);
// The anchor is an ABSOLUTE SCREEN position (ClientToScreen below), so moving the window
// moves the canvas out from under a chip that stays where it was. Dragging the frame by
// its title bar left the chip stranded mid-viewport until the next size, status or tab
// change happened to re-place it. Nothing on the canvas fires for a move that does not
// also resize, so it has to come from the frame.
top->Bind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this);
}
// The two viewport chips — the active tool's values bottom-right, the status line bottom-left
// — are drawn by the canvas itself, in the tool's ImGui pass: they go away with the canvas,
// the tab and the window, and never take the keyboard.
m_sketch_tool.on_readout = [this](const std::string& s) { set_readout(s); };
m_sketch_tool.render_overlays = [this] { render_hud(); };
refresh_bed();
@@ -253,16 +197,10 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// reverse order of binding, and GLCanvas3D swallows several events without skipping them —
// wxEVT_RIGHT_UP and wxEVT_ENTER_WINDOW in on_mouse, and wxEVT_SIZE in on_size, which is
// just `m_dirty = true;`. For those, whatever is bound LAST is the only handler that runs.
// The context menu, the focus-follows-mouse and the status-chip re-anchor all depend on
// running first, which is only true while this call stays ahead of them.
// The context menu and the focus-follows-mouse depend on running first, which is only true
// while this call stays ahead of them.
m_canvas->bind_event_handlers();
// The status-chip re-anchor promised above, bound AFTER the call so it runs first — ahead of
// it the handler never ran at all, leaving a stale anchor and wrap width after any resize
// that did not also move the frame or change the text. Its e.Skip() is load-bearing the
// other way: it falls through to on_size, which is what still marks the canvas dirty.
m_canvas_widget->Bind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this);
// The Design GL canvas only receives key events (Esc to exit/enter Select, Ctrl+Z undo)
// while it holds keyboard focus. Clicking a side-panel button steals focus, after which
// Esc/Ctrl+Z silently do nothing until the viewport is clicked again. Restore focus
@@ -271,7 +209,14 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// …but NOT while an inline value field is open: the field floats over the canvas, so
// the smallest pointer jiggle re-enters the viewport and would yank focus off the
// field (the "no cursor focus on the number, click to focus" bug).
if (m_canvas_widget && !m_sketch_tool.inline_busy()) m_canvas_widget->SetFocus();
bool take_focus = m_canvas_widget && !m_sketch_tool.inline_busy();
#if defined(__WXMSW__) && wxUSE_POPUPWIN
// …nor while a popup is open. A ribbon dropdown opens over the viewport, and the pointer
// crosses the viewport on its way to it: taking focus then reactivates the frame, which
// on MSW dismisses the popup under the pointer. GLCanvas3D::on_mouse has the same guard.
take_focus = take_focus && !wxCurrentPopupWindow;
#endif
if (take_focus) m_canvas_widget->SetFocus();
e.Skip();
});
@@ -285,18 +230,6 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// Both are idempotent, and neither destroys anything: the destructor still owns that.
void DesignCanvas::unbind_canvas_event_handlers()
{
if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) {
top->Unbind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this);
top->Unbind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this);
top->Unbind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this);
}
// Before the popups go down, or a resize still in flight re-places and re-shows the chip.
if (m_canvas_widget)
m_canvas_widget->Unbind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this);
// A popup is override-redirect: it does not go down with the frame, so one left showing sits
// on the bare desktop for however long the teardown takes.
show_status_hud(false);
if (m_hud) m_hud->Hide();
if (m_canvas) m_canvas->unbind_event_handlers();
}
@@ -307,18 +240,19 @@ void DesignCanvas::reset_canvas_volumes()
DesignCanvas::~DesignCanvas()
{
if (m_hud) m_hud->Destroy();
delete m_canvas;
delete m_canvas_widget;
}
// Distinct per-body colours (Onshape-style). Body 0 keeps the familiar gold; the rest
// cycle through a small saturated palette so coexisting solids read as separate parts.
// cycle through a small saturated palette so coexisting solids read as separate parts. No entry
// may sit near the selection cyan (design_selection_color): a blue or teal body would look
// selected, and its selected faces would barely stand out.
static ColorRGBA body_palette(int body_idx)
{
static const ColorRGBA kPalette[] = {
ColorRGBA(0.86f, 0.66f, 0.20f, 1.0f), // gold
ColorRGBA(0.30f, 0.62f, 0.90f, 1.0f), // blue
ColorRGBA(0.84f, 0.42f, 0.66f, 1.0f), // rose
ColorRGBA(0.45f, 0.78f, 0.42f, 1.0f), // green
ColorRGBA(0.86f, 0.45f, 0.40f, 1.0f), // coral
ColorRGBA(0.70f, 0.52f, 0.86f, 1.0f), // violet
@@ -363,14 +297,22 @@ void DesignCanvas::request_repaint()
}
}
// ImGui's display size is shared by every canvas and only refreshed when a canvas sees its own
// size change, so the canvas taking over must re-announce its size (Plater does the same between
// Prepare and Preview). Otherwise the overlays anchored to it, the FPS counter first, are laid
// out for the other canvas.
void DesignCanvas::enter_viewport()
{
if (!m_camera_swapped) swap_camera();
if (m_canvas) { m_canvas->reset_old_size(); m_canvas->set_as_dirty(); }
}
void DesignCanvas::leave_viewport()
{
if (m_camera_swapped) swap_camera();
if (Plater* plater = wxGetApp().plater())
if (GLCanvas3D* editor = plater->get_current_canvas3D())
editor->reset_old_size();
}
void DesignCanvas::swap_camera()
@@ -410,33 +352,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);
@@ -464,45 +398,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);
@@ -649,6 +561,21 @@ void DesignCanvas::set_show_bed(bool b)
request_repaint();
}
void DesignCanvas::set_sidebar_collapse(std::function<CollapseSide()> side, std::function<void()> toggle)
{
if (!m_canvas || !m_canvas_widget || !setup_collapse_toolbar(m_collapse_toolbar, toggle))
return;
m_canvas->set_collapse_toolbar(&m_collapse_toolbar, std::move(side));
m_canvas->enable_collapse_toolbar(true);
// Shift+Tab: the canvas posts this for its sidebar, as Prepare's canvases do.
m_canvas_widget->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [toggle](SimpleEvent&) { toggle(); });
}
void DesignCanvas::set_sidebar_collapse_tooltip(const std::string& tooltip)
{
m_collapse_toolbar.set_tooltip(m_collapse_toolbar.get_item_id("collapse_sidebar"), tooltip);
}
bool DesignCanvas::is_sketching() const { return m_sketch_tool.is_active(); }
void DesignCanvas::cancel_sketch()
@@ -743,12 +670,28 @@ void DesignCanvas::clear_loop_pick()
m_sketch_tool.clear_display_pick();
}
void DesignCanvas::clear_solid_pick()
{
m_sketch_tool.clear_solid_selection();
sync_selected_faces();
}
void DesignCanvas::set_loop_pick(int feature, int region)
{
m_sketch_tool.set_display_pick(feature, region);
request_repaint();
}
int DesignCanvas::loop_pick_feature() const
{
return m_sketch_tool.display_pick();
}
int DesignCanvas::loop_pick_region() const
{
return m_sketch_tool.display_pick_region();
}
void DesignCanvas::set_escalate_on_repick(bool on)
{
m_sketch_tool.set_escalate_on_repick(on);
@@ -760,7 +703,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
@@ -879,9 +824,9 @@ void DesignCanvas::set_on_shell_thickness_changed(std::function<void(double)> cb
}
void DesignCanvas::begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
int axis_sel, double angle, bool flip)
const Vec3d& axis_origin, const Vec3d& axis_dir, double angle, bool flip)
{
m_sketch_tool.set_revolve_gizmo(plane, centroid, axis_sel, angle, flip);
m_sketch_tool.set_revolve_gizmo(plane, centroid, axis_origin, axis_dir, angle, flip);
request_repaint();
}
@@ -956,19 +901,31 @@ void DesignCanvas::set_on_pattern_changed(std::function<void(double)> cb)
m_sketch_tool.on_pattern_changed = std::move(cb);
}
void DesignCanvas::set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb)
std::vector<int> DesignCanvas::selected_solid_edges() const
{
m_sketch_tool.on_solid_selection_changed = std::move(cb);
return m_sketch_tool.selected_edges();
}
void DesignCanvas::set_on_place_on_face(std::function<bool()> cb)
void DesignCanvas::set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb)
{
m_sketch_tool.on_place_on_face = 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::set_on_empty_pick(std::function<void()> cb)
{
m_sketch_tool.on_empty_pick = std::move(cb);
}
void DesignCanvas::select_body(int body)
{
m_sketch_tool.select_body(body);
sync_selected_faces();
request_repaint();
}
@@ -1078,9 +1035,9 @@ void DesignCanvas::set_on_sketch_exit_refused(std::function<void()> cb)
m_sketch_tool.on_exit_refused = std::move(cb);
}
void DesignCanvas::set_on_move_exit(std::function<void()> cb)
void DesignCanvas::set_on_sketch_notice(std::function<void(const std::string&, bool)> cb)
{
m_sketch_tool.on_move_exit = std::move(cb);
m_sketch_tool.on_notice = std::move(cb);
}
void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
@@ -1094,25 +1051,26 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
// 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, at the same 8 px budget
// the left-click pick uses.
// the view. The offer is the release of a STATIONARY right-click (kCadRightClickDriftPx).
m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
m_ctx_press = e.GetPosition();
m_ctx_press_ms = wxGetLocalTimeMillis().GetValue();
m_ctx_press = e.GetPosition();
m_ctx_travelled = false;
e.Skip(); // the canvas still needs the press to seed the orbit
});
m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
if (e.RightIsDown()) {
const wxPoint d = e.GetPosition() - m_ctx_press;
if (std::max(std::abs(d.x), std::abs(d.y)) > kCadRightClickDriftPx) m_ctx_travelled = true;
}
e.Skip();
});
m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) {
const wxPoint d = e.GetPosition() - m_ctx_press;
const long long dt = wxGetLocalTimeMillis().GetValue() - m_ctx_press_ms;
// 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? Both budgets must hold: a press that travelled orbited, and a
// press that was HELD was aiming to orbit even if the hand never quite moved. Two
// independent budgets because the two failure modes are independent — the drift one
// alone still popped a menu at the end of a slow, careful orbit.
const bool is_click = std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx
&& dt <= kCadRightClickMs;
// Click, or navigation? A press that travelled 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) {
// 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
@@ -1130,11 +1088,6 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
});
}
void DesignCanvas::set_on_undo_redo(std::function<void(bool)> cb)
{
m_sketch_tool.on_undo_redo = std::move(cb);
}
void DesignCanvas::set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds)
{
m_sketch_tool.set_display_sketches(std::move(ds));
@@ -1209,131 +1162,130 @@ double DesignCanvas::model_mid_z() const
void DesignCanvas::set_readout(const std::string& text)
{
if (!m_hud || !m_hud_label || !m_canvas_widget) return;
if (text == m_hud_last) return; // only touch the WM on a real change
if (text == m_hud_last) return;
m_hud_last = text;
if (text.empty()) { m_hud->Hide(); return; }
m_hud_label->SetLabel(wxString::FromUTF8(text));
place_readout_hud();
}
void DesignCanvas::place_readout_hud()
{
if (!m_hud || !m_hud_label || !m_canvas_widget) return;
if (m_hud_last.empty() || !m_canvas_widget->IsShownOnScreen()) { m_hud->Hide(); return; }
m_hud->Fit();
// Anchor to the canvas's bottom-right corner with a small margin (screen coords).
const wxSize cs = m_canvas_widget->GetClientSize();
const wxSize hs = m_hud->GetSize();
const wxPoint br = m_canvas_widget->ClientToScreen(
wxPoint(cs.GetWidth() - hs.GetWidth() - 12, cs.GetHeight() - hs.GetHeight() - 12));
if (!m_hud->IsShown()) m_hud->Show(); // Show before Move (GTK ignores pre-map Move)
m_hud->Move(br);
m_hud->Raise();
}
// A popup is override-redirect: the window manager does not own it, so an iconised or
// deactivated app would leave the chip sitting on the bare desktop. The status chip already
// had to answer this; now that the readout is a popup too, it answers it the same way.
void DesignCanvas::show_readout_hud(bool on)
{
if (!m_hud) return;
if (on) place_readout_hud();
else m_hud->Hide();
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.
// Shared by the placement and by the wrap width, which have to agree or the chip wraps to a
// width it is then not given.
static constexpr int kStatusHudLeftInsetDip = 190;
static constexpr float kStatusHudLeftInset = 190.f;
void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour)
{
if (!m_status_hud || !m_status_hud_label || !m_canvas_widget) return;
if (text == m_status_hud_last && colour == m_status_hud_colour) return;
m_status_hud_last = text;
m_status_hud_colour = colour;
if (text.IsEmpty()) { m_status_hud->Hide(); return; }
m_status_hud_label->SetForegroundColour(colour);
apply_status_label();
place_status_hud();
request_repaint();
}
// SetLabel + Wrap + Fit, in that order and always together. Moving the status out of the panel
// removed the clipping of 8cc but not the underlying problem: the chip is a top-level
// popup that Fit()s to its text, so a long sentence simply grew past the right edge of the canvas
// and hung over the window. Wrapping to the room actually available is what makes the earlier
// promise — "a sentence can be a sentence" — true at every window width, including the charter's
// 1366 reach. Wrap() rewrites the label it is given, so it must follow a fresh SetLabel every
// time; that is the whole reason this is one function instead of three call sites.
void DesignCanvas::apply_status_label()
void DesignCanvas::render_hud()
{
if (!m_status_hud || !m_status_hud_label || !m_canvas_widget) return;
m_status_hud_label->SetLabel(m_status_hud_last);
const int avail = m_canvas_widget->GetClientSize().GetWidth()
- m_canvas_widget->FromDIP(kStatusHudLeftInsetDip)
- m_canvas_widget->FromDIP(24);
if (avail > m_canvas_widget->FromDIP(120)) // a uselessly narrow canvas: leave it unwrapped
m_status_hud_label->Wrap(avail);
m_status_hud->Fit();
if (m_hud_last.empty() && m_status_hud_last.IsEmpty()) return;
ImGuiWrapper& imgui = *wxGetApp().imgui();
const ImVec2 ds = ImGui::GetIO().DisplaySize;
const float em = imgui.get_style_scaling(); // follows the font, so the DPI
const float margin = 12.f * em;
const int flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
auto chip = [&](const char* id, const std::string& text, float x, float pivot_x, float wrap,
const ImVec4* colour) {
ImGuiWrapper::push_common_window_style(m_canvas->get_scale());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f * em, 4.f * em));
imgui.set_next_window_pos(x, ds.y - margin, ImGuiCond_Always, pivot_x, 1.f);
imgui.begin(std::string(id), flags);
if (wrap > 0.f) ImGui::PushTextWrapPos(wrap);
if (colour) ImGui::PushStyleColor(ImGuiCol_Text, *colour);
ImGui::TextUnformatted(text.c_str());
if (colour) ImGui::PopStyleColor();
if (wrap > 0.f) ImGui::PopTextWrapPos();
imgui.end();
ImGui::PopStyleVar();
ImGuiWrapper::pop_common_window_style();
};
if (!m_status_hud_last.IsEmpty()) {
// A sentence can be a sentence: it wraps to the room left of the readout chip.
const float left = kStatusHudLeftInset * em;
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)
: ImVec4();
chip("##design_status", m_status_hud_last.ToUTF8().data(), left, 0.f,
std::max(ds.x * 0.6f - left, 120.f * em), m_status_hud_colour.IsOk() ? &col : nullptr);
}
if (!m_hud_last.empty())
chip("##design_readout", m_hud_last, ds.x - margin, 1.f, 0.f, &ImGuiWrapper::COL_ORCA);
}
void DesignCanvas::place_status_hud()
void DesignCanvas::set_highlight_faces(const std::vector<std::pair<int, int>>& faces)
{
if (!m_status_hud || !m_canvas_widget || m_status_hud_last.IsEmpty()) return;
// The canvas has a client size even while its page is hidden, and it is not the size the
// page will have when shown — anchoring against it put the chip up on the tab bar, where it
// then stayed until the next status change moved it. Nothing to anchor to: stay down.
if (!m_canvas_widget->IsShownOnScreen()) { m_status_hud->Hide(); return; }
const wxSize cs = m_canvas_widget->GetClientSize();
// Re-wrap first: this also runs on resize, and a chip wrapped for the old width either
// overhangs a narrowed canvas or wastes a widened one.
apply_status_label();
const wxSize hs = m_status_hud->GetSize();
const int kLeftInset = m_canvas_widget->FromDIP(kStatusHudLeftInsetDip);
const wxPoint bl = m_canvas_widget->ClientToScreen(
wxPoint(kLeftInset, cs.GetHeight() - hs.GetHeight() - 12));
// No Raise() and no focus juggling: a popup neither takes focus nor falls behind. This was
// caught with ORCA_CAD_KEYTRACE — shift+S logged a line, the following R logged nothing, and
// the only thing between them was the first status update showing this window.
if (!m_status_hud->IsShown()) m_status_hud->Show(); // Show before Move (GTK ignores pre-map Move)
m_status_hud->Move(bl);
m_sketch_tool.set_highlight_faces(faces);
sync_selected_faces();
request_repaint();
}
void DesignCanvas::on_frame_iconize(wxIconizeEvent& e)
void DesignCanvas::sync_selected_faces()
{
show_status_hud(!e.IsIconized());
show_readout_hud(!e.IsIconized());
e.Skip();
// 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);
});
}
void DesignCanvas::on_frame_activate(wxActivateEvent& e)
// 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()
{
show_status_hud(e.GetActive());
show_readout_hud(e.GetActive());
e.Skip();
}
// wxEvent& so one handler serves both events that invalidate the anchor: the frame moving out
// from under the chip, and the canvas resizing under it.
void DesignCanvas::on_status_hud_reanchor(wxEvent& e)
{
place_status_hud();
e.Skip();
}
void DesignCanvas::show_status_hud(bool on)
{
if (!m_status_hud) return;
if (on) place_status_hud(); // re-anchors first: the page may have been resized while away
else m_status_hud->Hide();
}
void DesignCanvas::set_body_highlight(bool on)
{
if (m_body_selected == on) return;
m_body_selected = on;
reload(true); // recolours the body volume (selected = cyan tint)
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)
@@ -1341,7 +1293,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)
@@ -1369,13 +1321,16 @@ 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
}
void DesignCanvas::delete_selected_sketch_entities()
bool DesignCanvas::delete_selected_sketch_entities()
{
if (m_sketch_tool.selection().empty()) return false;
m_sketch_tool.delete_selected();
request_repaint();
return true;
}
bool DesignCanvas::inline_busy() const
@@ -1414,16 +1369,16 @@ bool DesignCanvas::live_sketch_has_work() const
return m_sketch_tool.live_sketch_has_work();
}
bool DesignCanvas::undo_last_sketch_entity()
bool DesignCanvas::redo_last_sketch_entity()
{
const bool did = m_sketch_tool.undo_last_entity();
const bool did = m_sketch_tool.redo_last_entity();
if (did) request_repaint();
return did;
}
bool DesignCanvas::delete_selected_or_last_sketch_entity()
bool DesignCanvas::undo_last_sketch_entity()
{
const bool did = m_sketch_tool.delete_selected_or_last();
const bool did = m_sketch_tool.undo_last_entity();
if (did) request_repaint();
return did;
}
@@ -1470,6 +1425,7 @@ void DesignCanvas::open_inline_value(double current, std::function<void(double)>
[this, commit](double v) {
m_sketch_tool.set_inline_busy(false);
if (commit) commit(v);
if (m_inline_editor && m_inline_editor->is_open()) m_sketch_tool.set_inline_busy(true);
request_repaint();
},
[this, cancel]() {
@@ -1580,6 +1536,13 @@ bool DesignCanvas::sketch_abort_gesture()
return true;
}
bool DesignCanvas::sketch_confirm_pending()
{
if (!m_sketch_tool.confirm_pending()) return false;
request_repaint();
return true;
}
bool DesignCanvas::sketch_disarm_tool()
{
if (!m_sketch_tool.disarm_tool()) return false;
@@ -1589,23 +1552,25 @@ bool DesignCanvas::sketch_disarm_tool()
bool DesignCanvas::drawing_in_progress() const
{
return m_sketch_tool.pending_points() > 0;
return m_sketch_tool.gesture_pending();
}
bool DesignCanvas::has_any_selection() const
{
return m_sketch_tool.has_solid_selection() || m_sketch_tool.sketch_has_selection();
return m_sketch_tool.has_solid_selection() || m_sketch_tool.sketch_has_selection()
|| m_sketch_tool.display_pick() >= 0;
}
bool DesignCanvas::clear_any_selection()
{
if (!has_any_selection()) return false;
// Both, unconditionally: which of the two is live depends on the mode, and Esc at idle means
// "nothing is picked" in either of them. clear_selection() reports through the tool's own
// All three, unconditionally: which is live depends on the mode, and Esc at idle means
// "nothing is picked" in any of them. clear_selection() reports through the tool's own
// on_selection_changed; the solid side has no such notification, so the panel refreshes what
// depends on it (see DesignPanel::escape).
m_sketch_tool.clear_selection();
m_sketch_tool.clear_solid_selection();
m_sketch_tool.clear_display_pick();
// clear_solid_selection() is silent by design (recomputes call it while ids are invalid), but
// the panel mirrors the pick to aim Extrude and the dress-up tools. An Esc that cleared the
// highlight without telling the panel would leave those aimed at a body nothing points to.
+58 -54
View File
@@ -8,7 +8,6 @@
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/panel.h>
#include <wx/popupwin.h>
#include <functional>
#include <memory>
@@ -17,6 +16,7 @@
#include "slic3r/GUI/3DBed.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GLToolbar.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "slic3r/GUI/CAD/DesignSketchTool.hpp"
@@ -46,10 +46,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();
@@ -76,7 +76,7 @@ public:
// Is the sketch tool on Select (as opposed to a draw/edit tool being armed)? The
// Construction box needs it to tell "convert what I picked" from "arm what I draw next".
bool sketch_is_selecting() const { return m_sketch_tool.mode() == DesignSketchTool::Mode::Select; }
// Text / SVG art into the LIVE sketch, as ordinary editable lines. False = no session.
// SVG art into the LIVE sketch, as ordinary editable lines. False = no session.
bool add_sketch_regions(const std::vector<std::vector<std::vector<Vec2d>>>& regions);
void set_sketch_polygon_sides(int n);
void set_sketch_polygon_circumscribed(bool c);
@@ -93,6 +93,10 @@ public:
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void set_show_bed(bool b); // view option: draw the printer bed + plate grid, or not
// The sidebar's collapse button, as Prepare's: drawn on the edge `side` reports, it runs
// `toggle` on a click and on Shift+Tab. Set before the first paint.
void set_sidebar_collapse(std::function<CollapseSide()> side, std::function<void()> toggle);
void set_sidebar_collapse_tooltip(const std::string& tooltip);
// N: look straight down the sketch plane's normal, keeping the current zoom. A sketch drawn
// at an angle is a sketch drawn wrong, and no amount of orbiting by hand lands exactly square.
bool view_normal_to_sketch();
@@ -125,7 +129,12 @@ public:
// consumed loop must be compared against.
std::vector<std::vector<int>> region_entity_indices_with_holes(const std::vector<SketchEntity>& ents) const;
void clear_loop_pick(); // drop the click-selected loop highlight (e.g. after extrude)
void clear_solid_pick(); // drop the solid pick and its highlight; no callback, no repaint
void set_loop_pick(int feature, int region); // adopt a loop pick made before the commit
// The picked committed sketch (feature index, -1 = none) and its closed region (-1 also when
// the click hit a stroke on no closed loop). The panel reads the pick here and keeps no copy.
int loop_pick_feature() const;
int loop_pick_region() const;
void set_escalate_on_repick(bool on); // off while a card has armed a face/edge pick
// Solid whole/face/edge selection: point the tool at the bodies + concatenated
// tessellation (with per-triangle face & body ids), and a callback fired on each
@@ -135,7 +144,8 @@ public:
const std::vector<bool>* visible = nullptr,
const std::vector<Transform3d>* xform = nullptr);
void set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb);
void set_on_place_on_face(std::function<bool()> cb); // F key: Place on Face
void set_on_empty_pick(std::function<void()> cb); // a click or rubber band took nothing
std::vector<int> selected_solid_edges() const; // the Shift/Ctrl+click edge set, last-clicked at the end
void select_body(int body); // Parts-list -> highlight a whole body by index
// Effective display colour of a body: the per-body override (Color tool) when set,
// otherwise the auto body-index palette. Single source of truth shared with reload().
@@ -174,11 +184,11 @@ public:
void clear_shell_gizmo();
bool shelling() const;
void set_on_shell_thickness_changed(std::function<void(double)> cb);
// Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + axis
// (0=plane X, 1=plane Y) + angle + flip while its Revolve card is open; drag/edit fire the
// angle callback.
// Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + the
// world axis (a point on it, unit direction) + angle + flip while its Revolve card is open;
// drag/edit fire the angle callback.
void begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
int axis_sel, double angle, bool flip);
const Vec3d& axis_origin, const Vec3d& axis_dir, double angle, bool flip);
void clear_revolve_gizmo();
bool revolving() const;
void set_on_revolve_angle_changed(std::function<void(double)> cb);
@@ -226,7 +236,7 @@ public:
void set_on_datum_base_picked(std::function<void(int)> cb);
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_undo_redo(std::function<void(bool /*redo*/)> cb); // Ctrl+Z / Ctrl+Shift+Z
void set_on_sketch_notice(std::function<void(const std::string&, bool)> cb); // tool refusals/side effects
// Persistently draw committed sketches (un-consumed ones stay visible).
void set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds);
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl);
@@ -235,26 +245,23 @@ 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.
void set_status_text(const wxString& text, const wxColour& colour);
// Take the status line down / bring it back when the Design page leaves and re-enters view.
// A popup is a TOP-LEVEL window: hiding the page it belongs to does not hide it. Keeps the
// text, so coming back needs no re-selection.
void show_status_hud(bool on);
void set_operand_bodies(int target_body, int tool_body); // -1,-1 clears
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
void set_xray_focus(int body); // >=0: fade+lock out every other body (CoordSys picking)
void set_body_hidden(bool on); // preview-only: hide base bodies, show only the result ghost
void set_on_move_exit(std::function<void()> cb); // right-click finished the move-body gizmo
// Right-click (or its platform equivalent) on the viewport with no tool running: open the
// object-driven offer there. Fires with SCREEN coordinates. Deliberately NOT fired while a
// tool is live — right-click already ends a polyline chain and finishes the move gizmo, and
// taking those over would break two working interactions in order to add a third.
void set_on_context_menu(std::function<void(const wxPoint&)> cb);
void delete_selected_sketch_entities();
bool delete_selected_sketch_entities(); // false when nothing was selected
bool inline_busy() const; // a sketch value field is open (guard keys)
bool inline_has_focus() const; // the field itself holds keyboard focus
void inline_commit(); // accept the typed value (Enter/Tab)
@@ -264,8 +271,10 @@ public:
// panel can do it when focus is not on the canvas.
void request_sketch_exit();
bool live_sketch_has_work() const; // the live sketch holds entities a cancel would destroy
bool undo_last_sketch_entity(); // Ctrl+Z in a sketch: drop the last entity
bool delete_selected_or_last_sketch_entity(); // Delete in a sketch: selected, else last
bool undo_last_sketch_entity(); // Ctrl+Z in a sketch: drop the last drawn shape
bool redo_last_sketch_entity(); // Ctrl+Y in a sketch: bring it back
bool can_undo_sketch_entity() const { return m_sketch_tool.can_undo_entity(); }
bool can_redo_sketch_entity() const { return m_sketch_tool.can_redo_entity(); }
void clear_sketch_selection();
// View toggles (keys P / A): origin planes, world axis triad. Each returns the new on/off
@@ -312,11 +321,12 @@ public:
// Esc routing (DesignInteraction.hpp). The panel decides WHICH level one press belongs to;
// these are the levels it can act on inside the canvas. Each returns whether it did anything,
// so the panel can fall through to the next level without asking twice.
bool sketch_abort_gesture(); // CadLevel::Gesture — drop the entity being drawn
bool sketch_abort_gesture(); // CadLevel::Gesture — drop the entity being drawn, or the tool's picks
bool sketch_disarm_tool(); // CadLevel::Tool — armed sketch tool falls back to Select
bool drawing_in_progress() const;// an entity has clicks down but is not committed
bool has_any_selection() const; // model pick or sketch pick
bool clear_any_selection(); // CadLevel::Idle — drop both; true if anything was dropped
bool sketch_confirm_pending(); // Enter — apply a ready edit-op or transform
bool drawing_in_progress() const;// clicks or picks are down but nothing is committed yet
bool has_any_selection() const; // model pick, committed-loop pick or sketch pick
bool clear_any_selection(); // CadLevel::Idle — drop all three; true if anything was dropped
bool sketch_first_selected_type(SketchEntity::Type& out) const;
// Live sketch session (Fase 4.2 live constraint path): the panel reads the in-session
// selection and entities, and commits a planned constraint through the tool's
@@ -373,14 +383,30 @@ 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
GLToolbar m_collapse_toolbar{GLToolbar::Normal, "Collapse"};
std::function<void(const wxPoint&)> m_on_context_menu;
bool m_ctx_bound{false}; // bind the RIGHT_UP handler once, however often the cb is set
wxPoint m_ctx_press{0, 0}; // right-press origin: a right-DRAG orbits, it must not offer
long long m_ctx_press_ms{0}; // and a right-HOLD is navigation too, however still it is held
bool m_ctx_travelled{false}; // the right press wandered past the budget at ANY point,
// so an orbit that ends where it began is still an orbit
Bed3D m_bed;
// The half of the camera swap above that is NOT on screen: the editor tabs' view while
@@ -390,7 +416,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
@@ -408,36 +433,15 @@ private:
bool m_section_on{false};
std::unique_ptr<SketchInlineEditor> m_inline_editor; // floating in-canvas value editor
// Bottom-right viewport HUD: a borderless float label over the GL canvas showing the
// active tool's current values (fed by the tool's on_readout). Empty text hides it.
// A wxPopupWindow for the SAME reason as the status chip below, and it was a wxFrame until
// the reason was measured rather than assumed: "it appears mid-gesture and the next input is
// the mouse" is false. The chip keeps the last value on screen AFTER the gesture ends, and a
// frame holds the X input focus once it has it — so the next keystroke went to a 119x31
// window that has no use for it. Measured on :10: focus on the chip, `r` produced no
// CHAR_HOOK line at all; one bare canvas click moved focus back and the same key armed the
// tool. That is every sketch shortcut dead after every dimensioned entity.
wxPopupWindow* m_hud{nullptr};
wxStaticText* m_hud_label{nullptr};
// The viewport chips, drawn in the tool's ImGui pass (render_hud): bottom-right the active
// tool's current values (fed by the tool's on_readout), bottom-left the status line written by
// DesignPanel. Empty text draws nothing. They were top-level popups once; a popup does not
// follow its frame, so it floated over other applications and outlived the tab.
std::string m_hud_last;
void set_readout(const std::string& text);
void place_readout_hud(); // anchor + show, using m_hud_last
void show_readout_hud(bool on); // iconise/deactivate: a popup would float on the desktop
// Bottom-LEFT viewport HUD: the selection / tool status line, written by DesignPanel.
// A wxPopupWindow, NOT the wxFrame the readout HUD uses: a frame accepts keyboard focus,
// and this one is on screen permanently and re-raised on every status change, so it stole
// the keyboard from the canvas and killed every sketch shortcut in the tab.
wxPopupWindow* m_status_hud{nullptr};
wxStaticText* m_status_hud_label{nullptr};
wxString m_status_hud_last;
wxColour m_status_hud_colour;
void place_status_hud(); // re-anchors to the canvas corner (also on resize)
void apply_status_label(); // SetLabel + Wrap to the canvas width + Fit, always together
// On the top-level frame, which outlives this canvas — members so they can be unbound.
void on_frame_iconize(wxIconizeEvent& e);
void on_frame_activate(wxActivateEvent& e);
void on_status_hud_reanchor(wxEvent& e); // frame wxEVT_MOVE and canvas wxEVT_SIZE
wxColour m_status_hud_colour; // wxNullColour: the overlay's own text colour
void set_readout(const std::string& text);
void render_hud();
std::function<void(const SketchProfile&, const SketchPlane&)> m_on_sketch_commit;
std::function<void(const std::vector<SketchEntity>&,
const std::vector<SketchEntityConstraintDef>&,
+4 -4
View File
@@ -48,10 +48,10 @@ static_assert(cad_escape_level({false, false, true, true}) == CadLevel::Tool,
static_assert(cad_escape_level({false, false, false, true}) == CadLevel::Idle, "selection is idle-level");
static_assert(cad_escape_level({false, false, false, false}) == CadLevel::Idle, "empty is idle");
// Right-click vs. right-hold-orbit. A press that stays put and is let go promptly is a click and
// summons the offer; anything longer or further was navigation, and navigation must never be
// rewarded with a menu over wherever the camera happened to stop.
inline constexpr int kCadRightClickMs = 200; // press->release budget
// Right-click vs. right-drag-orbit. A press that stays put summons the offer; one that travelled
// was navigation, and navigation must never be rewarded with a menu over wherever the camera
// happened to stop. Distance only, never duration: charter 6.2 rules out any gesture that
// depends on timing, and a press that did not move did not move the camera either.
inline constexpr int kCadRightClickDriftPx = 3; // cursor drift budget, max(|dx|,|dy|)
}} // namespace Slic3r::GUI
+125 -103
View File
@@ -10,6 +10,13 @@
#include <cstdint>
#ifndef L
#define L(s) s // gettext marker, as in slic3r/GUI/I18N.hpp
#endif
#ifndef L_CONTEXT
#define L_CONTEXT(s, context) s
#endif
namespace Slic3r { namespace GUI {
// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in
@@ -48,8 +55,8 @@ inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }
// nullptr -> kernel support exists, no GUI path yet (row shows disabled)
struct OfferVerb {
const char* id;
const char* name; // drawing-office word (L10); translated at use with wxGetTranslation
int row; // 0..7, the ratified index — NEVER reorder
const char* name; // drawing-office word (L10); msgctxt "Design", translated at use
int row; // index into kOfferRowNames, the ratified address — NEVER reorder
const char* key; // shortcut shown in the row, or nullptr
const char* action;
const char* refusal; // why this row is greyed, in the product's own words
@@ -69,118 +76,133 @@ struct OfferVerb {
// Row labels, in ratified order.
static const char* const kOfferRowNames[] = {
"Create",
"Add material",
"Remove",
"Fillet / chamfer / draft",
"Repeat",
"Transform",
"Reference",
"Modify",
L("Create"),
L("Add material"),
L("Remove"),
L("Fillet / chamfer / draft"),
L("Repeat"),
L("Transform"),
L("Reference"),
L("Modify"),
"Top",
};
static const int kOfferRowCount = 9;
// A flat row is not a family: its verbs come first, at the top level of the offer, each
// an item of its own.
static const bool kOfferRowFlat[] = {
false,
false,
false,
false,
false,
false,
false,
false,
true,
};
static const int kOfferRowCount = 8;
static const OfferVerb kOfferVerbs[] = {
{"sketch", "Sketch", 0, "Shift+S", "key:S+S", "Click a face or a reference plane in the viewport, then a sketch tool", 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", "Click a face or a reference plane, then pick a drawing tool"},
{"extrude", "Extrude", 1, "Shift+E", "key:S+E", "Create a sketch, or pick a solid face, first", 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch profile, or push/pull a picked face"},
{"revolve", "Revolve", 1, "Shift+R", "key:S+R", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a profile about an axis"},
{"sweep", "Sweep", 1, "Shift+W", "key:S+W", "Create a profile sketch to sweep first", 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", "Sweep a profile along a path"},
{"loft", "Loft", 1, "Shift+L", "key:S+L", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between two or more profiles"},
{"thicken", "Thicken", 1, nullptr, "fly:material#4", "Thicken needs a solid body — add or import one first", 0x0000000au, 1, 0, false, false, nullptr, "design_thicken", "Offset a solid face into a thin plate (new body)"},
{"rib", "Rib", 1, nullptr, "fly:material#5", "Rib needs a solid body — add or import one first", 0x00010000u, 1, 0, false, false, nullptr, "design_rib", "Grow a thin wall from an open sketch line, fused to a body"},
{"boolean", "Union", 1, "Shift+B", "btn:bool#0", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Fuse the tool body into the target — one solid, no seam"},
{"bool_subtract", "Subtract", 1, nullptr, "btn:bool#1", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Cut the tool body out of the target"},
{"bool_intersect", "Intersect", 1, nullptr, "btn:bool#2", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Keep only where the two bodies overlap"},
{"surf_extrude", "Surface Extrude", 1, "Shift+G", "key:S+G", "Create a sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch into a sheet body (no end caps)"},
{"surf_revolve", "Surface Revolve", 1, nullptr, "fly:surface#1", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a sketch profile into a sheet body"},
{"surf_loft", "Surface Loft", 1, nullptr, "fly:surface#2", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between 2+ profiles, open (no end caps)"},
{"surf_fill", "Surface Fill", 1, nullptr, "fly:surface#3", "Create a closed sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_surface", "Fill a sketch boundary with a smooth face"},
{"thicken_surf", "Thicken Surface", 1, nullptr, "fly:surface#5", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_thicken", "Thicken a sheet body into a solid"},
{"hole", "Hole", 2, "Shift+H", "key:S+H", "Pick a face or a plane to drill into", 0x00000402u, 1, 0, false, false, nullptr, "design_hole", "Drill a hole, centred on a picked face or placed on a plane"},
{"thread", "Thread", 2, "Shift+T", "key:S+T", "Pick a cylindrical surface (bore / outer) or a circular edge for a thread", 0x00000024u, 1, 0, false, false, nullptr, "design_thread", "Thread a cylindrical surface (inner bore / outer) or a circular edge"},
{"shell", "Shell", 2, "Shift+K", "key:S+K", "Shell needs a solid body", 0x00000082u, 1, 0, false, false, nullptr, "design_shell", "Hollow the body to a wall thickness, opening a picked face"},
{"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x000004feu, 1, 0, false, false, nullptr, "design_cut", "Trim the body with a plane — drag the offset arrow; keep one half or both"},
{"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x000000feu, 1, 0, false, false, nullptr, nullptr, "Split the body along a picked face into two solids"},
{"fillet", "Fillet", 3, "Shift+F", "btn:dress#0", "Pick an edge to round", 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", "Pick an edge, then drag the radius arrow or type it"},
{"chamfer", "Chamfer", 3, nullptr, "btn:dress#1", "Pick an edge to bevel", 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", "Pick an edge, then drag the distance arrow or type it"},
{"draft", "Draft", 3, "Shift+D", "key:S+D", "Pick a face to taper", 0x0000000au, 1, 0, false, false, nullptr, "design_draft", "Tilt a picked face by a draft angle"},
{"surf_offset", "Surface Offset", 3, nullptr, "fly:surface#4", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_offset", "Offset a sheet body's shell by a signed distance"},
{"pattern", "Linear pattern", 4, "Shift+N", "btn:pat#0", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_array", "Repeat the body along a direction — drag the spacing, set the count"},
{"pattern_circular", "Circular pattern", 4, nullptr, "btn:pat#1", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_polararray", "Repeat the body around an axis — set the count and sweep"},
{"mirror", "Mirror", 4, "Shift+Z", "key:S+Z", "Mirror needs a body — add or import one first", 0x000004feu, 1, 0, false, false, nullptr, "design_mirror", "Reflect a body about a plane"},
{"pat_curve", "Pattern on Curve", 4, nullptr, nullptr, "Pattern on curve needs a body and a curve", 0x00000090u, 1, 0, false, false, nullptr, nullptr, "Repeat the body along a picked curve"},
{"transform", "Move", 5, "Shift+Y", "key:S+Y", "Transform needs a body — add or import one first", 0x000021feu, 1, 0, false, false, nullptr, "design_move", "Move and/or rotate an existing body"},
{"mate", "Mate", 5, nullptr, "fly:placement#2", "A mate needs two coordinate systems", 0x00001202u, 2, 0, false, false, nullptr, "design_c_coincident", "Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"},
{"align", "Align to", 5, nullptr, nullptr, "Align needs a body", 0x00000002u, 1, 0, false, false, nullptr, nullptr, "Align the body to a picked face or plane"},
{"plane", "Plane", 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false, nullptr, "design_plane", "Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"},
{"axis", "Axis", 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false, nullptr, "design_line", "Datum axis (two points, face normal, cylinder centerline, two planes, along edge)"},
{"coordsys_v", "Coord Sys", 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false, nullptr, "design_point", "Datum coordinate system (world point, or face + direction edge)"},
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false, nullptr, "design_thread", "Helical curve (spring path) — use as a sweep path for coils / springs / augers"},
{"project", "Project", 6, nullptr, "fly:plane#4", "Project needs a body — add or import one first", 0x00000482u, 1, 0, false, false, nullptr, "design_sketch", "Project body edges onto a plane as sketch entities"},
{"measure", "Measure", 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false, nullptr, nullptr, "Measure between the picked points, edges or faces"},
{"mass_props", "Mass", 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", "Report the volume and surface area of the selected body"},
{"interference", "Interference", 6, nullptr, nullptr, nullptr, 0x00000200u, 2, 0, false, false, nullptr, nullptr, "Check whether two bodies overlap — reports, changes nothing"},
{"edit_feature", "Edit", 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", "Reopen the selected feature to change what it was made from"},
{"rename", "Rename…", 7, "F2", "btn:rename", "Select a feature, or a body, to rename it", 0x00004080u, 0, 0, false, false, nullptr, nullptr, "Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)"},
{"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", "Remove faces from a body and heal the solid"},
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", "Set the selected body's display colour"},
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", "Delete what is selected"},
{"delete_body", "Delete Body", 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", "Delete this whole body — removes the feature it was made from"},
{"sk_line_t", "Line", 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_line", "Line — click start, then end"},
{"sk_polyline", "Polyline", 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_polyline", "Click points; click the first point to close the loop, right-click to end it open"},
{"sk_rect", "Corner rectangle", 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect", "Rectangle — click two opposite corners"},
{"sk_rect_center", "Centre rectangle", 0, nullptr, "fly:design_rect#1", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_crect", "Click center, then a corner"},
{"sk_rect_oblique", "Oblique rectangle", 0, nullptr, "fly:design_rect#2", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_oblique", "Click two corners of one edge, then a point for the width"},
{"sk_rect_rounded", "Rounded rectangle", 0, nullptr, "fly:design_rect#3", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_rounded", "Click two opposite corners, then a point for the corner radius"},
{"sk_circle", "Centre circle", 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle", "Circle — click center, then radius"},
{"sk_circle_2pt", "2-point circle", 0, nullptr, "fly:design_circle#1", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle2pt", "Click two ends of the diameter"},
{"sk_circle_3pt", "3-point circle", 0, nullptr, "fly:design_circle#2", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle3pt", "Click three points on the circle"},
{"sk_arc_t", "3-point arc", 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc3pt", "Arc — click start, end, then a point"},
{"sk_arc_tangent", "Tangent arc", 0, nullptr, "fly:design_arc3pt#1", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_tangentarc", "Click start (on the last entity) then end"},
{"sk_arc_center", "Centre-point arc", 0, nullptr, "fly:design_arc3pt#2", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc_center", "Click center, then start, then a point for the end angle"},
{"sk_slot", "Slot", 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot", "Slot — two centerline ends, then end radius"},
{"sk_slot_arc", "Arc slot", 0, nullptr, "fly:design_slot#1", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot_arc", "Click center, start, end, then a point for the width"},
{"sk_ellipse", "Ellipse", 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse", "Ellipse — center, major end, minor point"},
{"sk_ellipse_arc", "Elliptical arc", 0, nullptr, "fly:design_ellipse#1", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse_arc", "Click center, major-axis end, minor point, then arc start and end"},
{"sk_spline", "Spline", 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_bspline", "Spline — click control points"},
{"sk_poly_3", "Triangle", 0, nullptr, "btn:poly#3", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Triangle — click centre, then a vertex"},
{"sk_poly_4", "Square", 0, nullptr, "btn:poly#4", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Square — click centre, then a vertex"},
{"sk_poly_5", "Pentagon", 0, nullptr, "btn:poly#5", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Pentagon — click centre, then a vertex"},
{"sk_polygon", "Hexagon", 0, "G", "btn:poly#6", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Hexagon — click centre, then a vertex"},
{"sk_poly_8", "Octagon", 0, nullptr, "btn:poly#8", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Octagon — click centre, then a vertex"},
{"sk_poly_12", "Dodecagon", 0, nullptr, "btn:poly#12", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Dodecagon — click centre, then a vertex"},
{"sk_poly_inscribed", "Inscribed", 0, nullptr, "btn:polyfit#0", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its corners (inscribed)"},
{"sk_poly_circumscribed", "Circumscribed", 0, nullptr, "btn:polyfit#1", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its flats (circumscribed)"},
{"sk_point_t", "Point", 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_point", "Point — click to place"},
{"sk_text", "Text", 0, nullptr, "btn:text", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_text", "Type text; its outline is added to this sketch as editable lines"},
{"sk_svg", "SVG", 0, nullptr, "btn:svg", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_svg", "Import an SVG outline into this sketch as editable lines"},
{"sk_offset", "Offset", 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_offset", "Offset — pick an entity, drag the distance"},
{"sk_trim", "Trim", 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_trim", "Trim — click a segment to trim it"},
{"sk_fillet", "Fillet", 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_filletedge", "Fillet — pick two lines, set the radius"},
{"sk_chamfer", "Chamfer", 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_chamfer", "Chamfer — pick two lines, set the distance"},
{"sk_array", "Linear array", 4, nullptr, "fly:design_array#0", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_array", "Pick entities, drag the spacing handle, click the count; click empty to apply"},
{"sk_array_polar", "Polar array", 4, nullptr, "fly:design_array#1", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_polararray", "Pick entities, drag the sweep handle, click the count; click empty to apply"},
{"sk_mirror", "Mirror", 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_mirror", "Mirror — pick axis, then entities"},
{"sk_move", "Move", 5, nullptr, "fly:design_move#0", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_move", "Pick entities, then drag the handle or click the distance; click empty to apply"},
{"sk_rotate", "Rotate", 5, nullptr, "fly:design_move#1", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_rotate", "Pick entities, then drag around the pivot or click the angle; click empty to apply"},
{"sk_scale", "Scale", 5, nullptr, "fly:design_move#2", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_scale", "Pick entities, then drag the handle or click the factor; click empty to apply"},
{"sk_dimension", "Dimension", 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_dimension", "Dimension — click 2 points or an entity"},
{"sk_constrain", "Constrain", 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_constrain", "Constrain the selected sketch entities to each other"},
{"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")},
{"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")},
{"loft", L_CONTEXT("Loft", "Design"), 1, "Shift+L", "key:S+L", L("Create at least two profile sketches to loft"), 0x00004000u, 0, 2, false, false, nullptr, "design_loft", L("Loft (skin) between two or more profiles")},
{"thicken", L_CONTEXT("Thicken", "Design"), 1, nullptr, "fly:material#4", L("Thicken needs a solid body — add or import one first"), 0x0000000au, 1, 0, false, false, nullptr, "design_thicken", L("Offset a solid face into a thin plate (new body)")},
{"rib", L_CONTEXT("Rib", "Design"), 1, "R", "fly:material#5", L("Rib needs a solid body — add or import one first"), 0x00010000u, 1, 0, false, false, nullptr, "design_rib", L("Grow a thin wall from an open sketch line, fused to a body")},
{"boolean", L_CONTEXT("Join", "Design"), 1, "Shift+B", "btn:bool#0", L("Boolean needs two bodies — add or import a second one"), 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", L("Fuse the tool body into the target — one solid, no seam")},
{"bool_subtract", L_CONTEXT("Subtract", "Design"), 1, nullptr, "btn:bool#1", L("Boolean needs two bodies — add or import a second one"), 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", L("Cut the tool body out of the target")},
{"bool_intersect", L_CONTEXT("Intersect", "Design"), 1, nullptr, "btn:bool#2", L("Boolean needs two bodies — add or import a second one"), 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", L("Keep only where the two bodies overlap")},
{"surf_extrude", L_CONTEXT("Surface Extrude", "Design"), 1, "Shift+G", "key:S+G", L("Create a sketch first"), 0x00004000u, 0, 0, false, false, nullptr, "design_extrude", L("Extrude a sketch into a sheet body (no end caps)")},
{"surf_revolve", L_CONTEXT("Surface Revolve", "Design"), 1, nullptr, "fly:surface#1", L("Create a sketch profile to revolve first"), 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", L("Revolve a sketch profile into a sheet body")},
{"surf_loft", L_CONTEXT("Surface Loft", "Design"), 1, nullptr, "fly:surface#2", L("Create at least two profile sketches to loft"), 0x00004000u, 0, 2, false, false, nullptr, "design_loft", L("Loft (skin) between 2+ profiles, open (no end caps)")},
{"surf_fill", L_CONTEXT("Surface Fill", "Design"), 1, nullptr, "fly:surface#3", L("Create a closed sketch first"), 0x00004000u, 0, 0, false, false, nullptr, "design_surface", L("Fill a sketch boundary with a smooth face")},
{"thicken_surf", L_CONTEXT("Thicken Surface", "Design"), 1, nullptr, "fly:surface#5", L("Thicken Surface needs a surface body — make one with a Surface tool first"), 0x00000100u, 0, 0, true, false, nullptr, "design_thicken", L("Thicken a sheet body into a solid")},
{"hole", L_CONTEXT("Hole", "Design"), 2, "Shift+H", "key:S+H", L("Pick a face or a plane to drill into"), 0x00000402u, 1, 0, false, false, nullptr, "design_hole", L("Drill a hole, centerd on a picked face or placed on a plane")},
{"thread", L_CONTEXT("Thread", "Design"), 2, "Shift+T", "key:S+T", L("Pick a cylindrical surface (bore / outer) or a circular edge for a thread"), 0x00000024u, 1, 0, false, false, nullptr, "design_thread", L("Thread a cylindrical surface (inner bore / outer) or a circular edge")},
{"shell", L_CONTEXT("Shell", "Design"), 2, "Shift+K", "key:S+K", L("Shell needs a solid body — add or import one first"), 0x00000082u, 1, 0, false, false, nullptr, "design_shell", L("Hollow the body to a wall thickness, opening a picked face")},
{"cut", L_CONTEXT("Cut", "Design"), 2, "Shift+X", "key:S+X", L("Cut needs a solid body — add or import one first"), 0x000004feu, 1, 0, false, false, nullptr, "design_cut", L("Trim the body with a plane — drag the offset arrow; keep one half or both")},
{"split", L_CONTEXT("Split", "Design"), 2, nullptr, nullptr, L("Split needs a solid body — add or import one first"), 0x000000feu, 1, 0, false, false, nullptr, nullptr, L("Split the body along a picked face into two solids")},
{"fillet", L_CONTEXT("Fillet", "Design"), 3, "Shift+F", "btn:dress#0", L("Pick an edge to round"), 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", L("Pick an edge, then drag the radius arrow or type it")},
{"chamfer", L_CONTEXT("Chamfer", "Design"), 3, nullptr, "btn:dress#1", L("Pick an edge to bevel"), 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", L("Pick an edge, then drag the distance arrow or type it")},
{"draft", L_CONTEXT("Draft", "Design"), 3, "Shift+D", "key:S+D", L("Pick a face to taper"), 0x0000000au, 1, 0, false, false, nullptr, "design_draft", L("Tilt a picked face by a draft angle")},
{"surf_offset", L_CONTEXT("Surface Offset", "Design"), 3, nullptr, "fly:surface#4", L("Surface Offset needs a surface body — make one with a Surface tool first"), 0x00000100u, 0, 0, true, false, nullptr, "design_offset", L("Offset a sheet body's shell by a signed distance")},
{"pattern", L_CONTEXT("Linear pattern", "Design"), 4, "Shift+N", "btn:pat#0", L("Pattern needs a solid body — add or import one first"), 0x00006082u, 1, 0, false, false, nullptr, "design_array", L("Repeat the body along a direction — drag the spacing, set the count")},
{"pattern_circular", L_CONTEXT("Circular pattern", "Design"), 4, nullptr, "btn:pat#1", L("Pattern needs a solid body — add or import one first"), 0x00006082u, 1, 0, false, false, nullptr, "design_polararray", L("Repeat the body around an axis — set the count and sweep")},
{"mirror", L_CONTEXT("Mirror", "Design"), 4, "Shift+Z", "key:S+Z", L("Mirror needs a body — add or import one first"), 0x000004feu, 1, 0, false, false, nullptr, "design_mirror", L("Reflect a body about a plane")},
{"pat_curve", L_CONTEXT("Pattern on Curve", "Design"), 4, nullptr, nullptr, L("Pattern on Curve needs a body and a curve"), 0x00000090u, 1, 0, false, false, nullptr, nullptr, L("Repeat the body along a picked curve")},
{"transform", L_CONTEXT("Move", "Design"), 5, "Shift+Y", "key:S+Y", L("Transform needs a body — add or import one first"), 0x000021feu, 1, 0, false, false, nullptr, "design_move", L("Move and/or rotate an existing body")},
{"mate", L_CONTEXT("Mate", "Design"), 5, nullptr, "fly:placement#2", L("Mate needs two coordinate systems — create them first"), 0x00001202u, 2, 0, false, false, nullptr, "design_c_coincident", L("Assembly: align two coordinate systems (fastened, planar, revolute, slider, cylindrical)")},
{"align", L_CONTEXT("Align to", "Design"), 5, nullptr, nullptr, L("Align needs a body — add or import one first"), 0x00000002u, 1, 0, false, false, nullptr, nullptr, L("Align the body to a picked face or plane")},
{"plane", L_CONTEXT("Plane", "Design"), 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false, nullptr, "design_plane", L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)")},
{"axis", L_CONTEXT("Axis", "Design"), 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false, nullptr, "design_line", L("Datum axis (two points, face normal, cylinder centerline, two planes, along edge)")},
{"coordsys_v", L_CONTEXT("Coordinate system", "Design"), 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false, nullptr, "design_point", L("Datum coordinate system (world point, or face + direction edge)")},
{"helix", L_CONTEXT("Helix", "Design"), 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false, nullptr, "design_thread", L("Helical curve (spring path) — use as a sweep path for coils / springs / augers")},
{"project", L_CONTEXT("Project", "Design"), 6, nullptr, "fly:plane#4", L("Project needs a body — add or import one first"), 0x00000482u, 1, 0, false, false, nullptr, "design_sketch", L("Project body edges onto a plane as sketch entities")},
{"measure", L_CONTEXT("Measure", "Design"), 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false, nullptr, nullptr, L("Measure between the picked points, edges or faces")},
{"mass_props", L_CONTEXT("Volume and area", "Design"), 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", L("Report the volume and surface area of the selected body")},
{"interference", L_CONTEXT("Interference", "Design"), 6, nullptr, "btn:interference", L("Interference needs at least two bodies"), 0x00000280u, 2, 0, false, false, nullptr, nullptr, L("Check whether two bodies overlap — reports, changes nothing")},
{"edit_feature", L_CONTEXT("Edit", "Design"), 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", L("Reopen the selected feature to change what it was made from")},
{"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")},
{"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")},
{"sk_polyline", L_CONTEXT("Polyline", "Design"), 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Line"), "design_polyline", L("Click points; click the first point to close the loop, right-click to end it open")},
{"sk_rect", L_CONTEXT("Corner rectangle", "Design"), 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_rect", L("Rectangle — click two opposite corners")},
{"sk_rect_center", L_CONTEXT("Center rectangle", "Design"), 0, nullptr, "fly:design_rect#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_crect", L("Click center, then a corner")},
{"sk_rect_oblique", L_CONTEXT("Oblique rectangle", "Design"), 0, nullptr, "fly:design_rect#2", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_rect_oblique", L("Click two corners of one edge, then a point for the width")},
{"sk_rect_rounded", L_CONTEXT("Rounded rectangle", "Design"), 0, nullptr, "fly:design_rect#3", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_rect_rounded", L("Click two opposite corners, then a point for the corner radius")},
{"sk_circle", L_CONTEXT("Center circle", "Design"), 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true, L("Circle"), "design_circle", L("Circle — click center, then radius")},
{"sk_circle_2pt", L_CONTEXT("2-point circle", "Design"), 0, nullptr, "fly:design_circle#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Circle"), "design_circle2pt", L("Click two ends of the diameter")},
{"sk_circle_3pt", L_CONTEXT("3-point circle", "Design"), 0, nullptr, "fly:design_circle#2", nullptr, 0x000f8000u, 0, 0, false, true, L("Circle"), "design_circle3pt", L("Click three points on the circle")},
{"sk_arc_t", L_CONTEXT("3-point arc", "Design"), 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true, L("Arc"), "design_arc3pt", L("Arc — click start, end, then a point")},
{"sk_arc_tangent", L_CONTEXT("Tangent arc", "Design"), 0, nullptr, "fly:design_arc3pt#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Arc"), "design_tangentarc", L("Click start (on the last entity) then end")},
{"sk_arc_center", L_CONTEXT("Center-point arc", "Design"), 0, nullptr, "fly:design_arc3pt#2", nullptr, 0x000f8000u, 0, 0, false, true, L("Arc"), "design_arc_center", L("Click center, then start, then a point for the end angle")},
{"sk_slot", L_CONTEXT("Slot", "Design"), 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true, L("Slot"), "design_slot", L("Slot — two centerline ends, then the width")},
{"sk_slot_arc", L_CONTEXT("Arc slot", "Design"), 0, nullptr, "fly:design_slot#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Slot"), "design_slot_arc", L("Click center, start, end, then a point for the width")},
{"sk_ellipse", L_CONTEXT("Ellipse", "Design"), 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true, L("Ellipse"), "design_ellipse", L("Ellipse — center, major end, minor point")},
{"sk_ellipse_arc", L_CONTEXT("Elliptical arc", "Design"), 0, nullptr, "fly:design_ellipse#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Ellipse"), "design_ellipse_arc", L("Click center, major-axis end, minor point, then arc start and end")},
{"sk_spline", L_CONTEXT("Spline", "Design"), 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_bspline", L("Spline — click control points")},
{"sk_poly_3", L_CONTEXT("Triangle", "Design"), 0, nullptr, "btn:poly#3", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Triangle — click center, then a vertex")},
{"sk_poly_4", L_CONTEXT("Square", "Design"), 0, nullptr, "btn:poly#4", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Square — click center, then a vertex")},
{"sk_poly_5", L_CONTEXT("Pentagon", "Design"), 0, nullptr, "btn:poly#5", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Pentagon — click center, then a vertex")},
{"sk_polygon", L_CONTEXT("Hexagon", "Design"), 0, "G", "btn:poly#6", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Hexagon — click center, then a vertex")},
{"sk_poly_8", L_CONTEXT("Octagon", "Design"), 0, nullptr, "btn:poly#8", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Octagon — click center, then a vertex")},
{"sk_poly_12", L_CONTEXT("Dodecagon", "Design"), 0, nullptr, "btn:poly#12", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Dodecagon — click center, then a vertex")},
{"sk_poly_inscribed", L_CONTEXT("Inscribed", "Design"), 0, nullptr, "btn:polyfit#0", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Measure the polygon to its corners (inscribed)")},
{"sk_poly_circumscribed", L_CONTEXT("Circumscribed", "Design"), 0, nullptr, "btn:polyfit#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Measure the polygon to its flats (circumscribed)")},
{"sk_point_t", L_CONTEXT("Point", "Design"), 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_point", L("Point — click to place")},
{"sk_text", L_CONTEXT("Text", "Design"), 0, nullptr, "btn:text", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_text", L("Type text; it becomes a Text feature on this sketch's plane, editable later")},
{"sk_svg", L_CONTEXT("SVG", "Design"), 0, nullptr, "btn:svg", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_svg", L("Import an SVG outline into this sketch as editable lines")},
{"sk_offset", L_CONTEXT("Offset", "Design"), 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_offset", L("Offset — pick an entity, drag the distance")},
{"sk_trim", L_CONTEXT("Trim", "Design"), 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_trim", L("Trim — click a segment to trim it")},
{"sk_fillet", L_CONTEXT("Fillet", "Design"), 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_filletedge", L("Fillet — pick two lines, set the radius")},
{"sk_chamfer", L_CONTEXT("Chamfer", "Design"), 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_chamfer", L("Chamfer — pick two lines, set the distance")},
{"sk_array", L_CONTEXT("Linear array", "Design"), 4, nullptr, "fly:design_array#0", nullptr, 0x000b0000u, 0, 0, false, true, L("Array"), "design_array", L("Pick entities, drag the spacing handle, click the count; click empty to apply")},
{"sk_array_polar", L_CONTEXT("Polar array", "Design"), 4, nullptr, "fly:design_array#1", nullptr, 0x000b0000u, 0, 0, false, true, L("Array"), "design_polararray", L("Pick entities, drag the sweep handle, click the count; click empty to apply")},
{"sk_mirror", L_CONTEXT("Mirror", "Design"), 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_mirror", L("Mirror — pick axis, then entities")},
{"sk_move", L_CONTEXT("Move", "Design"), 5, nullptr, "fly:design_move#0", nullptr, 0x000f0000u, 0, 0, false, true, L("Move"), "design_move", L("Pick entities, then drag the handle or click the distance; click empty to apply")},
{"sk_rotate", L_CONTEXT("Rotate", "Design"), 5, nullptr, "fly:design_move#1", nullptr, 0x000f0000u, 0, 0, false, true, L("Move"), "design_rotate", L("Pick entities, then drag around the pivot or click the angle; click empty to apply")},
{"sk_scale", L_CONTEXT("Scale", "Design"), 5, nullptr, "fly:design_move#2", nullptr, 0x000f0000u, 0, 0, false, true, L("Move"), "design_scale", L("Pick entities, then drag the handle or click the factor; click empty to apply")},
{"sk_dimension", L_CONTEXT("Dimension", "Design"), 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_dimension", L("Dimension — click 2 points or an entity")},
{"sk_constrain", L_CONTEXT("Constrain", "Design"), 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_constrain", L("Constrain the selected sketch entities to each other")},
// Same verb, model-mode vocabulary: offered when a SKETCH is selected (bit 14, SkLoop), the
// state a user is in right after finishing one. Without this row the only way in was the
// toolbar icon, and constraints read as absent — see the Onshape-comparison report.
{"constrain", "Constrain sketch", 7, nullptr, "btn:constrain", "Select a sketch to constrain it", 0x00004000u, 0, 1, false, false, nullptr, "design_constrain", "Add dimensions and relations (coincident, tangent, parallel...) to the selected sketch"},
{"sk_construct", "Construction", 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true, nullptr, nullptr, "Toggle construction: geometry that guides but is never built"},
{"sk_extend", "Extend", 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", "Extend — click a line/arc to extend it"},
{"sk_delete", "Delete", 7, "Del", "btn:sk_delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", "Delete the selected sketch entities"},
// It sits in the Reference row, the address the sketch-mode Constrain has: one verb, one row.
{"constrain", L_CONTEXT("Constrain sketch", "Design"), 6, nullptr, "btn:constrain", L("Select a sketch to constrain it"), 0x00004000u, 0, 1, false, false, nullptr, "design_constrain", L("Add dimensions and relations (coincident, tangent, parallel...) to the selected sketch")},
{"sk_construct", L_CONTEXT("Construction", "Design"), 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true, nullptr, nullptr, L("Toggle construction: geometry that guides but is never built")},
{"sk_extend", L_CONTEXT("Extend", "Design"), 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", L("Extend — click a line/arc to extend it")},
{"sk_delete", L_CONTEXT("Delete", "Design"), 7, "Del", "btn:sk_delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", L("Delete the selected sketch entities")},
// Typing the defining number of the element you pointed at. Three rows rather than one so
// each names the quantity in the drawing-office word for THAT element; all three land on
// the same handler, because dimension_kind() already resolves the quantity from the
// selection. Without these, an element's own numbers were reachable only by arming the
// Dimension tool and re-picking geometry that was already selected.
{"sk_length", "Length…", 7, "V", "key:V", nullptr, 0x00010000u, 0, 0, false, true, nullptr, "design_dimension", "Type the length of this line"},
{"sk_radius", "Radius / diameter…", 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", "Type the radius of this arc, or the diameter of this circle"},
{"sk_angdist", "Angle / distance…", 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", "Type the angle between two lines, or the distance between the two picks"},
{"sk_length", L_CONTEXT("Length…", "Design"), 7, "V", "key:V", nullptr, 0x00010000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the length of this line")},
{"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;
File diff suppressed because it is too large Load Diff
+139 -52
View File
@@ -1,7 +1,9 @@
#ifndef slic3r_DesignPanel_hpp_
#define slic3r_DesignPanel_hpp_
#include <cstdint>
#include <string>
#include <utility>
#include "libslic3r/Point.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -12,7 +14,6 @@
#include <wx/panel.h>
#include <wx/scrolwin.h>
#include <wx/string.h>
#include <wx/treebase.h> // wxTreeItemId
#include <vector>
#include <memory>
@@ -21,6 +22,7 @@
#include "libslic3r/CAD/CadDocument.hpp"
#include "slic3r/GUI/CAD/DesignInteraction.hpp" // CadLevel: what one Esc press means
#include "slic3r/GUI/AuiMgr.hpp"
#include "slic3r/GUI/Lazy.hpp"
class ComboBox; // Orca dropdown (Widgets/ComboBox.hpp) — replaces wxChoice everywhere here
@@ -29,12 +31,11 @@ class wxCheckBox;
class wxCheckListBox;
class wxSpinCtrl;
class wxSpinCtrlDouble;
class wxTreeCtrl;
class wxImageList;
class wxStaticText;
class wxStaticLine;
class Button; // Orca-styled button (Widgets/Button.hpp)
class CheckBox; // Orca teal checkbox (Widgets/CheckBox.hpp)
class DropDown; // Orca themed popup list (Widgets/DropDown.hpp)
class wxSizer;
// wxBoxSizer, wxTextCtrl and wxListCtrl are used here as pointers only, so a forward
// declaration is enough — but they must be declared. Every ordinary build happened to pull
@@ -45,12 +46,15 @@ class wxSizer;
class wxBoxSizer;
class wxTextCtrl;
class wxListCtrl;
class wxButton;
class Button;
class wxPanel;
class ScalableButton;
namespace Slic3r { namespace GUI {
class DesignTextDialog;
class DesignRowList;
class DesignCanvas;
// Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel.
@@ -62,8 +66,14 @@ public:
explicit DesignPanel(wxWindow* parent);
void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated
void on_tab_hidden(); // another tab took over: take the viewport status line down with us
// From MainFrame, like the other pages: re-rasterise the icons at the new scale, and on a theme
// switch also move every token colour onto the new theme.
void msw_rescale();
void on_sys_color_changed();
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void shutdown(); // app close / language switch, from MainFrame: save the window layout
void reset_window_layout(); // View > Reset Window Layout, with Prepare's
void clear_document(); // New Project / Open Project: drop the document with the project
// Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature
// op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result.
@@ -96,6 +106,13 @@ public:
// Defined out of line in DesignPanel.cpp: it needs kOfferVerbs, which this header deliberately
// does not include (the table is generated and belongs to the offer-menu code).
bool mcp_run_verb(const char* verb_id);
// Would a request from the control socket collide with what the GUI is doing — a rebuild
// in progress, an open feature card, a sketch session? `sketch_method` = it drives the live
// sketch rather than the feature list. Fills `why` when it would.
bool mcp_busy(bool sketch_method, std::string& why) const;
// Load the project's recipe into an empty document, as showing the tab does. The control
// socket may be the first thing to touch the Design tab after a project was opened.
void hydrate_from_model();
private:
enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert, Axis, CoordSys, SurfaceExtrude, SurfaceRevolve, SurfaceLoft, SurfaceFill, SurfaceOffset, ThickenSurface, Transform, Mirror, Thicken, Rib, Project, DeleteFace, Helix, Mate };
@@ -124,17 +141,20 @@ private:
// used to be handled in four places that could not see each other, and that is how two
// presses in a row reached past a tool and discarded the sketch under it.
CadLevel escape_level() const;
bool confirm_enabled() const; // would the ✓ act right now? (its greying follows this)
void escape();
void update_action_bar(); // show the ✓/✗ bar iff a tool or mode is active
void update_confirm_button(); // grey the ✓ to confirm_enabled()
void on_shape_changed();
void on_add_sketch();
std::string feature_name(const wxString& kind) const;
void on_add_extrude();
void on_add_dressup();
void on_add_hole();
void on_add_thread();
void apply_thread_standard(); // fill pitch/depth/radius from m_thread_std selection
void infer_thread_spec(double diameter); // nearest M-standard from a picked cylinder diameter
void infer_thread_spec(double diameter, bool internal); // nearest standard from a picked cylinder
void on_add_revolve();
void on_add_sweep();
void on_add_loft();
@@ -199,6 +219,11 @@ private:
// imported_regions (no solver entities). on_add_text/on_import_svg gather
// input; add_imported_sketch builds the feature, refreshes tree + display.
void on_add_text();
// Text is its own feature ("Text N"), drawn in the canvas while its (modeless) dialog is
// open. feat < 0 starts a new one; otherwise the text feature `feat` is reopened for editing.
void open_text_dialog(int feat);
void text_dialog_changed();
void text_dialog_done(bool accepted);
void on_import_svg();
void on_import_step(); // STEP -> editable B-rep body (keeps the OCCT solid, not a mesh)
void on_import_mesh(); // STL/OBJ -> B-rep body via GeometryEngine::mesh_to_brep
@@ -215,6 +240,11 @@ private:
// a modal dialog editing the feature's placement transform in place.
void on_transform_imported(int feat_idx);
void on_commit();
// What Commit to Plate sends: one object with a part per body (an assembly, which keeps the
// bodies' relative placement), or, "as bodies", one Prepare object per body. The split
// button's dropdown switches it, like Prepare's Slice button, without committing.
enum class CommitMode { Assembly, Bodies };
void set_commit_mode(CommitMode mode);
void on_export_step(); // write all bodies to a .step file (native B-rep)
// Rehydrate the parametric model from a project's saved recipe (3MF
// Metadata/orca_cad.bin): deserialize -> recompute -> refresh viewport + tree.
@@ -271,6 +301,9 @@ private:
void after_edit_op(); // shared edit-op refresh tail
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)
@@ -284,6 +317,14 @@ private:
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) from the viewport. With a tool/dialog open it
// cancels that (Esc-like); otherwise it undoes/redoes the committed feature history.
void do_undo_redo(bool redo);
public:
// Edit > Undo / Redo while this tab is shown: the same route and the same gate as the keys
// and the toolbar buttons.
void menu_undo_redo(bool redo) { do_undo_redo(redo); }
bool menu_can_undo_redo(bool redo) const;
// Grey the app's Undo/Redo to this tab's history and gate (shown tab only).
void update_undo_redo_buttons();
private:
// The plane the Hole tool drills on: a picked face (inward, centred) or the dropdown.
SketchPlane hole_plane() const;
// The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown.
@@ -323,6 +364,7 @@ private:
void update_extrude_gizmo();
void update_fillet_gizmo(); // edge-anchored radius arrow (Dressup card)
void sync_dressup_target(); // Dressup card: show picked edge vs group, gate the combo
std::vector<int> dressup_edges() const; // the picked edge(s) a dress-up targets; empty = face group
void update_hole_gizmo(); // footprint circle + diameter/depth arrows (Hole card)
// A FEATURE button whose tool needs bodies it may not have yet. Greyed with an explanatory
// tooltip below min_bodies, rather than accepting the click and refusing afterwards.
@@ -366,7 +408,10 @@ private:
void show_move_card(bool show);
void apply_move_card(); // numeric move/rotate -> same xform the gizmo builds
void push_polygon_params();
wxSizer* m_tb_commit{nullptr}; // far-right Commit to Plate, beside Confirm/Cancel
wxSizer* m_tb_commit{nullptr}; // far-right Commit to Plate split button, beside Confirm/Cancel
CommitMode m_commit_mode{CommitMode::Assembly};
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)
@@ -403,6 +448,15 @@ private:
wxSizer* m_box_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art
wxSizer* m_box_expr{nullptr}; // expression binding card (visible during edit only)
int m_insert_feat{-1}; // provisional imported-art feature awaiting Confirm
// The open Text dialog, the feature it draws into (-1 until there is text to draw) and,
// for a new text, where it goes: plane, offset in that plane, and the body of the face it
// sits on (-1 = not on a face).
DesignTextDialog* m_text_dlg{nullptr};
int m_text_feat{-1};
bool m_text_editing{false};
SketchPlane m_text_plane;
Vec2d m_text_offset{0, 0};
int m_text_face_body{-1};
// Move-body gizmo runs through the unified action bar too: Confirm keeps the placement,
// Cancel reverts to the pose captured when the move started.
int m_move_body{-1};
@@ -459,6 +513,18 @@ private:
wxScrolledWindow* m_form{nullptr};
DesignCanvas* m_viewport{nullptr};
// Below the toolbar, m_form docks beside the viewport column like Prepare's sidebar: it can
// move to the other side, float, be resized and be collapsed, and the layout is kept between
// sessions.
AuiMgr m_aui;
wxString m_default_layout;
bool m_sidebar_collapsed{false};
bool m_laid_out{false}; // the tab has been shown, so the panel has its size
void load_window_layout();
void load_default_layout();
void collapse_sidebar(bool collapse);
void update_sidebar_pane(bool force_update = false);
// Top contextual toolbar (parented to the panel, above the form/viewport row).
UiMode m_ui_mode{UiMode::Feature};
// Sketch environment banner: a strip across the top of the viewport saying, in words, that
@@ -477,19 +543,16 @@ private:
// Unified Confirm/Cancel action bar (right end of the ribbon). Shown whenever any
// tool or mode is active; the single confirm/cancel surface for the whole tab.
wxSizer* m_tb_action{nullptr};
// Persistent Undo/Redo group at the left of the ribbon — always visible, independent
// of the mode-gated tool groups. The buttons are greyed per the document history and
// the do_undo_redo gate (see update_undo_redo_buttons).
wxSizer* m_tb_history{nullptr};
ScalableButton* m_btn_undo{nullptr};
ScalableButton* m_btn_redo{nullptr};
void update_undo_redo_buttons(); // enable/disable Undo/Redo from can_undo/can_redo + gate
// All tool buttons, for the active-tool teal highlight (Onshape-style).
std::vector<ScalableButton*> m_tool_btns;
ScalableButton* m_active_tool_btn{nullptr};
void set_active_tool_btn(ScalableButton* b); // nullptr clears the highlight
// Owns the themed DropDown flyouts (and the item vectors they hold by ref).
std::vector<std::shared_ptr<void>> m_flyout_keepalive;
// Icons that are not a plain ScalableButton face (flyout rows, card headers, theme-twinned
// buttons): each re-creates its bitmaps for the current scale and theme.
std::vector<std::function<void()>> m_icon_refresh;
void refresh_icons();
wxCheckBox* m_construction{nullptr}; // sketch-mode construction toggle
wxSpinCtrlDouble* m_move_dx{nullptr}; // Move/Rotate card: world translation
wxSpinCtrlDouble* m_move_dy{nullptr};
@@ -523,11 +586,13 @@ private:
wxStaticText* m_extrude_sketch_label{nullptr};
int m_extrude_sketch_ref{-1};
int m_extrude_auto_body{-1}; // body the profile touches: the inferred Join target
// Revolve controls (sweep a sketch profile about an in-plane axis).
wxStaticText* m_revolve_sketch_label{nullptr};
wxSpinCtrlDouble* m_revolve_angle{nullptr};
ComboBox* m_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
ComboBox* m_revolve_axis{nullptr}; // Plane X, Plane Y, then the sketch's lines
std::vector<int> m_revolve_axis_ents; // entity index of each line entry, in order
ComboBox* m_revolve_mode{nullptr}; // New/Add/Cut/Intersect
CheckBox* m_revolve_flip{nullptr};
int m_revolve_sketch_ref{-1};
@@ -554,7 +619,14 @@ private:
// Surface Revolve controls (sheet from sketch about axis).
wxStaticText* m_surf_revolve_sketch_label{nullptr};
wxSpinCtrlDouble* m_surf_revolve_angle{nullptr};
ComboBox* m_surf_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
ComboBox* m_surf_revolve_axis{nullptr}; // as m_revolve_axis
std::vector<int> m_surf_revolve_axis_ents;
// Fill a revolve axis combo for the profile sketch `sketch_ref`: "Plane X", "Plane Y", then
// one entry per Line of the sketch (entity index in `ents`), and select axis/entity. A fresh
// revolve passes entity = -2: the sketch's only centerline when it has exactly one, else X.
void fill_revolve_axes(ComboBox* combo, std::vector<int>& ents, int sketch_ref, int axis, int entity);
// The combo's selection as CadFeature::revolve_axis + revolve_axis_entity.
static void read_revolve_axis(ComboBox* combo, const std::vector<int>& ents, int& axis, int& entity);
CheckBox* m_surf_revolve_flip{nullptr};
int m_surf_revolve_sketch_ref{-1};
@@ -614,7 +686,7 @@ private:
// Delete Face controls (remove faces, heal the solid).
ComboBox* m_del_face_body{nullptr}; // target body
wxButton* m_del_face_add_btn{nullptr}; // "Add picked face" button
::Button* m_del_face_add_btn{nullptr}; // "Add picked face" button
wxStaticText* m_del_face_list{nullptr}; // shows the accumulated face ids
std::vector<int> m_del_faces; // accumulated face list
@@ -639,8 +711,8 @@ private:
// Expression binding (per-feature, visible during edit only)
ComboBox* m_expr_field{nullptr}; // field-name picker (editable)
wxTextCtrl* m_expr_text{nullptr}; // expression string
wxButton* m_expr_set_btn{nullptr}; // Apply / bind
wxButton* m_expr_clear_btn{nullptr}; // Remove binding
::Button* m_expr_set_btn{nullptr}; // Apply / bind
::Button* m_expr_clear_btn{nullptr}; // Remove binding
wxStaticText* m_expr_status{nullptr}; // shows current bindings for the edited feature
void populate_expr_fields(Tool t); // fill m_expr_field from feature-type fields
void on_set_expr(); // checkpoint + write -> recompute -> undo on fail
@@ -649,16 +721,16 @@ private:
// Document variables panel (below the feature tree / parts)
StaticBox* m_var_box{nullptr};
wxListCtrl* m_var_list{nullptr};
wxButton* m_btn_add_var{nullptr};
wxButton* m_btn_edit_var{nullptr};
wxButton* m_btn_del_var{nullptr};
::Button* m_btn_add_var{nullptr};
::Button* m_btn_edit_var{nullptr};
::Button* m_btn_del_var{nullptr};
void refresh_variables(); // rebuild m_var_list from m_doc.variables
void on_add_variable();
void on_edit_variable();
void on_remove_variable();
// Feature-tree button
ScalableButton* m_btn_interfere{nullptr};
::Button* m_btn_interfere{nullptr};
// Pattern controls (replicate the target body: linear or circular).
ComboBox* m_pattern_type{nullptr}; // 0 = Linear, 1 = Circular
@@ -689,10 +761,10 @@ private:
ComboBox* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y
// Plane construction method + contextual face/edge reference picks (Onshape/Fusion parity).
ComboBox* m_plane_type{nullptr}; // PlaneType: Offset/Angle/Midplane/Tangent/TwoEdges/Coincident
wxButton* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
wxButton* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
wxButton* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
wxButton* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
::Button* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
::Button* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
::Button* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
::Button* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
wxSpinCtrlDouble* m_plane_usize{nullptr}; // datum rectangle extent u (mm) — also driven by drag handles
wxSpinCtrlDouble* m_plane_vsize{nullptr}; // datum rectangle extent v (mm)
// Captured references for the candidate datum (body index + face/edge index, -1 = none).
@@ -701,14 +773,14 @@ private:
int m_pl_edgeA_body{-1}, m_pl_edgeA{-1};
int m_pl_edgeB_body{-1}, m_pl_edgeB{-1};
PlanePick m_plane_pick{PlanePick::None}; // which ref the next solid pick fills
// Plate loop selection (click a committed sketch loop): the Sketch feature + the
// clicked closed-region index, so Extrude builds just that one loop. -1 = none.
int m_sel_sketch_feat{-1};
int m_sel_sketch_region{-1};
// Click-selected solid topology (whole/face/edge cycle): face id for up-to-face / dress-up.
int m_sel_solid_body{-1}; // which body the face/edge selection is on
int m_sel_solid_face{-1};
int m_sel_solid_edge{-1};
// The picked edge set (Shift/Ctrl+click), m_sel_solid_edge last. Read through dressup_edges(),
// which drops it once m_sel_solid_edge moves on — the many places that reset the single edge
// then need not know the set exists.
std::vector<int> m_sel_solid_edges;
bool m_sel_solid_vertex{false}; // a corner is picked (body+point, no face/edge)
// The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level.
// The first click on a solid selects the WHOLE body, but the ray has already resolved which face
@@ -727,8 +799,13 @@ private:
std::map<std::string, std::function<void()>> m_verb_actions;
// Append an offer row with its toolbar glyph. The bitmap must be set BEFORE Append —
// wxGTK builds the GtkMenuItem there and only makes an image item if one is present.
// Every status write goes through here so long hints wrap instead of clipping.
void set_status(const wxString& text);
// Every status write goes through here so long hints wrap instead of clipping. The kind
// sets the colour AND a leading glyph, so a message's meaning never rests on colour alone
// (charter 6.2), and every message sets its own kind instead of inheriting the last one's.
enum class StatusKind { Info, Ok, Warning, Error };
void set_status(StatusKind kind, const wxString& text);
void set_status(const wxString& text) { set_status(StatusKind::Info, text); }
static wxString kernel_error_text(const std::string& err); // a kernel message, in words a modeller reads
wxString idle_hint() const; // what to say when nothing is selected
// Reason detect_mate_conflicts() recorded for a feature, or nullptr. Marks the tree row and
// feeds the status line; a conflict is a diagnostic, not a document error.
@@ -743,6 +820,7 @@ private:
// monitor, and the menu would map there — detached from the geometry it is about.
wxPoint offer_anchor() const;
int offer_selection_kind() const; // an OfferSel, as int to keep the header light
wxString offer_header(int kind) const; // the offer menu's title line for that selection
// Does the SKETCH half of the map apply? A mode question, not a session one: begin_sketch
// does not run until the first tool is armed, so between "press Sketch" and "pick a tool"
// is_sketching() is still false — precisely when the drawing tools must be on offer. The
@@ -805,8 +883,8 @@ private:
// Axis controls (datum axis: line through two points or derived from geometry).
ComboBox* m_axis_type{nullptr}; // AxisType: TwoPoints/FaceNormal/CylinderCenterline/PlaneIntersection/AlongEdge
wxButton* m_axis_pick_face{nullptr}; wxStaticText* m_axis_face_lbl{nullptr};
wxButton* m_axis_pick_edge{nullptr}; wxStaticText* m_axis_edge_lbl{nullptr};
::Button* m_axis_pick_face{nullptr}; wxStaticText* m_axis_face_lbl{nullptr};
::Button* m_axis_pick_edge{nullptr}; wxStaticText* m_axis_edge_lbl{nullptr};
ComboBox* m_axis_plane_a{nullptr};
ComboBox* m_axis_plane_b{nullptr};
wxSpinCtrlDouble* m_axis_p1x{nullptr}; wxSpinCtrlDouble* m_axis_p1y{nullptr}; wxSpinCtrlDouble* m_axis_p1z{nullptr};
@@ -819,8 +897,8 @@ private:
ComboBox* m_coordsys_type{nullptr}; // CoordSysType: PointWorld/FaceAndDirection
ComboBox* m_cs_body{nullptr}; // body-focus chooser: restrict picking to one body
wxSpinCtrlDouble* m_cs_x{nullptr}; wxSpinCtrlDouble* m_cs_y{nullptr}; wxSpinCtrlDouble* m_cs_z{nullptr};
wxButton* m_cs_pick_face{nullptr}; wxStaticText* m_cs_face_lbl{nullptr};
wxButton* m_cs_pick_edge{nullptr}; wxStaticText* m_cs_edge_lbl{nullptr};
::Button* m_cs_pick_face{nullptr}; wxStaticText* m_cs_face_lbl{nullptr};
::Button* m_cs_pick_edge{nullptr}; wxStaticText* m_cs_edge_lbl{nullptr};
wxSpinCtrlDouble* m_cs_hx{nullptr}; wxSpinCtrlDouble* m_cs_hy{nullptr}; wxSpinCtrlDouble* m_cs_hz{nullptr};
int m_cs_face_body{-1}, m_cs_face{-1};
int m_cs_edge_body{-1}, m_cs_edge{-1};
@@ -835,21 +913,29 @@ 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: a wxTreeCtrl with per-feature-type icons. Callers keep using
// integer row indices via tree_selection()/set_tree_selection(); m_tree_items
// maps feature order -> tree node, rebuilt by refresh_tree().
wxTreeCtrl* m_tree{nullptr};
wxTreeCtrl* m_parts{nullptr}; // Bodies list under the feature tree
// 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.
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();
bool deselect_rows(); // Esc / a click on nothing: drop the tree and Bodies rows
// 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};
wxStaticText* m_parts_label{nullptr}; // its "Bodies" caption (hidden when empty)
wxBoxSizer* m_parts_hdr{nullptr}; // Bodies card header (icon + title)
wxBoxSizer* m_parts_hdr{nullptr}; // Bodies card header (icon + title + body actions)
wxStaticLine* m_parts_rule{nullptr}; // rule under that header
wxBoxSizer* m_hdr_tree_row{nullptr}; // Feature tree header: title + row actions
wxBoxSizer* m_hdr_tree_row{nullptr}; // Feature tree header: title + list actions
wxStaticText* m_hdr_tree{nullptr}; // its title label
wxImageList* m_tree_images{nullptr};
std::vector<wxTreeItemId> m_tree_items;
// Parts list: tree rows for each body (parallel to m_doc.bodies). Selecting one
// highlights that body and makes it the target for the next op.
std::vector<wxTreeItemId> m_tree_body_items;
// Section views (non-destructive): named "Section View N" entries listed in the tree, each a
// horizontal clip height. View-only — NOT bodies/features, never serialized. Key X adds one;
@@ -884,15 +970,13 @@ private:
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1
void refresh_parts(); // rebuild the Bodies list under the feature tree
void sync_sidebar_width(); // keep the panel as wide as Prepare's sidebar
void set_tree_selection(int row);
static int tree_icon_for(CadFeatureType t);
static const char* tree_icon_for(CadFeatureType t);
wxStaticText* m_status{nullptr};
// m_status's foreground as created, captured before any caller touches it. Callers signal
// "no opinion" by setting wxNullColour, which restores exactly this — so it is the only
// reliable way to tell a chosen colour (the error red) from the default. See set_status().
wxColour m_status_default_fg;
// The guidance sentence for the step the armed sketch tool is on, kept so a transient
// readout (the live length/angle while a segment is being dragged) can be appended to it
// instead of replacing it — the guidance used to vanish on the first mouse move after a
@@ -908,7 +992,10 @@ private:
bool m_dof_last_has{false};
int m_feature_counter{0};
std::vector<wxButton*> m_confirm_btns;
std::vector<::Button*> m_confirm_btns;
// refresh_preview's verdict on the open card's candidate (open_tool resets it). Kept here, not
// read back from the ✓, which every card and mode shares; the ✓ is greyed from confirm_enabled().
bool m_candidate_ok{true};
// Edit-in-place state: add-mode is m_edit_index == -1. Single-feature edit
// (Sketch or Extrude independently) uses only m_edit_index as the row to replace.
+387
View File
@@ -0,0 +1,387 @@
#include "slic3r/GUI/CAD/DesignRowList.hpp"
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include <wx/control.h>
#include <wx/dc.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
#include <wx/graphics.h>
#include <wx/scrolwin.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/types.h>
#include <wx/vlbox.h>
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r { namespace GUI {
// Row and icon cell geometry, in DIP.
static constexpr int kIconPx = 16; // type and action icons
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)
, m_max_visible(std::max(max_visible, 1))
{
Bind(wxEVT_LISTBOX, [this](wxCommandEvent&) { if (on_select) on_select(); });
Bind(wxEVT_LISTBOX_DCLICK, [this](wxCommandEvent&) { if (on_activate) on_activate(); });
Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
set_hover(hit_test(e.GetPosition()));
e.Skip();
});
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) {
set_hover(Hit{});
m_pressed = Hit{};
e.Skip();
});
// A press on an action cell is remembered and skipped, so the list still selects the row.
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
m_pressed = h.cell >= 0 ? h : Hit{};
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; }
m_pressed = h;
});
Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
e.Skip();
const Hit pressed = m_pressed;
m_pressed = Hit{};
if (pressed.cell < 0 || hit_test(e.GetPosition()) != pressed)
return;
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.
CallAfter([this, row = pressed.row, id] {
if (row < int(GetItemCount()) && row == GetSelection() && on_action)
on_action(row, id);
});
});
// Right-click targets the row under the pointer. Skipped, because on MSW a handled right
// 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);
e.Skip();
});
Bind(wxEVT_CONTEXT_MENU, [this](wxContextMenuEvent& e) {
wxPoint screen = e.GetPosition(); // screen coordinates; wxDefaultPosition from the keyboard
int row = GetSelection();
if (screen == wxDefaultPosition) {
if (row == wxNOT_FOUND) return;
screen = ClientToScreen(GetItemRect(row).GetBottomLeft());
} else {
row = VirtualHitTest(ScreenToClient(screen).y);
}
if (row == wxNOT_FOUND || row >= int(m_rows.size()))
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 (on_menu) on_menu(row, screen);
});
// wxVListBox takes every wheel event, even with nothing to scroll, so a short list would stop
// the sidebar it sits in from scrolling under the pointer. Hand it on instead. Not on GTK,
// where the sidebar scrolls natively and passes on an unhandled wheel by itself.
Bind(wxEVT_MOUSEWHEEL, [this](wxMouseEvent& e) {
end_rename(true);
#ifndef __WXGTK__
if (GetVisibleRowsBegin() == 0 && GetVisibleRowsEnd() >= GetItemCount()) {
for (wxWindow* w = GetParent(); w != nullptr; w = w->GetParent())
if (auto* sw = dynamic_cast<wxScrolledWindow*>(w)) {
wxMouseEvent fwd(e);
fwd.SetEventObject(sw);
sw->GetEventHandler()->ProcessEvent(fwd);
return;
}
}
#endif
e.Skip();
});
Bind(wxEVT_SIZE, [this](wxSizeEvent& e) {
end_rename(true); // the editor sits on a row position the new size may have moved
e.Skip();
});
measure_row();
fit_rows();
}
// Every row is one height. Measured once per font and scale rather than on each OnMeasureItem,
// which wxVListBox calls for every row it walks on every paint and hit test.
void DesignRowList::measure_row()
{
m_row_h = std::max(GetCharHeight() + 8, FromDIP(kCellDip + 2));
}
void DesignRowList::set_rows(std::vector<Row> rows)
{
end_rename(false); // the row it names may no longer be that row
SetSelection(wxNOT_FOUND);
m_rows = std::move(rows);
m_hover = Hit{};
m_pressed = Hit{};
UnsetToolTip();
load_icons();
SetItemCount(m_rows.size());
fit_rows();
Refresh();
}
void DesignRowList::select(int row)
{
if (row < 0 || row >= int(GetItemCount()))
row = wxNOT_FOUND;
if (row == GetSelection())
return;
SetSelection(row);
if (on_select) on_select();
}
void DesignRowList::fit_rows()
{
const int shown = std::clamp(int(GetItemCount()), 1, m_max_visible);
// Plus the frame, so the last row is not clipped by it.
const int h = shown * m_row_h + (GetSize().y - GetClientSize().y);
SetMinSize(wxSize(-1, h));
SetMaxSize(wxSize(-1, h));
}
void DesignRowList::Rescale()
{
m_icons.clear();
load_icons();
measure_row();
// The scrollbar's total is only recomputed when the item count is set; RefreshAll() alone
// would leave it at the old scale.
SetItemCount(GetItemCount());
fit_rows();
Refresh();
}
void DesignRowList::begin_rename(int row)
{
CallAfter([this, row] { open_editor(row); });
}
void DesignRowList::open_editor(int row)
{
if (row < 0 || row >= int(m_rows.size()))
return;
end_rename(false);
if (m_editor == nullptr) {
// A raw wxTextCtrl, as wxTreeCtrl's own label editor is: it is a transient overlay on one
// row, not a form field, and ::TextInput's frame would not fit inside the row.
m_editor = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
wxTE_PROCESS_ENTER | wxBORDER_SIMPLE);
m_editor->Hide();
m_editor->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) {
end_rename(true);
SetFocus();
});
// Esc must stop here. CHAR_HOOK reaches the focused editor before its ancestors, and the
// Design panel's own hook answers Esc whatever holds focus, so a skipped Esc would close
// the open tool as well as the editor.
m_editor->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) {
if (e.GetKeyCode() != WXK_ESCAPE) { e.Skip(); return; }
end_rename(false);
SetFocus();
});
m_editor->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) {
e.Skip();
end_rename(true);
});
}
if (!IsRowVisible(row))
ScrollToRow(row);
const Row& r = m_rows[row];
const wxRect rc = label_rect(GetItemRect(row), r);
m_editor->SetFont(GetFont());
m_editor->SetBackgroundColour(GetBackgroundColour());
m_editor->SetForegroundColour(r.colour.IsOk() ? r.colour : GetForegroundColour());
m_editor->SetValue(r.edit_text.empty() ? r.label : r.edit_text);
const int h = std::max(rc.height, m_editor->GetBestSize().y);
const int y = std::clamp(rc.y + (rc.height - h) / 2, 0, std::max(0, GetClientSize().y - h));
m_editor->SetSize(rc.x - FromDIP(2), y, rc.width + FromDIP(2), h);
m_edit_row = row;
m_editor->Show();
m_editor->SetFocus();
m_editor->SelectAll();
}
void DesignRowList::end_rename(bool commit)
{
if (m_edit_row == wxNOT_FOUND)
return;
// Cleared first: hiding the focused editor moves the focus, whose kill-focus comes back here.
const int row = m_edit_row;
wxString text = m_editor->GetValue();
m_edit_row = wxNOT_FOUND;
m_editor->Hide();
text.Trim(true).Trim(false);
if (commit && on_rename && !text.empty()) // a nameless row is worse than a badly named one
CallAfter([this, row, text] {
if (on_rename && row < int(GetItemCount()))
on_rename(row, text);
});
}
wxCoord DesignRowList::OnMeasureItem(size_t) const
{
return m_row_h;
}
void DesignRowList::OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) const
{
// ObjectList's selection teal, and the dropdowns' lighter hover teal; both are dark-mapped.
wxColour bg;
if (IsSelected(n))
bg = StateColor::darkModeColorFor(wxColour("#BFE1DE"));
else if (int(n) == m_hover.row)
bg = StateColor::darkModeColorFor(wxColour("#E5F0EE"));
if (!bg.IsOk())
return; // the list already cleared to its background colour
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(bg));
dc.DrawRectangle(rect);
}
void DesignRowList::OnDrawItem(wxDC& dc, const wxRect& rect, size_t n) const
{
if (n >= m_rows.size())
return;
const Row& r = m_rows[n];
if (const ScalableBitmap* bmp = icon(r.icon)) {
const wxSize sz = bmp->GetBmpSize();
dc.DrawBitmap(bmp->bmp(), rect.x + FromDIP(kPadDip), rect.y + (rect.height - sz.y) / 2, true);
}
const wxRect lr = label_rect(rect, r);
dc.SetFont(GetFont());
dc.SetTextForeground(r.colour.IsOk() ? r.colour : GetForegroundColour());
const wxString text = wxControl::Ellipsize(r.label, dc, wxELLIPSIZE_END, std::max(lr.width, 0));
dc.DrawText(text, lr.x, lr.y + (lr.height - dc.GetCharHeight()) / 2);
for (size_t i = 0; i < r.actions.size(); ++i) {
const wxRect cell = cell_rect(rect, r.actions.size(), i);
if (m_hover.row == int(n) && m_hover.cell == int(i)) {
// Drawn through a graphics context so the rounded corners are anti-aliased on MSW too,
// and released before the icon goes on top.
const wxColour chip = StateColor::darkModeColorFor(wxColour(hover_chip));
const double rad = FromDIP(4);
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::CreateFromUnknownDC(dc));
if (gc) {
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(wxBrush(chip));
gc->DrawRoundedRectangle(cell.x, cell.y, cell.width, cell.height, rad);
} else {
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(chip));
dc.DrawRoundedRectangle(cell, rad);
}
}
if (const ScalableBitmap* bmp = icon(r.actions[i].icon)) {
const wxSize sz = bmp->GetBmpSize();
dc.DrawBitmap(bmp->bmp(), cell.x + (cell.width - sz.x) / 2, cell.y + (cell.height - sz.y) / 2, true);
}
}
}
DesignRowList::Hit DesignRowList::hit_test(const wxPoint& pt) const
{
Hit h;
h.row = VirtualHitTest(pt.y);
if (h.row == wxNOT_FOUND || h.row >= int(m_rows.size()))
return Hit{};
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)) {
h.cell = int(i);
break;
}
return h;
}
wxRect DesignRowList::cell_rect(const wxRect& row, size_t count, size_t i) const
{
const int side = FromDIP(kCellDip);
const int x = row.GetRight() + 1 - FromDIP(kPadDip) - int(count - i) * side;
return wxRect(x, row.y + (row.height - side) / 2, side, side);
}
wxRect DesignRowList::label_rect(const wxRect& row, const Row& r) const
{
int left = row.x + FromDIP(kPadDip);
if (const ScalableBitmap* bmp = icon(r.icon))
left += bmp->GetBmpSize().x + FromDIP(kGapDip);
const int right = r.actions.empty() ? row.GetRight() + 1 - FromDIP(kPadDip)
: cell_rect(row, r.actions.size(), 0).x - FromDIP(kPadDip);
return wxRect(left, row.y, std::max(right - left, 0), row.height);
}
// nullptr for no icon: load_icons() never stores an empty name.
const ScalableBitmap* DesignRowList::icon(const std::string& name) const
{
const auto it = m_icons.find(name);
return it == m_icons.end() ? nullptr : &it->second;
}
void DesignRowList::load_icons()
{
auto load = [this](const std::string& name) {
if (!name.empty() && m_icons.find(name) == m_icons.end())
m_icons.emplace(name, ScalableBitmap(this, name, kIconPx));
};
for (const Row& r : m_rows) {
load(r.icon);
for (const Action& a : r.actions)
load(a.icon);
}
}
void DesignRowList::set_hover(const Hit& h)
{
if (h == m_hover)
return;
const int old = m_hover.row;
m_hover = h;
if (old != wxNOT_FOUND && old < int(GetItemCount()))
RefreshRow(old);
if (h.row != wxNOT_FOUND && h.row != old)
RefreshRow(h.row);
// The tip names the icon under the pointer, or the whole label where the row cuts it short.
wxString tip;
if (h.row != wxNOT_FOUND) {
const Row& r = m_rows[h.row];
if (h.cell >= 0)
tip = r.actions[h.cell].tip;
else if (GetTextExtent(r.label).x > label_rect(GetItemRect(h.row), r).width)
tip = r.label;
}
if (tip.empty())
UnsetToolTip();
else if (tip != GetToolTipText())
SetToolTip(tip);
}
}} // namespace Slic3r::GUI
+115
View File
@@ -0,0 +1,115 @@
#ifndef slic3r_DesignRowList_hpp_
#define slic3r_DesignRowList_hpp_
#include <wx/vlbox.h>
#include <wx/colour.h>
#include <wx/dc.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
#include <wx/string.h>
#include <wx/types.h>
#include <cstddef>
#include <functional>
#include <map>
#include <string>
#include <vector>
#include "slic3r/GUI/wxExtensions.hpp" // ScalableBitmap
class wxTextCtrl;
namespace Slic3r { namespace GUI {
// The Design tab's Feature tree and Bodies lists: a single-selection list whose rows carry their
// own action icons. A wxTreeCtrl cannot host a clickable icon per row (it is native on MSW), so
// this list draws its own: each row draws its actions at its right end, highlights the one under
// the pointer, and takes its icons from the owner, so the eye can show whether the row is hidden.
//
// 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.
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"
wxString tip;
};
struct Row {
std::string icon; // type icon at the left; empty for none
wxString label;
wxString edit_text; // what the rename editor opens with; empty for the label
wxColour colour; // label colour
std::vector<Action> actions; // icon cells at the right end, in display order
};
// The hover chip behind an action icon, a light colour dark-mapped at paint. The sidebar's
// icon buttons hover in it too.
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);
// Replace every row. Clears the selection without notifying and cancels a rename in progress;
// the owner re-selects the row it keeps, which notifies.
void set_rows(std::vector<Row> rows);
int selection() const { return GetSelection(); }
// Select `row` (wxNOT_FOUND clears) and call on_select if that changed the selection.
void select(int row);
void unselect() { select(wxNOT_FOUND); }
// Open the in-place editor on a row, once the current event has finished: opened from inside
// a popup menu's nested loop it would never appear. Enter or clicking away commits, Esc cancels.
void begin_rename(int row);
// Re-rasterise the icons and re-measure the rows after a DPI or theme change.
void Rescale();
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.
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.
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;
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;
wxCoord OnMeasureItem(size_t n) const override;
private:
struct Hit {
int row{wxNOT_FOUND};
int cell{-1}; // index into the row's actions, -1 for the rest of the row
bool operator==(const Hit& o) const { return row == o.row && cell == o.cell; }
bool operator!=(const Hit& o) const { return !(*this == o); }
};
Hit hit_test(const wxPoint& pt) const;
wxRect cell_rect(const wxRect& row, size_t count, size_t i) const;
wxRect label_rect(const wxRect& row, const Row& r) const;
const ScalableBitmap* icon(const std::string& name) const;
void load_icons();
void set_hover(const Hit& h);
void open_editor(int row);
void end_rename(bool commit);
void measure_row();
void fit_rows();
std::vector<Row> m_rows;
std::map<std::string, ScalableBitmap> m_icons;
int m_max_visible;
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
wxTextCtrl* m_editor{nullptr}; // created on the first rename, then reused
int m_edit_row{wxNOT_FOUND};
};
}} // namespace Slic3r::GUI
#endif // slic3r_DesignRowList_hpp_
File diff suppressed because it is too large Load Diff
+151 -43
View File
@@ -8,6 +8,7 @@
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies
#include <Eigen/Core>
#include <cstddef>
#include <functional>
#include "libslic3r/Color.hpp"
#include <math.h>
@@ -69,6 +70,17 @@ public:
// row off arms a NEIGHBOURING tool and then grades whatever that drew. ekt9.
Mode mode() const { return m_mode; }
int pending_points() const { return int(m_points.size()); }
// Picks the armed tool is holding that are not yet an entity or a constraint: the Dimension
// tool's first anchor, an edit-op's or a transform's subjects, Constrain's picks. Together
// with pending_points() they are CadLevel::Gesture, so Esc drops them and keeps the tool.
bool has_pending_picks() const {
return m_dim_has0 || m_op_a >= 0 || !m_tf_targets.empty() || m_pick0 >= 0 || m_sel_a >= 0;
}
bool gesture_pending() const { return !m_points.empty() || has_pending_picks(); }
// Enter while an edit-op or a transform is ready: apply it, the same as its value field's
// Enter. False when nothing is pending, so the key can fall through to the tab's ✓.
bool confirm_pending();
bool end_chain(); // Polyline / Spline: finish the open chain as drawn
void emit_step_hint(); // fires on_step_changed when the step actually moved
// Is an in-canvas value field open? While one is, the canvas is frozen and every letter is
// swallowed — the single most common reason a driven gesture "does nothing".
@@ -145,6 +157,8 @@ public:
// 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.
class SketchInlineEditor* inline_editor{nullptr};
// The canvas's own overlays (its status and readout chips), drawn in this tool's ImGui pass.
std::function<void()> render_overlays;
// Persistent committed sketches to draw even when no session is active (e.g. an
// un-consumed sketch left visible after its extrude is removed). Each carries its
@@ -189,11 +203,24 @@ public:
// another solid is reachable without hiding anything. -1 = no restriction.
// Survives set_solid_pick() — it is owned by the panel, not by the mesh feed.
void set_pick_only_body(int b) { m_pick_only_body = b; }
// Off while the bodies themselves are hidden (a dress-up previewing its result alone), so
// their edges do not float over the preview.
void set_body_edges_hidden(bool h) { m_body_edges_hidden = h; }
void clear_solid_selection();
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.
// Empty unless the selection is at edge level.
std::vector<int> selected_edges() const;
// 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
@@ -205,12 +232,12 @@ public:
void clear_move_gizmo();
bool moving_body() const { return m_mv_active; }
int move_body_index() const { return m_mv_body; }
// F key forwarded from the canvas (Prepare's Place on Face): returns true if it acted.
bool request_place_on_face() { return on_place_on_face ? on_place_on_face() : false; }
std::function<bool()> on_place_on_face;
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;
// A click or rubber band (no live session) that took nothing, so the host can drop the
// selections the tool does not hold, such as the panel's list rows.
std::function<void()> on_empty_pick;
// Click a committed sketch overlay (no live session) -> select that loop: the Sketch
// feature index + the clicked closed-region index within it (-1 = no specific loop).
// entity = the sketch entity index under the cursor when the click landed on a loop
@@ -236,6 +263,8 @@ public:
// sketch there is nothing left that would set this — and selected_loop_entities(), which is
// what Extrude consumes, reads exactly these two fields.
void set_display_pick(int feature, int region) { m_display_pick = feature; m_display_pick_region = region; }
int display_pick() const { return m_display_pick; }
int display_pick_region() const { return m_display_pick_region; }
// Visual Extrude gizmo (C5b). The Extrude tool is a DesignPanel docked card, so the
// sketch tool is NOT active during it; the panel feeds the profile plane + a 2D centroid
@@ -361,12 +390,12 @@ public:
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> l) { m_mate_links = std::move(l); }
void clear_mate_connectors() { m_mate_connectors.clear(); m_mate_links.clear(); }
// Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + axis (0=plane X,
// 1=plane Y) + angle + flip while its Revolve card is open; an angle-arc is drawn in the
// revolve plane at the profile radius. Dragging the tip sweeps the angle, a stationary click
// Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + the world axis
// (a point on it and its direction) + angle + flip while its Revolve card is open; an
// angle-arc is drawn in the revolve plane at the profile radius, and the axis dashed. Dragging the tip sweeps the angle, a stationary click
// edits it; both fire on_revolve_angle_changed.
void set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
int axis_sel, double angle, bool flip);
const Vec3d& axis_origin, const Vec3d& axis_dir, double angle, bool flip);
void clear_revolve_gizmo();
bool revolving() const { return m_rv_active; }
std::function<void(double angle)> on_revolve_angle_changed;
@@ -513,6 +542,8 @@ public:
std::vector<int> holes; // indices into LoopReport::loops that this loop encloses
bool closed{false};
double area{0.0}; // signed shoelace area of the loop polyline
bool defect{false}; // crosses or folds back on itself (see RegionLoop)
Vec2d defect_at{0, 0};
};
struct LoopReport {
std::vector<LoopInfo> loops;
@@ -552,25 +583,15 @@ public:
reset_autoedit();
if (on_readout) on_readout(std::string()); // the HUD is not redrawn once the tool stops
}
// Ctrl+Z while sketching: drop the last drawn entity (reuses delete_selected's remap).
bool undo_last_entity() {
if (!m_active || m_entities.empty()) return false;
m_selection.assign(1, int(m_entities.size()) - 1);
delete_selected();
reset_autoedit();
return true;
}
// Delete while sketching: the selected entities, or the last drawn one if none is selected.
bool delete_selected_or_last() {
if (!m_active) return false;
if (m_selection.empty()) {
if (m_entities.empty()) return false;
m_selection.assign(1, int(m_entities.size()) - 1);
}
delete_selected();
reset_autoedit();
return true;
}
// Ctrl+Z while sketching: drop the last thing DRAWN — a whole rectangle, slot or polygon
// when the last entity belongs to one, since that was one gesture. Undoing one side of a
// rectangle left three lines and dissolved the shape.
bool undo_last_entity();
// Ctrl+Y / Ctrl+Shift+Z while sketching: bring back what undo_last_entity removed, as long
// as nothing was drawn or deleted since (then the old state is no longer "the next step").
bool redo_last_entity();
bool can_undo_entity() const { return m_active && !m_entities.empty(); }
bool can_redo_entity() const;
std::function<void(int count)> on_selection_changed;
// Dimension tool: infer a driving dimension from the current selection and set
@@ -614,6 +635,14 @@ public:
// characteristic dimensions). Empty string -> hide the HUD. The owner (DesignCanvas)
// shows it as a floating corner label over the GL canvas.
std::function<void(const std::string&)> on_readout;
// Why something did not happen, or what a gesture did as a side effect. NOT on_readout:
// that one is rewritten every frame from build_readout(), so a message sent through it was
// gone before anyone could read it. The host shows this on its persistent status line.
std::function<void(const std::string& msg, bool error)> on_notice;
void notify(const std::string& msg, bool error = true) { if (on_notice) on_notice(msg, error); }
// A typed value the tool cannot take: the open value field comes back with the reason, or,
// when no field is committing, the reason goes to the status line.
void show_refusal(const std::string& why);
// Driving dimension constraints accumulated during the session (the Dimension
// tool records a SketchEntityConstraintDef per applied dimension); committed
@@ -635,17 +664,11 @@ public:
// request_exit() declined to leave because the session holds geometry. The panel owns the
// status line, so the tool reports through this instead of writing text itself.
std::function<void()> on_exit_refused;
std::function<void()> on_move_exit; // right-click finished the move-body gizmo
// The two inner Esc levels, callable on their own so the panel can route one press to one
// level (see DesignInteraction.hpp). Each returns whether it had anything to unwind.
bool abort_gesture(); // CadLevel::Gesture — drop the entity being drawn
bool disarm_tool(); // CadLevel::Tool — armed draw/edit tool falls back to Select
void request_exit();
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) while the Design canvas is focused: undo/redo the
// committed feature history. The tool just forwards to the host, which owns the
// CadDocument (the tool has no document of its own). redo == true requests redo.
std::function<void(bool /*redo*/)> on_undo_redo;
void request_undo_redo(bool redo);
private:
bool screen_to_plane(GLCanvas3D& canvas, const wxMouseEvent& evt, Vec2d& out) const;
@@ -662,10 +685,17 @@ private:
// chains join across entities (a line + an arc can close into one loop). Shift
// disables it. `snapped` reports whether a vertex was hit.
Vec2d snap_vertex(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw, bool& snapped) const;
bool click_snaps() const; // does the NEXT click of the armed draw tool land on a snap target?
// --- P1 inference / auto-constraint engine ---------------------------------
// Plane-units tolerance equivalent to ~`px` screen pixels at the cursor.
double screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& at, double px = 8.0) const;
// The sketcher's pick budgets, in screen pixels, converted to plane units by screen_tol().
// One set for every tool, so the same thing is equally easy to hit whichever tool is armed.
static constexpr double kPickPx = 8.0; // grab a point, a handle or an edge; hover uses the same
static constexpr double kToolPickPx = 24.0; // pick an entity for a tool to act on (trim, edit-ops,
// transforms, constrain)
static constexpr double kLabelPx = 24.0; // click or double-click a value label
double screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& at, double px = kPickPx) const;
// Run kernel inference at the cursor, cache the target for the hint renderer.
InferenceSnap infer_at(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw) const;
// True if m_constraints already holds an equivalent Coincident between the two refs.
@@ -692,6 +722,8 @@ private:
bool selection_valid() const; // all selection indices in range
void record_dimension_constraint(double v); // append the driving def for the selection
void resolve_live(); // solve accumulated constraints on m_entities now
void announce_loop_defects(); // status line, once, when a loop starts crossing itself
bool m_loop_defect_shown{false};
// Drag-aware re-solve: pins the dragged point at its current coord and lets the
// solver move the rest (Slvs dragged[]). Used live while a point grab is active.
void resolve_live_drag(int dragged_ei, SketchPointRole dragged_role);
@@ -810,12 +842,19 @@ private:
// EllipseArc endpoint drag: Center translates; P0/P1 move the sweep start/end to the
// parametric angle of the cursor on the ellipse frame (radius/shape preserved).
void drag_ellipsearc_handle(int ei, SketchPointRole role, const Vec2d& target);
// The ONE way to remove constraints. m_dimensions[i].con caches a POSITIONAL index into
// m_constraints, so an erase that does not repair those links leaves a dimension pointing
// at whatever slid into the hole — and set_dimension_value then writes a def into that
// slot, silently overwriting an unrelated constraint. `drop(idx, def)` returns true to
// remove. Every removal in this file routes through here.
// Returns how many were removed.
int erase_constraints(const std::function<bool(int, const SketchEntityConstraintDef&)>& drop);
// Drop orientation constraints (H/V/Parallel/Perp/Angle/LockX/LockY) on entities in
// [begin,end). A ROTATION makes inferred per-edge H/V inconsistent, so re-solving
// against them collapses the shape — drop them first (fixes up DimAnnot.con indices).
// against them collapses the shape — drop them first.
void drop_orientation_constraints(int begin, int end);
// Drop every live constraint that references entity `ei` (Trim/Extend slide an endpoint,
// invalidating its constraints) and fix the dimensions' cached constraint indices.
// invalidating its constraints).
void drop_constraints_referencing(int ei);
// Standalone Trim/Extend scissors on the LIVE sketch: pick the entity nearest `p` (within
// `tol` plane units) and cut it back to / out to its nearest intersection with the others.
@@ -838,6 +877,9 @@ private:
// UPDATE it, not append a rival asking for something else. Both return the index.
int upsert_constraint(const SketchEntityConstraintDef& c);
int upsert_dimension(const DimAnnot& a);
// The same slot rule for the DRIVING constraint: a Distance and its zero case (recorded as
// a Coincident) are one dimension, so they replace each other instead of stacking.
int upsert_dimension_constraint(const SketchEntityConstraintDef& c);
int place_dimension(DimAnnot a); // create+drive+notify
std::string dim_text(const DimAnnot& a) const; // rendered label string
void render_dimensions(double unit_per_px); // quote lines + labels
@@ -946,6 +988,10 @@ private:
std::vector<Vec2d> poly;
std::vector<int> ents;
std::vector<int> holes; // indices into the same vector; one nesting level
// Closed, but crossing itself or turning straight back somewhere (sketch_loop_defect):
// it does not bound one region, whatever the chainer says. defect_at names the place.
bool defect{false};
Vec2d defect_at{0, 0};
};
std::vector<RegionLoop> region_loops(const std::vector<SketchEntity>& ents) const;
// Index of the closed region containing plane-point p (point-in-polygon), or -1.
@@ -990,6 +1036,12 @@ private:
};
std::vector<AutoEditStep> m_autoedit_dims; // queued steps to edit in sequence
int m_autoedit_dim_idx{-1}; // index into m_autoedit_dims (-1 = idle)
// Plane coords of the label the OPEN value field sits on. The field is anchored OVER its
// label (open_next_autoedit_dim) and the label is drawn after it, on top — the same number
// twice at the same spot. draw_text skips exactly this position while a step is open; the
// shape's other values stay legible as the chain walks them. Far-off sentinel = suppress
// nothing.
Vec2d m_autoedit_label_pos{1e18, 1e18};
std::vector<int> m_selection; // selected entity indices (Mode::Select)
std::vector<std::pair<int, SketchPointRole>> m_point_sel; // selected individual points
int m_last_mouse_x{0}; // last cursor pos (canvas client px), for
@@ -1036,6 +1088,16 @@ private:
Vec2d m_live_slot_angle_label{0,0}; // straight-slot centreline angle label
int m_live_slot_fi{-1}; // the straight-slot Feature (rebuild edits)
std::vector<Feature> m_features; // parametric groups over m_entities
// Sketch-local redo: the state each undo_last_entity() replaced, and the entity/constraint
// counts it left behind (a mismatch means the sketch moved on and the redo is stale).
struct SketchSnap {
std::vector<SketchEntity> entities; std::vector<SketchEntityConstraintDef> constraints;
std::vector<Feature> features; std::vector<DimAnnot> dimensions;
size_t after_entities{0}; size_t after_constraints{0};
};
std::vector<SketchSnap> m_sketch_redo;
int m_skipped_last{0}; // constraints the last solve could not apply (reported on change)
double m_chain_dup_tol{1e-9}; // plane units ~3 px at the last chain click (double-click repeat)
int m_open_feature{-1}; // index of the Feature being built, or -1
// In-canvas edit-op gizmo state (Fillet/Chamfer/Offset/Mirror). GUI-only, reset by
@@ -1054,6 +1116,10 @@ private:
std::vector<SketchEntity> m_op_ghost; // live result preview (recomputed on value change)
bool m_op_dragging_arrow{false}; // arrowhead drag in progress
std::vector<int> m_mirror_targets; // Mirror: entities to be mirrored (axis = m_op_a)
// Offset: the whole chain the picked entity belongs to (connected by shared endpoints), so
// an outline offsets as one outline. A single entity when it is not part of a chain.
std::vector<int> m_op_chain;
std::vector<SketchEntity> op_chain_entities() const;
// In-canvas imported-art transform gizmo (Mode::TransformArt). GUI-only. The art's
// untransformed contours + its bbox in base coords; the live offset/scale; the grabbed
@@ -1109,6 +1175,7 @@ private:
bool m_solve_ok{true}; // solver consistent (no conflicting constraints)
std::vector<char> m_entity_conflict; // per-entity flag: touched by a conflicting constraint
std::vector<DimAnnot> m_dimensions; // placed dimension quotes (Mode::Dimension)
std::vector<int> m_bad_dims; // dims whose driving constraint the solver rejected
int m_dim_e0{-1}; // first picked point's entity (Dimension)
SketchPointRole m_dim_r0{SketchPointRole::P0};
bool m_dim_has0{false}; // a first point is pending
@@ -1132,6 +1199,13 @@ private:
GLModel m_vertex_model;
GLModel m_highlight_model;
int m_dim_label_seq{0};
// Screen rects of the labels already drawn this frame (cleared with m_dim_label_seq). Two
// labels on a SMALL feature collide: every anchor offset in this file is a multiple of the
// text height, so once the feature's on-screen size drops below one, a line's Length and
// Angle labels land on the same spot. Each label is its own centred window and nothing
// detects proximity, so the later one is pushed clear before it is drawn.
struct LabelRect { double x{0}, y{0}, w{0}, h{0}; };
std::vector<LabelRect> m_label_rects;
float m_render_scale{1.0f}; // canvas scale for Measure-style dim labels
GLModel m_fill_model; // translucent face fill for closed regions
std::vector<DisplaySketch> m_display_sketches; // committed sketches drawn persistently
@@ -1149,12 +1223,29 @@ private:
int m_pick_only_body{-1}; // >=0: only this body catches clicks (body-focus x-ray for CoordSys picking)
const std::vector<Transform3d>* m_solid_xform{nullptr}; // per-body display transform (for edge sampling)
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.
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
int m_sel_face{-1};
int m_sel_edge{-1};
std::vector<Vec3d> m_sel_edge_pts;
// Edges picked BEFORE m_sel_edge in a Shift/Ctrl+click set, same body, with their world
// polylines for the highlight. m_sel_edge stays the last-clicked one, so everything that
// reads a single edge (the radius gizmo, the offer header) keeps working unchanged.
std::vector<int> m_sel_edges_more;
std::vector<std::vector<Vec3d>> m_sel_edges_more_pts;
Vec3d m_sel_vertex_pt{Vec3d::Zero()}; // world point of a picked vertex
bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // pick + notify
// What a click at (mx,my) WOULD take, resolved without touching the selection. One
@@ -1173,9 +1264,8 @@ private:
// change) read honestly — the change has to be predictable before the click, not only after.
SolidPick m_pre; // what the pointer is currently over (kind None = nothing)
bool update_solid_hover(GLCanvas3D& canvas, const wxMouseEvent& evt); // true when it changed
// Left-drag rubber band: sweep a rectangle over the plate to take a whole body. Orbit
// moves to middle-drag in this canvas (DesignCanvas::set_cad_navigation) so the left
// button is free for it, which is the CAD convention (Onshape/SolidWorks).
// Left-drag rubber band: sweep a rectangle over the plate to take a whole body. While
// Preferences give left-drag to the camera it takes Shift+left-drag, as in Prepare.
GLSelectionRectangle m_rubber;
void pick_bodies_in_rectangle(); // resolve the swept rectangle -> whole-body selection
bool on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas); // the body; on_mouse wraps it
@@ -1187,10 +1277,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};
@@ -1207,8 +1315,8 @@ 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;
int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none)
+316
View File
@@ -0,0 +1,316 @@
#include "DesignTextDialog.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Widgets/DialogButtons.hpp"
#include "slic3r/GUI/Widgets/TextInput.hpp"
#include "slic3r/GUI/Widgets/ComboBox.hpp"
#include "slic3r/GUI/Widgets/CheckBox.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include "slic3r/Utils/WxFontUtils.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Emboss.hpp"
#include "libslic3r/CAD/SketchImport.hpp"
#include "libslic3r/Point.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include <wx/arrstr.h>
#include <wx/dcbuffer.h>
#include <wx/event.h>
#include <wx/font.h>
#include <wx/fontenc.h>
#include <wx/fontenum.h>
#include <wx/gdicmn.h>
#include <wx/graphics.h>
#include <wx/settings.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/tglbtn.h>
#include <wx/toplevel.h>
#include <algorithm>
#include <cstddef>
#include <limits>
#include <memory>
#include <string>
namespace Slic3r { namespace GUI {
static const char* kFontKey = "cad_text_font"; // WxFontUtils::store_wxFont descriptor
static const char* kHeightKey = "cad_text_height"; // mm
// Installed scalable faces, sorted, enumerated once per session: listing them takes long enough on
// a machine with many fonts to be felt each time the dialog opened.
static const wxArrayString& font_faces()
{
static wxArrayString faces = [] {
// '@' faces are the vertical-writing aliases GTK/MSW list twice.
wxArrayString f = wxFontEnumerator::GetFacenames(wxFONTENCODING_SYSTEM, false);
f.erase(std::remove_if(f.begin(), f.end(), [](const wxString& n) { return n.StartsWith("@"); }), f.end());
f.Sort();
return f;
}();
return faces;
}
static bool parse_mm(wxString t, double& out)
{
t.Replace(",", ".");
return t.Trim(true).Trim(false).ToCDouble(&out) && out >= 0.5 && out <= 500.0;
}
DesignTextDialog::DesignTextDialog(wxWindow* parent, const Spec* initial)
: DPIDialog(parent, wxID_ANY, _L("Text"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
{
SetFont(Label::Body_14);
SetBackgroundColour(*wxWHITE); // light palette colour, dark-mapped by UpdateDlgDarkUI
const int em = em_unit();
// The text being edited, else the last used font and height, else the GUI font at 10 mm.
wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
double height = 10.0;
if (initial != nullptr) {
wxFont f = WxFontUtils::load_wxFont(initial->font);
if (f.IsOk()) font = f;
if (initial->height > 0.0) height = std::clamp(initial->height, 0.5, 500.0);
} else if (AppConfig* cfg = wxGetApp().app_config) {
const std::string desc = cfg->get(kFontKey);
if (!desc.empty()) {
wxFont f = WxFontUtils::load_wxFont(desc);
if (f.IsOk()) font = f;
}
const std::string h = cfg->get(kHeightKey);
if (!h.empty()) {
try { height = std::clamp(std::stod(h), 0.5, 500.0); } catch (...) {}
}
}
m_last_height = height;
auto* form = new wxFlexGridSizer(2, em / 2, em);
form->AddGrowableCol(1, 1);
auto label = [this, form](const wxString& text) {
form->Add(new wxStaticText(this, wxID_ANY, text), 0, wxALIGN_CENTER_VERTICAL);
};
m_text = new ::TextInput(this, initial ? initial->text : wxString(), "", "", wxDefaultPosition,
wxSize(30 * em, -1), wxTE_PROCESS_ENTER);
m_text->GetTextCtrl()->SetHint(_L("Type the text to insert"));
label(_L("Text"));
form->Add(m_text, 1, wxEXPAND);
m_face = new ::ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(30 * em, -1), 0, nullptr,
wxCB_READONLY);
const wxArrayString& faces = font_faces();
for (const wxString& f : faces)
m_face->Append(f);
const int sel = faces.Index(font.GetFaceName());
m_face->SetSelection(sel != wxNOT_FOUND ? sel : 0);
label(_L("Font"));
form->Add(m_face, 1, wxEXPAND);
// Orca's CheckBox carries no label: each one sits beside its own text.
auto* style = new wxBoxSizer(wxHORIZONTAL);
auto check = [this, style, em](const wxString& text, bool value) {
auto* c = new ::CheckBox(this);
c->SetValue(value);
style->Add(c, 0, wxALIGN_CENTER_VERTICAL);
style->Add(new wxStaticText(this, wxID_ANY, text), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, em / 2);
return c;
};
m_bold = check(_L("Bold"), WxFontUtils::is_bold(font));
m_italic = check(_L("Italic"), WxFontUtils::is_italic(font));
form->AddSpacer(0);
form->Add(style, 0);
m_height = new ::TextInput(this, wxString::FromCDouble(height, 1), _L("mm"), "", wxDefaultPosition,
wxSize(10 * em, -1), wxTE_PROCESS_ENTER);
label(_L("Height"));
form->Add(m_height, 0);
// The outline of what will be inserted, fitted to the box, with its real size under it.
// A thumbnail only: the text itself is drawn in the canvas, where it will be.
m_preview = new wxWindow(this, wxID_ANY, wxDefaultPosition, wxSize(30 * em, 5 * em));
m_preview->SetBackgroundStyle(wxBG_STYLE_PAINT);
m_preview->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { draw_preview(m_preview); });
m_preview->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { m_preview->Refresh(); e.Skip(); });
m_size = new wxStaticText(this, wxID_ANY, wxEmptyString);
auto* buttons = new DialogButtons(this, {"OK", "Cancel"});
m_ok = buttons->GetOK();
auto* top = new wxBoxSizer(wxVERTICAL);
top->Add(form, 0, wxEXPAND | wxALL, em);
top->Add(m_preview, 1, wxEXPAND | wxLEFT | wxRIGHT, em);
top->Add(m_size, 0, wxLEFT | wxRIGHT | wxTOP, em);
top->Add(buttons, 0, wxEXPAND);
m_text->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) { update_preview(); e.Skip(); });
m_text->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { accept(); }); // Enter = OK, as everywhere in the tab
m_face->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) { load_font(); update_preview(); e.Skip(); });
for (::CheckBox* c : {m_bold, m_italic})
c->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); CallAfter([this] { load_font(); update_preview(); }); });
m_height->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) { update_preview(); e.Skip(); });
m_height->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { accept(); });
buttons->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { accept(); });
buttons->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { cancel(); });
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { cancel(); });
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_ESCAPE) cancel();
else e.Skip();
});
SetSizerAndFit(top);
// Out of the middle of the window, where the text is being placed: top right of the parent.
if (parent != nullptr) {
const wxRect pr = parent->GetScreenRect();
SetPosition(wxPoint(std::max(pr.GetLeft(), pr.GetRight() - GetSize().x - 2 * em), pr.GetTop() + 8 * em));
} else {
CenterOnParent();
}
wxGetApp().UpdateDlgDarkUI(this);
load_font();
update_preview();
m_text->GetTextCtrl()->SetFocus();
}
double DesignTextDialog::height_mm() const
{
double h = 0.0;
if (parse_mm(m_height->GetTextCtrl()->GetValue(), h))
const_cast<DesignTextDialog*>(this)->m_last_height = h;
return m_last_height;
}
wxString DesignTextDialog::text() const { return m_text->GetTextCtrl()->GetValue(); }
DesignTextDialog::Spec DesignTextDialog::spec() const
{
return { text(), WxFontUtils::store_wxFont(current_font()), height_mm() };
}
void DesignTextDialog::accept()
{
if (m_done || m_regions.empty()) return; // nothing to insert: the size line says why
m_done = true;
if (AppConfig* cfg = wxGetApp().app_config) {
cfg->set(kFontKey, WxFontUtils::store_wxFont(current_font()));
cfg->set(kHeightKey, std::to_string(height_mm()));
}
if (on_accept) on_accept();
}
void DesignTextDialog::cancel()
{
if (m_done) return;
m_done = true;
if (on_cancel) on_cancel();
}
wxFont DesignTextDialog::current_font() const
{
wxFontInfo info(12);
if (m_face->GetSelection() != wxNOT_FOUND)
info.FaceName(m_face->GetString(m_face->GetSelection()));
info.Bold(m_bold->GetValue()).Italic(m_italic->GetValue());
return wxFont(info);
}
void DesignTextDialog::load_font()
{
const wxFont font = current_font();
m_font_file.reset();
if (font.IsOk() && WxFontUtils::can_load(font))
m_font_file = WxFontUtils::create_font_file(font);
}
void DesignTextDialog::update_preview()
{
const std::string utf8(text().ToUTF8().data());
m_regions.clear();
if (m_font_file && !utf8.empty())
m_regions = text_to_regions(utf8, height_mm(), m_font_file);
wxString line;
if (!m_font_file)
line = _L("This font cannot be used for text — pick another one");
else if (utf8.empty())
line = _L("Type the text to see its size");
else if (m_regions.empty())
line = _L("This font has no outline for this text");
else {
double lo_x = std::numeric_limits<double>::max(), lo_y = lo_x;
double hi_x = -lo_x, hi_y = -lo_x;
for (const auto& r : m_regions)
for (const auto& c : r)
for (const Vec2d& p : c) {
lo_x = std::min(lo_x, p.x()); hi_x = std::max(hi_x, p.x());
lo_y = std::min(lo_y, p.y()); hi_y = std::max(hi_y, p.y());
}
line = wxString::Format(_L("Size: %.1f × %.1f mm"), hi_x - lo_x, hi_y - lo_y);
}
m_size->SetLabel(line);
if (m_ok) m_ok->Enable(!m_regions.empty());
m_preview->Refresh();
if (on_change) on_change();
}
void DesignTextDialog::draw_preview(wxWindow* canvas)
{
wxAutoBufferedPaintDC dc(canvas);
const bool dark = wxGetApp().dark_mode();
dc.SetBackground(wxBrush(dark ? wxColour(0x1e, 0x1e, 0x22) : wxColour(0xf4, 0xf4, 0xf6)));
dc.Clear();
if (m_regions.empty()) return;
double lo_x = std::numeric_limits<double>::max(), lo_y = lo_x, hi_x = -lo_x, hi_y = -lo_x;
for (const auto& r : m_regions)
for (const auto& c : r)
for (const Vec2d& p : c) {
lo_x = std::min(lo_x, p.x()); hi_x = std::max(hi_x, p.x());
lo_y = std::min(lo_y, p.y()); hi_y = std::max(hi_y, p.y());
}
const wxSize sz = canvas->GetClientSize();
const double pad = 0.08 * std::min(sz.x, sz.y);
const double w = std::max(hi_x - lo_x, 1e-6), h = std::max(hi_y - lo_y, 1e-6);
const double k = std::min((sz.x - 2 * pad) / w, (sz.y - 2 * pad) / h);
const double ox = 0.5 * (sz.x - k * w), oy = 0.5 * (sz.y + k * h); // y up -> screen y down
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (!gc) return;
wxGraphicsPath path = gc->CreatePath();
for (const auto& r : m_regions)
for (const auto& c : r) {
if (c.empty()) continue;
path.MoveToPoint(ox + k * (c[0].x() - lo_x), oy - k * (c[0].y() - lo_y));
for (size_t i = 1; i < c.size(); ++i)
path.AddLineToPoint(ox + k * (c[i].x() - lo_x), oy - k * (c[i].y() - lo_y));
path.CloseSubpath();
}
gc->SetBrush(wxBrush(wxColour(0x00, 0x96, 0x88))); // Orca accent
gc->SetPen(*wxTRANSPARENT_PEN);
gc->FillPath(path, wxODDEVEN_RULE); // holes (the counter of an "o") stay open
}
void DesignTextDialog::on_dpi_changed(const wxRect&)
{
m_text->Rescale();
m_face->Rescale();
m_height->Rescale();
m_bold->Rescale();
m_italic->Rescale();
GetSizer()->SetSizeHints(this);
Refresh();
}
void DesignTextDialog::on_sys_color_changed()
{
SetBackgroundColour(*wxWHITE);
wxGetApp().UpdateDlgDarkUI(this);
m_preview->Refresh(); // its colours are read at paint time
Refresh();
}
}} // namespace Slic3r::GUI
+81
View File
@@ -0,0 +1,81 @@
#ifndef slic3r_GUI_DesignTextDialog_hpp_
#define slic3r_GUI_DesignTextDialog_hpp_
#include "slic3r/GUI/GUI_Utils.hpp"
#include "libslic3r/CAD/SketchImport.hpp"
#include <wx/font.h>
#include <wx/string.h>
#include <functional>
#include <memory>
#include <string>
class wxStaticText;
class TextInput;
class ComboBox;
class CheckBox;
namespace Slic3r {
namespace Emboss { struct FontFile; }
namespace GUI {
// Text for the Design tab: the words, the font (face, bold, italic) and the height, with the
// size in millimetres of exactly what will be inserted. MODELESS: the host draws the text in
// the canvas as it is typed (on_change), so the dialog stays out of the way and can be moved —
// a modal dialog is pinned over the middle of the window on GNOME. Enter or OK accepts, Esc,
// Cancel or closing it cancels (charter 4.2); each fires its callback once and the host then
// destroys the dialog. A new text starts from the last font and height used.
class DesignTextDialog : public DPIDialog
{
public:
struct Spec {
wxString text;
std::string font; // WxFontUtils::store_wxFont descriptor (face, bold, italic)
double height{10.0};
};
// `initial` reopens an existing text for editing; nullptr starts a new one.
DesignTextDialog(wxWindow* parent, const Spec* initial = nullptr);
// The vectorised text, centred on the origin, in mm. Empty when there is nothing to insert.
const ImportRegions& regions() const { return m_regions; }
wxString text() const;
Spec spec() const;
std::function<void()> on_change; // the outline changed (text, font or height)
std::function<void()> on_accept; // Enter / OK with something to insert
std::function<void()> on_cancel; // Esc / Cancel / closed
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
void on_sys_color_changed() override;
private:
void load_font(); // m_face/m_bold/m_italic -> m_font_file
void update_preview(); // text/font/height -> m_regions + size label
wxFont current_font() const;
void draw_preview(wxWindow* canvas);
void accept();
void cancel();
bool m_done{false}; // accept/cancel fire once
double height_mm() const; // the typed height, or the last valid one while it is being typed
::TextInput* m_text{nullptr};
::ComboBox* m_face{nullptr};
::CheckBox* m_bold{nullptr};
::CheckBox* m_italic{nullptr};
::TextInput* m_height{nullptr}; // mm; Orca's SpinInput is integer-only
double m_last_height{10.0};
wxWindow* m_preview{nullptr};
wxStaticText* m_size{nullptr};
wxWindow* m_ok{nullptr};
std::shared_ptr<const Emboss::FontFile> m_font_file;
ImportRegions m_regions;
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GUI_DesignTextDialog_hpp_
+93 -16
View File
@@ -24,6 +24,8 @@
#include <cmath>
#include <algorithm>
#include <utility>
#include <atomic>
#include <set>
#include <nlohmann/json.hpp>
#include <boost/log/trivial.hpp>
@@ -161,15 +163,15 @@ json describe_tools()
json{{"name", "profile"}, {"type", "array"}, {"default", json::array()}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}},
json{{"name", "boolean"}, {"type", "string"}, {"enum", json::array({"new", "union", "subtract", "intersect"})}, {"default", "new"}},
})}},
json{{"name", "fillet"}, {"summary", "Round a measured edge of a body (edge id from query_topology on that body)."},
json{{"name", "fillet"}, {"summary", "Round measured edges of a body (edge ids from query_topology on that body)."},
{"params", json::array({
json{{"name", "edge"}, {"type", "integer"}},
json{{"name", "edge"}, {"type", "integer|array"}, {"description", "one edge id, or an array of ids rounded together in one feature"}},
json{{"name", "radius"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}},
json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}},
})}},
json{{"name", "chamfer"}, {"summary", "Chamfer a measured edge of a body (edge id from query_topology on that body)."},
json{{"name", "chamfer"}, {"summary", "Chamfer measured edges of a body (edge ids from query_topology on that body)."},
{"params", json::array({
json{{"name", "edge"}, {"type", "integer"}},
json{{"name", "edge"}, {"type", "integer|array"}, {"description", "one edge id, or an array of ids chamfered together in one feature"}},
json{{"name", "distance"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}},
json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}},
})}},
@@ -872,16 +874,30 @@ int target_body_arg(const json& params, const CadDocument& doc)
return bi; // <0 -> kernel uses the last body
}
// `edge` is one id or an array of ids; an array becomes ONE feature, every id resolved
// against the same body (a chain of single-edge features would see the ids drift).
std::vector<int> edge_ids_arg(const json& params, const char* verb)
{
if (!params.contains("edge"))
throw std::runtime_error(std::string(verb) + " needs 'edge' (id or array of ids from query_topology)");
const json& e = params["edge"];
std::vector<int> ids;
if (e.is_array()) for (const json& v : e) ids.push_back(v.get<int>());
else ids.push_back(e.get<int>());
if (ids.empty()) throw std::runtime_error(std::string(verb) + ": 'edge' is an empty array");
return ids;
}
json action_fillet(DesignPanel* panel, const json& params)
{
if (!params.contains("edge")) throw std::runtime_error("fillet needs 'edge' (id from query_topology)");
const std::vector<int> edges = edge_ids_arg(params, "fillet");
const double radius = params.value("radius", 1.0);
if (radius <= 0) throw std::runtime_error("radius must be > 0");
CadDocument& doc = panel->mcp_doc();
if (doc.bodies.empty()) throw std::runtime_error("no body to fillet");
int bi = target_body_arg(params, doc);
doc.checkpoint();
int f = doc.add_fillet(radius, params["edge"].get<int>(), "Fillet");
int f = doc.add_fillet(radius, edges, "Fillet");
if (bi >= 0) doc.features[f].target_body = bi; // edge id resolved against THIS body's shape
bool ok = doc.recompute();
if (!ok) { const std::string why = doc.error; doc.undo(); doc.error = why; }
@@ -892,14 +908,14 @@ json action_fillet(DesignPanel* panel, const json& params)
json action_chamfer(DesignPanel* panel, const json& params)
{
if (!params.contains("edge")) throw std::runtime_error("chamfer needs 'edge' (id from query_topology)");
const std::vector<int> edges = edge_ids_arg(params, "chamfer");
const double dist = params.value("distance", 1.0);
if (dist <= 0) throw std::runtime_error("distance must be > 0");
CadDocument& doc = panel->mcp_doc();
if (doc.bodies.empty()) throw std::runtime_error("no body to chamfer");
int bi = target_body_arg(params, doc);
doc.checkpoint();
int c = doc.add_chamfer(dist, params["edge"].get<int>(), "Chamfer");
int c = doc.add_chamfer(dist, edges, "Chamfer");
if (bi >= 0) doc.features[c].target_body = bi; // edge id resolved against THIS body's shape
bool ok = doc.recompute();
if (!ok) { const std::string why = doc.error; doc.undo(); doc.error = why; }
@@ -1491,9 +1507,13 @@ json sketch_report(DesignSketchTool& t)
}
json open_ends = json::array();
for (const Vec2d& p : rep.open_ends) open_ends.push_back(json::array({p.x(), p.y()}));
// A profile is buildable when at least one loop closed and nothing is left dangling.
const bool buildable = !rep.loops.empty() && rep.open_ends.empty();
return json{{"closed_loops", loops}, {"open_ends", open_ends}, {"buildable", buildable}};
// A profile is buildable when at least one loop closed, nothing is left dangling and no loop
// crosses or folds back on itself.
json defects = json::array();
for (const auto& l : rep.loops)
if (l.defect) defects.push_back(json::array({l.defect_at.x(), l.defect_at.y()}));
const bool buildable = !rep.loops.empty() && rep.open_ends.empty() && defects.empty();
return json{{"closed_loops", loops}, {"open_ends", open_ends}, {"defects", defects}, {"buildable", buildable}};
}
json action_sketch_describe(DesignPanel* panel, const json& params)
@@ -2002,6 +2022,22 @@ std::string handle_on_main(const std::string& method, const json& params, const
DesignPanel* panel = DesignPanel::ensure();
if (!panel)
return rpc_error(id, -32001, "Design panel not ready");
// A project opened without ever showing the Design tab has its recipe only in the Model;
// load it, as showing the tab would, before anything reads or writes the document.
panel->hydrate_from_model();
// Methods that only LOOK. Everything else changes the document or the live sketch, and
// must not do it under a GUI editor, a sketch session it does not own, or a rebuild in
// progress (the GUI's worker thread holds the document then).
static const std::set<std::string> kReadOnly = {
"describe_tools", "describe_scene", "query_topology", "measure", "mass_properties",
"slice_body", "validate_against", "list_verbs", "sketch_describe", "sketch_validate",
"check_interference" };
if (kReadOnly.count(method) == 0) {
std::string why;
if (panel->mcp_busy(method.rfind("sketch_", 0) == 0, why))
return rpc_error(id, -32002, "Design tab busy: " + why);
}
// Stale-id guard, checked here rather than in each handler.
//
@@ -2116,11 +2152,16 @@ std::string dispatch_request(const std::string& line)
auto prom = std::make_shared<std::promise<std::string>>();
auto fut = prom->get_future();
// 0 = queued, 1 = running, 2 = abandoned. A request that timed out while still QUEUED must
// never run: the client has been told it failed, and a retry would otherwise apply it twice.
auto state = std::make_shared<std::atomic<int>>(0);
// Nothing may escape this lambda. It is invoked by the wx event loop, which has no
// handler of its own, so an escaping exception is std::terminate — the socket would
// become a way for any client to kill the application. handle_on_main() catches what
// it knows about; this catches what it does not, and still answers the caller.
wxGetApp().CallAfter([prom, method, params, id]() {
wxGetApp().CallAfter([prom, state, method, params, id]() {
int queued = 0;
if (!state->compare_exchange_strong(queued, 1)) return; // abandoned by a timeout
try {
prom->set_value(handle_on_main(method, params, id));
} catch (const std::exception& ex) {
@@ -2129,20 +2170,33 @@ std::string dispatch_request(const std::string& line)
prom->set_value(rpc_error(id, -32000, "internal error: unknown exception"));
}
});
if (fut.wait_for(std::chrono::seconds(15)) != std::future_status::ready)
return rpc_error(id, -32000, "main-thread timeout");
if (fut.wait_for(std::chrono::seconds(15)) != std::future_status::ready) {
int queued = 0;
if (state->compare_exchange_strong(queued, 2))
return rpc_error(id, -32000, "main-thread timeout: the command was NOT run");
// Already running: it will finish, so wait for its real answer rather than report a
// failure for a command that is in fact being applied.
fut.wait();
}
return fut.get();
}
// Read newline-delimited requests off one client connection until EOF.
// Read newline-delimited requests off one client connection until EOF. A line may not grow
// past kMaxLine: a client that never sends a newline would otherwise grow this buffer until the
// process runs out of memory.
void serve_client(int cfd)
{
constexpr size_t kMaxLine = 16u << 20; // 16 MB — far above any real request
std::string buf;
char chunk[4096];
for (;;) {
ssize_t n = ::read(cfd, chunk, sizeof(chunk));
if (n <= 0) break;
buf.append(chunk, size_t(n));
if (buf.size() > kMaxLine && buf.find('\n') == std::string::npos) {
BOOST_LOG_TRIVIAL(error) << "MCP: request line over " << kMaxLine << " bytes; closing the connection";
return;
}
size_t nl;
while ((nl = buf.find('\n')) != std::string::npos) {
std::string line = buf.substr(0, nl);
@@ -2162,9 +2216,30 @@ void serve_client(int cfd)
}
}
// Where the socket lives, for the exit handler.
std::string g_sock_path;
void remove_socket_at_exit()
{
struct stat st{};
if (!g_sock_path.empty() && ::lstat(g_sock_path.c_str(), &st) == 0 && S_ISSOCK(st.st_mode))
::unlink(g_sock_path.c_str());
}
void server_thread(std::string sock_path)
{
::unlink(sock_path.c_str());
// Clear a stale socket from an earlier run — and ONLY a socket. ORCA_CAD_MCP names a path,
// and unlinking it unconditionally deleted whatever file that path happened to be.
{
struct stat st{};
if (::lstat(sock_path.c_str(), &st) == 0) {
if (!S_ISSOCK(st.st_mode)) {
BOOST_LOG_TRIVIAL(error) << "MCP: " << sock_path << " exists and is not a socket; refusing to replace it";
return;
}
::unlink(sock_path.c_str());
}
}
int sfd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd < 0) { BOOST_LOG_TRIVIAL(error) << "MCP: socket() failed"; return; }
@@ -2189,6 +2264,8 @@ void server_thread(std::string sock_path)
::close(sfd); ::unlink(sock_path.c_str()); return;
}
if (::listen(sfd, 1) < 0) { BOOST_LOG_TRIVIAL(error) << "MCP: listen() failed"; ::close(sfd); return; }
g_sock_path = sock_path;
std::atexit(remove_socket_at_exit); // do not leave the socket file behind
BOOST_LOG_TRIVIAL(info) << "MCP control listening on " << sock_path;
for (;;) {
+11 -9
View File
@@ -1,22 +1,24 @@
#ifndef slic3r_GUI_McpControl_hpp_
#define slic3r_GUI_McpControl_hpp_
// MCP control surface (slice 1): a local JSON-RPC 2.0 server, line-delimited over a
// Unix domain socket, that lets an external MCP bridge drive and perceive the Design
// tab. Off unless the env var ORCA_CAD_MCP is set:
// MCP control surface: a local JSON-RPC 2.0 server, line-delimited over a Unix domain
// socket, that lets an external MCP bridge drive and perceive the Design tab. Off unless the
// CAD feature is enabled in Preferences AND the env var ORCA_CAD_MCP is set:
// ORCA_CAD_MCP=1 -> socket at /tmp/orca-cad-mcp.sock
// ORCA_CAD_MCP=/path/to.sock -> socket at that path
// All CAD work is marshalled onto the wx main thread and runs through the SAME
// CadDocument kernel the GUI uses (no parallel engine). Slice-1 methods:
// describe_tools, describe_scene, extrude.
// ORCA_CAD_MCP=/path/to.sock -> socket at that path (an existing non-socket file there is
// left alone and the server does not start)
// The socket is created 0600 and removed at exit. All CAD work is marshalled onto the wx main
// thread and runs through the SAME CadDocument kernel the GUI uses (no parallel engine);
// describe_tools lists the methods. A method that changes the document is refused (-32002)
// while the Design tab is busy with it — a rebuild, an open feature card, a sketch session.
//
// ponytail: Unix-socket only (POSIX). Windows compiles this to a no-op; add a named
// pipe transport when a Windows agent actually needs it.
namespace Slic3r { namespace GUI {
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once after the
// MainFrame + DesignPanel exist. No-op when the env var is unset or on Windows.
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once the MainFrame exists —
// the Design panel is built on the first request. No-op when the env var is unset or on Windows.
void start_mcp_control_if_enabled();
}} // namespace Slic3r::GUI
+60 -9
View File
@@ -6,7 +6,7 @@
#include <functional>
#include <imgui/imgui.h>
#include <imgui/imgui_internal.h> // BringWindowToDisplayFront / GetCurrentWindow
#include <imgui/imgui_internal.h> // FindWindowByName / BringWindowToDisplayFront
#include <cstdio>
#include <cstdlib>
@@ -20,10 +20,13 @@ namespace GUI {
namespace {
const char* const kWindowName = "##sketchvalue";
// Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel,
// and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error.
// Parsing accepts either separator because a keyboard's numeric pad may only offer one.
std::string fmt_value(double v, int digits = 2)
// Trailing zeros are dropped, so a count opens as "3" and a length as "37.457", not "37.46".
std::string fmt_value(double v, int digits = 3)
{
char fmt[16];
std::snprintf(fmt, sizeof(fmt), "%%.%df", digits);
@@ -31,7 +34,13 @@ std::string fmt_value(double v, int digits = 2)
std::snprintf(buf, sizeof(buf), fmt, v);
for (char* c = buf; *c; ++c)
if (*c == ',') *c = '.';
return std::string(buf);
std::string s(buf);
if (s.find('.') != std::string::npos) {
while (!s.empty() && s.back() == '0') s.pop_back();
if (!s.empty() && s.back() == '.') s.pop_back();
}
if (s == "-0") s = "0";
return s;
}
bool parse_value(const char* text, double& out)
@@ -81,6 +90,9 @@ void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std:
m_cancel = std::move(on_cancel);
const std::string v = fmt_value(value);
std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str());
m_prefill_value = value;
m_prefill_text = v;
m_active = false;
m_open = true;
// ImGui takes keyboard focus for one frame on request; asking on the frame the field first
// appears is what makes typing land without a click. There is no window manager to consult.
@@ -92,6 +104,7 @@ void SketchInlineEditor::close()
{
m_open = false;
m_focus_pending = false;
m_active = false;
m_commit = nullptr;
m_cancel = nullptr;
m_err.clear();
@@ -115,10 +128,32 @@ void SketchInlineEditor::do_cancel()
if (cb) cb();
}
bool SketchInlineEditor::refuse(const std::string& why)
{
// Only meaningful while a commit callback runs and before it opened another field.
if (!m_in_commit || m_open) return false;
m_anchor = m_last.anchor;
m_title = m_last.title;
m_commit = m_last.commit;
m_cancel = m_last.cancel;
m_prefill_value = m_last.prefill_value;
m_prefill_text = m_last.prefill_text;
std::snprintf(m_buf, sizeof(m_buf), "%s", m_last.buf.c_str());
m_err = why;
m_open = true;
m_focus_pending = true;
ux_trace("refused", m_title, "reason=" + why);
return true;
}
void SketchInlineEditor::do_commit()
{
double v = 0.0;
if (!parse_value(m_buf, v)) {
// An untouched field commits the value it opened with, not its rounded display: Enter on a
// 37.4567 line must not turn it into a 37.457 driving dimension.
if (m_prefill_text == m_buf)
v = m_prefill_value;
else if (!parse_value(m_buf, v)) {
// Refusing input in silence is indistinguishable from the app having frozen: the field
// just sits there and the user has no idea what it wants. Say so in the title line and
// keep editing.
@@ -129,11 +164,15 @@ void SketchInlineEditor::do_commit()
}
ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4));
auto cb = m_commit;
m_last = Closed{ m_anchor, m_title, std::string(m_buf), m_commit, m_cancel, m_prefill_value, m_prefill_text };
close();
// AFTER close(): the callback may open the next queued dimension (a rectangle queues Width
// then Height), and doing that into a field that still believes it is open would drop the
// second one's prefill on the floor.
m_in_commit = true;
if (cb) cb(v);
m_in_commit = false;
m_last = Closed{};
}
bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
@@ -145,10 +184,9 @@ bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
// NoInputs is what every other sketch overlay sets and is exactly what this one must not:
// it is the only overlay in the tab that the user types into.
imgui.begin(std::string("##sketchvalue"),
imgui.begin(std::string(kWindowName),
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
if (!m_title.empty() || !m_err.empty()) {
if (m_err.empty()) {
@@ -168,10 +206,15 @@ bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
// EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is
// replaced by the first digit typed, which is what "pre-selected" meant when this was a
// wxTextCtrl and is what makes typing a value a single gesture.
// No CharsDecimal: it lets only the LOCALE's decimal separator through, so in a C locale a
// comma could not be typed at all, and it admits '*' and '/' that the parser then refuses.
// parse_value() is the one judge of what is a number.
const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf),
ImGuiInputTextFlags_EnterReturnsTrue
| ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_CharsDecimal);
| ImGuiInputTextFlags_AutoSelectAll);
m_active = ImGui::IsItemActive();
// Tab accepts the value like Enter does (and moves on to the next queued field, if any).
const bool tabbed = m_active && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Tab));
// MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the
// keyboard and whether our widget is the active one. "Typing does not arrive" has two very
// different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never
@@ -195,11 +238,19 @@ bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
// Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that
// must not happen inside this frame's window.
if (entered)
if (entered || tabbed)
do_commit();
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
do_cancel();
return true;
}
void SketchInlineEditor::bring_to_front()
{
// Not gated on m_open: a commit closes the field after it was drawn, and that frame still
// shows it. A window that did not Begin this frame is not rendered, so lifting it is inert.
if (ImGuiWindow* w = ImGui::FindWindowByName(kWindowName))
ImGui::BringWindowToDisplayFront(w);
}
}} // namespace Slic3r::GUI
+20 -4
View File
@@ -48,11 +48,17 @@ public:
void close(); // drop it with neither callback
void cancel(); // if open, run the registered cancel (keep-as-drawn)
void commit(); // if open, run the registered commit (accept the typed value)
// Called from inside a commit callback that cannot accept the value (a negative length, a
// count of 1): the field comes back with the typed text and `why` in its title line, instead
// of closing as if the value had been taken.
bool refuse(const std::string& why); // true if the field came back
bool is_open() const { return m_open; }
// Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the
// frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew.
bool render(ImGuiWrapper& imgui, float scale);
// Lift the field above every other overlay. Call it after the frame's last overlay is drawn.
void bring_to_front();
// Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock
// that only a per-frame trace shows:
@@ -69,11 +75,10 @@ public:
// breaks the circle.
std::function<void()> request_frame;
// Kept because callers ask them, but there is no longer any difference to report: with no
// window there is no state where the field is on screen but logically closed, and no state
// where it is open but somebody else holds the keyboard.
bool is_mapped() const { return m_open; }
bool has_focus() const { return m_open; }
// Does the ImGui text widget own the keyboard? False once a click elsewhere deactivated it;
// the panel then forwards Enter/Tab to commit() itself, since ImGui no longer sees them.
bool has_focus() const { return m_open && (m_active || m_focus_pending); }
void dismiss() { close(); }
private:
@@ -88,6 +93,17 @@ private:
std::string m_title;
std::string m_err; // why the last value was refused, shown in the title line
char m_buf[64]{}; // the edited text; ImGui::InputText writes into it
double m_prefill_value{0.0}; // the exact value the field opened with...
std::string m_prefill_text; // ...and how it was shown: an untouched field commits the former
bool m_active{false}; // the InputText was the active item last frame
// What the last commit closed, so refuse() can bring the same field back.
struct Closed {
wxPoint anchor; std::string title; std::string buf;
std::function<void(double)> commit; std::function<void()> cancel;
double prefill_value{0.0}; std::string prefill_text;
};
Closed m_last;
bool m_in_commit{false};
};
}} // namespace Slic3r::GUI
+5 -4
View File
@@ -2,6 +2,7 @@
#include "CalibUtils.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "Widgets/Label.hpp"
#include "MsgDialog.hpp"
#include "DeviceCore/DevConfigUtil.h"
@@ -67,13 +68,13 @@ static wxString get_default_name(wxString filament_name, CalibMode mode){
case Slic3r::CalibMode::Calib_PA_Tower:
break;
case Slic3r::CalibMode::Calib_Flow_Rate:
filament_name += " Flow Rate Calibrated";
filament_name = format_wxstr(_L("%1% Flow Rate Calibrated"), filament_name);
break;
case Slic3r::CalibMode::Calib_Temp_Tower:
filament_name += " Temperature Calibrated";
filament_name = format_wxstr(_L("%1% Temperature Calibrated"), filament_name);
break;
case Slic3r::CalibMode::Calib_Vol_speed_Tower:
filament_name += " Max Vol Speed Calibrated";
filament_name = format_wxstr(_L("%1% Max Vol Speed Calibrated"), filament_name);
break;
case Slic3r::CalibMode::Calib_VFA_Tower:
break;
@@ -95,7 +96,7 @@ static wxString get_tray_name_by_tray_id(int tray_id)
{
wxString tray_name;
if (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID) {
tray_name = "Ext";
tray_name = _L("Ext");
}
else {
int ams_id = tray_id / 4;
+9 -9
View File
@@ -1130,7 +1130,7 @@ void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool mat
for (const auto& item : prusa_profiles)
list->append(item, &const_cast<std::string&>(item.get()));
for (const auto& item : other_profiles)
list->append(item, &const_cast<std::string&>(item.get()));
list->append(item.get() == Materials::UNKNOWN ? _L(item.get()) : from_u8(item.get()), &const_cast<std::string&>(item.get()));
}
void PageMaterials::sort_list_data(PresetList* list, const std::vector<ProfilePrintData>& data)
@@ -1210,13 +1210,13 @@ void PageMaterials::on_activate()
}
const char *PageCustom::default_profile_name = "My Settings";
const char *PageCustom::default_profile_name = L("My Settings");
PageCustom::PageCustom(ConfigWizard *parent)
: ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer"))
{
cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile"));
tc_profile_name = new wxTextCtrl(this, wxID_ANY, default_profile_name);
tc_profile_name = new wxTextCtrl(this, wxID_ANY, _L(default_profile_name));
auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:"));
wxGetApp().UpdateDarkUI(tc_profile_name);
@@ -1224,7 +1224,7 @@ PageCustom::PageCustom(ConfigWizard *parent)
tc_profile_name->Enable(false);
tc_profile_name->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent &evt) {
if (tc_profile_name->GetValue().IsEmpty()) {
if (profile_name_prev.IsEmpty()) { tc_profile_name->SetValue(default_profile_name); }
if (profile_name_prev.IsEmpty()) { tc_profile_name->SetValue(_L(default_profile_name)); }
else { tc_profile_name->SetValue(profile_name_prev); }
} else {
profile_name_prev = tc_profile_name->GetValue();
@@ -1463,7 +1463,7 @@ PageTemperatures::PageTemperatures(ConfigWizard *parent)
auto *default_bed = def_bed.get_default_value<ConfigOptionInts>();
spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0);
append_text(_L("Enter the nozzle_temperature needed for extruding your filament."));
append_text(_L("Enter the nozzle temperature needed for extruding your filament."));
append_text(_L("A rule of thumb is 160 to 230℃ for PLA, and 215 to 250℃ for ABS."));
#endif
@@ -1720,7 +1720,7 @@ void ConfigWizardIndex::msw_rescale()
// Materials
const std::string Materials::UNKNOWN = "(Unknown)";
const std::string Materials::UNKNOWN = L("(Unknown)");
void Materials::push(const Preset *preset)
{
@@ -2188,8 +2188,8 @@ void ConfigWizard::priv::select_default_materials_for_printer_models(Technology
}
printer_names += "\n\n";
std::string message = (technology & T_FFF ?
GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) :
GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names));
GUI::format(_L("The following printer profiles have no default filament: %1%Please select one manually."), printer_names) :
GUI::format(_L("The following printer profiles have no default material: %1%Please select one manually."), printer_names));
MessageDialog msg(q, message, _L("Notice"), wxOK);
msg.ShowModal();
}
@@ -2449,7 +2449,7 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
}
if (!check_unsaved_preset_changes)
if ((check_unsaved_preset_changes = install_bundles.size() > 0))
header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size());
header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of their printers will be activated", install_bundles.size());
// Decide whether to create snapshot based on run_reason and the reset profile checkbox
bool snapshot = true;
+2 -2
View File
@@ -39,7 +39,7 @@ CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent)
m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em);
m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em);
auto* dlg_btns = new DialogButtons(this, {"Scan", "OK", "Cancel"}, "", 1 /*left_aligned*/);
auto* dlg_btns = new DialogButtons(this, {L("Scan"), "OK", "Cancel"}, "", 1 /*left_aligned*/);
auto* ok_btn = dlg_btns->GetOK();
ok_btn->SetLabel(_L("Use Selected"));
ok_btn->Disable();
@@ -91,7 +91,7 @@ void CrealityDiscoveryDialog::run_discovery()
if (!h.model_name.empty())
row.model = h.model_name;
else if (h.cfs_capable)
row.model = "(unknown K-series)";
row.model = _u8L("(unknown K-series)");
else
row.model = "Creality";
m_rows.push_back(std::move(row));
+1 -1
View File
@@ -652,7 +652,7 @@ DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent)
wxString text = _L("Desktop Integration sets this binary to be searchable by the system.\n\nPress \"Perform\" to proceed.");
if (can_undo)
text += "\nPress \"Undo\" to remove previous integration.";
text += "\n" + _L("Press \"Undo\" to remove previous integration.");
vbox->Add(
new wxStaticText( this, wxID_ANY, text),
+1 -1
View File
@@ -154,7 +154,7 @@ void calib_fail_message(MachineObject* obj, std::string cali_mode, std::string r
} else if (reason == "nozzle_diameter is not matched") {
info = _L("Selected diameter and machine diameter do not match");
} else if (reason == "generate auto filament cali gcode failure") {
info = _L("Failed to generate cali gcode");
info = _L("Failed to generate calibration G-code");
} else {
info = wxString(reason);
}
+1 -1
View File
@@ -4313,7 +4313,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
info = _L("Selected diameter and machine diameter do not match");
}
else if (reason == "generate auto filament cali gcode failure") {
info = _L("Failed to generate cali G-code");
info = _L("Failed to generate calibration G-code");
}
else {
info = reason;
@@ -981,7 +981,7 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
}
if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY)
{
material = "Empty";
material = L("Empty");
}
auto itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){
+1 -1
View File
@@ -60,7 +60,7 @@ void wgtMsgPanelItem::CreateGui()
text_sizer->Add(m_text_label, 0, wxALIGN_CENTER_VERTICAL);
if (!m_wiki_url.IsEmpty()) {
m_wiki_link = new wxHyperlinkCtrl(this, wxID_ANY, "Wiki->", m_wiki_url);
m_wiki_link = new wxHyperlinkCtrl(this, wxID_ANY, _L("Wiki->"), m_wiki_url);
m_wiki_link->SetNormalColour(m_colour);
m_wiki_link->SetHoverColour(wxColour(0, 0, 200));
m_wiki_link->SetVisitedColour(*wxBLUE);
+2 -1
View File
@@ -19,6 +19,7 @@
#include "libslic3r/Utils.hpp"
#include <functional>
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
#include <wx/arrstr.h>
@@ -165,7 +166,7 @@ void Downloader::start_download(const std::string& full_url)
// Orca: show error
NotificationManager* ntf_mngr = wxGetApp().notification_manager();
ntf_mngr->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel,
"Could not start download due to malformed URL");
_u8L("Could not start download due to malformed URL"));
return;
}
size_t id = get_next_id();
+2 -2
View File
@@ -161,7 +161,7 @@ void FileGet::priv::get_perform()
}
if (!found) {
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
evt->SetString(GUI::format_wxstr(L"Failed to find suitable filename. Last name: %1%." , (m_dest_folder / final_filename).string()));
evt->SetString(GUI::format_wxstr(_L("Failed to find suitable filename. Last name: %1%."), (m_dest_folder / final_filename).string()));
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
return;
@@ -367,7 +367,7 @@ void FileGet::priv::get_perform()
//TODO: report?
//error_message = GUI::format("Failed to write and move %1% to %2%", tmp_path, dest_path);
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
evt->SetString("Failed to write and move.");
evt->SetString(_L("Failed to write and move."));
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
return;
+3
View File
@@ -78,6 +78,7 @@ EditGCodeDialog::EditGCodeDialog(wxWindow* parent, const std::string& key, const
m_search_bar->ShowCancelButton(true);
m_search_bar->SetDescriptiveText(_L("Search G-code placeholders"));
m_search_bar->SetForegroundColour(*wxBLACK);
m_search_bar->SetBackgroundColour(*wxWHITE);
wxGetApp().UpdateDarkUI(m_search_bar);
m_search_bar->Bind(wxEVT_SET_FOCUS, [](wxFocusEvent&) {
@@ -90,6 +91,7 @@ EditGCodeDialog::EditGCodeDialog(wxWindow* parent, const std::string& key, const
param_sizer->Add(m_search_bar, 0, wxEXPAND | wxALL, border);
m_params_list = new ParamsViewCtrl(this, wxDefaultSize);
m_params_list->SetBackgroundColour(*wxWHITE);
m_params_list->SetFont(wxGetApp().code_font());
wxGetApp().UpdateDarkUI(m_params_list);
param_sizer->Add(m_params_list, 1, wxEXPAND | wxALL, border);
@@ -104,6 +106,7 @@ EditGCodeDialog::EditGCodeDialog(wxWindow* parent, const std::string& key, const
);
m_gcode_editor->SetFont(wxGetApp().code_font());
m_gcode_editor->SetBackgroundColour(*wxWHITE);
m_gcode_editor->SetInsertionPointEnd();
wxGetApp().UpdateDarkUI(m_gcode_editor);
+2 -1
View File
@@ -11,6 +11,7 @@
#include <utility>
#include "GUI_App.hpp"
#include "I18N.hpp"
using json = nlohmann::json;
@@ -165,7 +166,7 @@ wxString FilamentColorCode::GetFilaColorName() const
if (it != m_fila_color_names.end() && !it->second.empty()) { return it->second; }
it = m_fila_color_names.find("en");// retry with English as fallback
return (it != m_fila_color_names.end()) ? it->second : "Unknown";
return (it != m_fila_color_names.end()) ? it->second : _L("Unknown");
}
FilamentColorCode::FilamentColorCode(const wxString& color_code, FilamentColorCodes* owner, FilamentColor&& color, std::unordered_map<wxString, wxString>&& name_map)
+1 -1
View File
@@ -75,7 +75,7 @@ void ExportPresetBundleDialog::on_script_message(const nlohmann::json& j)
OnRequestPresets();
} else if (strCmd == "export_local") {
wxFileDialog dlg(this, _L("Save preset bundle"), "", "export.orca_bundle",
"Orca Preset Bundle (*.orca_bundle)|*.orca_bundle", wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
_L("Orca Preset Bundle (*.orca_bundle)|*.orca_bundle"), wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
wxString path;
wxString name;
if (dlg.ShowModal() == wxID_OK) {
+8 -4
View File
@@ -1015,8 +1015,11 @@ void TextCtrl::BUILD() {
if (m_opt.is_code)
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
if(m_opt.multiline){
temp->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); // only effects wxTextCtrl
temp->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
}
temp->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK));
wxGetApp().UpdateDarkUI(temp);
if (! m_opt.multiline && !wxOSX)
@@ -2176,7 +2179,8 @@ void PrinterAgentChoice::set_value(const std::string& value, bool change_event)
if (match == wxNOT_FOUND)
{
field->SetSelection(wxNOT_FOUND); // nothing shows as selected in the dropdown
field->SetValue(from_u8(value + " (missing)")); // set a value not in the selection (upper display field)
// TRN %1% is the ID of a printer agent that is no longer available
field->SetValue(format_wxstr(_L("%1% (missing)"), value)); // set a value not in the selection (upper display field)
}
else
{
@@ -2505,7 +2509,7 @@ nlohmann::json plugin_overrides_as_json(const std::string& text)
void PluginConfigField::BUILD()
{
m_button = new ::Button(m_parent, _L("Configure"));
m_button = new ::Button(m_parent, _L("Configure") + dots);
// ButtonType::Parameter gives the button the same height as the parameter fields above it.
m_button->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
@@ -2535,7 +2539,7 @@ void PluginConfigField::update_button_label()
const nlohmann::json entries = plugin_overrides_as_json(m_json);
const size_t count = entries.is_array() ? entries.size() : 0;
m_button->SetLabel(count == 0 ? _L("Configure")
m_button->SetLabel(count == 0 ? _L("Configure" + dots)
: wxString::Format(_L("Configure (%d)"), int(count)));
}
+1 -1
View File
@@ -340,7 +340,7 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent,
bottom_sizer->Add(dlg_btns, 0, wxEXPAND);
}
main_sizer->Add(bottom_panel, 0, wxEXPAND);
main_sizer->Add(bottom_panel, 0, wxEXPAND | wxTOP, FromDIP(15));
m_ok_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_ok, this);
m_cancel_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_cancel, this);
+14 -9
View File
@@ -454,7 +454,8 @@ void FilamentMapManualPanel::UpdateNozzleCountDisplay()
if (m_right_panel->IsUseSeparation()) {
int standard_count = getExtruderNozzleCount(preset_bundle, 1, NozzleVolumeType::nvtStandard);
int highflow_count = getExtruderNozzleCount(preset_bundle, 1, NozzleVolumeType::nvtHighFlow);
wxString right_title = _L("Right Nozzle") + wxString::Format("(Std: %d, HF: %d)", standard_count, highflow_count);
// TRN Nozzle counts: Std = standard flow, HF = high flow
wxString right_title = _L("Right Nozzle") + wxString::Format(_L("(Std: %d, HF: %d)"), standard_count, highflow_count);
m_right_panel->UpdateLabel(right_title);
} else {
int right_count = getExtruderNozzleCountTotal(preset_bundle, 1);
@@ -524,22 +525,26 @@ GUI::FilamentMapBtnPanel::FilamentMapBtnPanel(wxWindow *parent, const wxString &
m_btn = new wxBitmapButton(this, wxID_ANY, icon_enabled, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
m_btn->SetBackgroundStyle(wxBG_STYLE_PAINT);
auto icon_sizer = new wxBoxSizer(wxVERTICAL);
icon_sizer->Add(m_btn , 0, wxLEFT, horizontal_margin);
m_label = new wxStaticText(this, wxID_ANY, label);
m_label->SetFont(Label::Head_14);
m_label->SetForegroundColour(TextNormalBlackColor);
auto label_sizer = new wxBoxSizer(wxHORIZONTAL);
label_sizer->AddStretchSpacer();
label_sizer->Add(m_btn, 0, wxEXPAND | wxLEFT, FromDIP(1));
label_sizer->Add(m_label, 0, wxEXPAND| wxALL, FromDIP(3));
label_sizer->AddStretchSpacer();
auto label_sizer = new wxBoxSizer(wxVERTICAL);
label_sizer->Add(m_label, 0, wxLEFT, horizontal_margin);
m_disable_tip = new Label(this, _L("(Sync with printer)"));
m_disable_tip->SetFont(Label::Body_12);
label_sizer->AddSpacer(FromDIP(2));
label_sizer->Add(m_disable_tip, 0, wxLEFT, horizontal_margin);
sizer->AddSpacer(FromDIP(32));
sizer->AddSpacer(FromDIP(15));
sizer->Add(icon_sizer, 0, wxEXPAND);
sizer->AddSpacer(FromDIP(10));
sizer->Add(label_sizer, 0, wxEXPAND);
sizer->Add(m_disable_tip, 0, wxALIGN_CENTER);
sizer->AddSpacer(FromDIP(3));
sizer->AddSpacer(FromDIP(6));
auto detail_sizer = new wxBoxSizer(wxHORIZONTAL);
m_detail = new Label(this, detail);
+1 -1
View File
@@ -209,7 +209,7 @@ FileArchiveDialog::FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* ar
m_avc = new ArchiveViewCtrl(this, wxSize(45 * em, 30 * em));
wxDataViewColumn* toggle_column = m_avc->AppendToggleColumn(L"\u2714", 0, wxDATAVIEW_CELL_ACTIVATABLE, 6 * em);
m_avc->AppendTextColumn("filename", 1);
m_avc->AppendTextColumn(_L("filename"), 1);
std::vector<std::shared_ptr<ArchiveViewNode>> stack;
+8 -1
View File
@@ -678,7 +678,14 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(9.f, 1.f) * m_scale);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, 0.f));
ImVec4 scroll_col = ImVec4(0.77f, 0.77f, 0.77f, m_is_dark ? .6f : 1.0f); // same color with sliced plates toolbar scrollbar
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f)); // ORCA using background color with opacity creates a second color. This prevents secondary color
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, scroll_col);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, scroll_col);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, scroll_col);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered , style.Colors[ImGuiCol_TableHeaderBg]);
const int hover_id = m_actual_speed_imgui_widget.plot("##ActualSpeedProfile", { -1.f, plot_height});
const ImGuiTableFlags table_flags = ImGuiTableFlags_Borders | (needs_scroll ? ImGuiTableFlags_ScrollY : 0);
if (ImGui::BeginTable("ToolPositionTable", 2, table_flags, ImVec2(0.0f, needs_scroll ? table_view_h : 0.0f))) {
@@ -706,7 +713,7 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
ImGui::EndTable();
}
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(1);
ImGui::PopStyleColor(5);
imgui.end();
}
+83 -115
View File
@@ -1457,7 +1457,7 @@ void GLCanvas3D::on_change_color_mode(bool is_dark, bool reinit) {
// set dirty to re-generate icon texture
m_separator_toolbar.set_icon_dirty();
m_main_toolbar.set_icon_dirty();
wxGetApp().plater()->get_collapse_toolbar().set_icon_dirty();
collapse_toolbar().set_icon_dirty();
m_assemble_view_toolbar.set_icon_dirty();
m_gizmos.set_icon_dirty();
}
@@ -1989,6 +1989,12 @@ void GLCanvas3D::enable_collapse_toolbar(bool enable)
m_collapse_toolbar_enabled = enable;
}
void GLCanvas3D::set_collapse_toolbar(GLToolbar* toolbar, std::function<CollapseSide()> side)
{
m_collapse_toolbar = toolbar;
m_collapse_side = std::move(side);
}
void GLCanvas3D::enable_plate_chrome(bool enable)
{
m_plate_chrome_enabled = enable;
@@ -2069,24 +2075,33 @@ void GLCanvas3D::update_volumes_colors_by_extruder()
bool GLCanvas3D::is_collapse_toolbar_on_left() const
{
auto state = wxGetApp().plater()->get_sidebar_docking_state();
return state == Sidebar::Left;
return collapse_side() == CollapseSide::Left;
}
float GLCanvas3D::get_collapse_toolbar_width() const
{
GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
const auto state = wxGetApp().plater()->get_sidebar_docking_state();
return state != Sidebar::None ? collapse_toolbar.get_width() : 0;
return collapse_side() != CollapseSide::None ? collapse_toolbar().get_width() : 0;
}
float GLCanvas3D::get_collapse_toolbar_height() const
{
GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
const auto state = wxGetApp().plater()->get_sidebar_docking_state();
return collapse_side() != CollapseSide::None ? collapse_toolbar().get_height() : 0;
}
return state != Sidebar::None ? collapse_toolbar.get_height() : 0;
GLToolbar& GLCanvas3D::collapse_toolbar() const
{
return m_collapse_toolbar != nullptr ? *m_collapse_toolbar : wxGetApp().plater()->get_collapse_toolbar();
}
CollapseSide GLCanvas3D::collapse_side() const
{
if (m_collapse_side)
return m_collapse_side();
switch (wxGetApp().plater()->get_sidebar_docking_state()) {
case Sidebar::Left: return CollapseSide::Left;
case Sidebar::Right: return CollapseSide::Right;
default: return CollapseSide::None;
}
}
bool GLCanvas3D::make_current_for_postinit() {
@@ -2291,7 +2306,7 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
tooltip = m_assemble_view_toolbar.get_tooltip();
if (tooltip.empty())
tooltip = wxGetApp().plater()->get_collapse_toolbar().get_tooltip();
tooltip = collapse_toolbar().get_tooltip();
// BBS
#if 0
@@ -3523,7 +3538,7 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
m_overlay_dirty |= m_assemble_view_toolbar.update_items_state();
// BBS
//m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state();
m_overlay_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state();
m_overlay_dirty |= collapse_toolbar().update_items_state();
// apply() DRAINS the 3D-mouse queue, so only the canvas actually on screen may call it: a
// hidden canvas renders nothing, so the motion it swallowed moves the shared camera without
// ever being drawn and the next visible frame jumps several states at once.
@@ -3599,68 +3614,10 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
return;
}
#ifdef SLIC3R_CAD
const int ctrlMask = wxMOD_CONTROL;
const int shiftMask = wxMOD_SHIFT;
#endif
// Design tab: Delete/Backspace removes the selected sketch entities while a
// sketch tool is active and the canvas has focus (dialog text fields are separate
// wx controls, so this never eats their editing keys).
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& (keyCode == WXK_DELETE || keyCode == WXK_BACK)
&& !m_design_sketch_tool->selection().empty()) {
m_design_sketch_tool->delete_selected();
m_dirty = true;
render();
return;
}
#endif
// Esc exits the active sketch tool (Onshape-like, layered: abort in-progress entity ->
// drop to Select -> exit the session back to Feature mode).
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& keyCode == WXK_ESCAPE) {
m_design_sketch_tool->request_exit();
m_dirty = true;
render();
return;
}
#endif
// Design tab: Ctrl+Z / Ctrl+Shift+Z (and Ctrl+Y) undo/redo the Design feature
// history. Scoped by m_design_sketch_tool — only the Design canvas owns one — so the
// main 3D editor's undo/redo (the CanvasView3D-gated cases further below) is untouched.
// Handled here, before the generic Ctrl block, so it takes precedence and early-returns.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && (evt.GetModifiers() & ctrlMask) != 0) {
const bool is_z = (keyCode == 'z' || keyCode == 'Z' || keyCode == WXK_CONTROL_Z);
const bool is_y = (keyCode == 'y' || keyCode == 'Y' || keyCode == WXK_CONTROL_Y);
if (is_z || is_y) {
const bool redo = is_y || ((evt.GetModifiers() & shiftMask) != 0);
m_design_sketch_tool->request_undo_redo(redo);
m_dirty = true;
render();
return;
}
}
#endif
// Design tab: F = Place on Face (Prepare's lay-flat), when the Design viewport is up
// and a body face is selected. The tool forwards to DesignPanel::place_on_face; it returns
// false (no face picked) so F falls through to the default handler below.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()
&& (keyCode == 'f' || keyCode == 'F') && (evt.GetModifiers() & ctrlMask) == 0) {
if (m_design_sketch_tool->request_place_on_face()) {
m_dirty = true;
render();
return;
}
}
#endif
// Design tab: Delete, Esc, Ctrl+Z/Y and F are NOT handled here. DesignPanel's CHAR_HOOK owns
// them (one route per key, whatever holds focus) and only lets a key through to this canvas
// when an in-canvas value field is open -- which is exactly when Backspace must edit the
// number rather than delete the geometry it measures.
//BBS: add orient deactivate logic
if (keyCode == WXK_ESCAPE
@@ -3948,19 +3905,6 @@ static void key_released(int key) { s_keys_down.erase(key); }
void GLCanvas3D::on_key(wxKeyEvent& evt)
{
// Design tab: Delete/Backspace removes selected sketch entities. GTK delivers
// these as KEY_DOWN rather than CHAR, so handle it here too.
#ifdef SLIC3R_CAD
if (evt.GetEventType() == wxEVT_KEY_DOWN
&& m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& (evt.GetKeyCode() == WXK_DELETE || evt.GetKeyCode() == WXK_BACK)
&& !m_design_sketch_tool->selection().empty()) {
m_design_sketch_tool->delete_selected();
m_dirty = true;
render();
return;
}
#endif
const int keyCode = evt.GetKeyCode();
if (evt.GetEventType() == wxEVT_KEY_DOWN)
@@ -4396,6 +4340,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
// (drag/orbit/wheel) so the camera keeps working over the display-only plate.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()) {
// Except over the sidebar collapse button, which is drawn over the sketch.
if (collapse_toolbar().on_mouse(evt, *this)) {
if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
mouse_up_cleanup();
m_mouse.set_start_position_3D_as_invalid();
return;
}
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)) {
@@ -4444,7 +4395,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
return;
}
if (!mouse_in_layer_editing && wxGetApp().plater()->get_collapse_toolbar().on_mouse(evt, *this)) {
if (!mouse_in_layer_editing && collapse_toolbar().on_mouse(evt, *this)) {
if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
mouse_up_cleanup();
m_mouse.set_start_position_3D_as_invalid();
@@ -5116,8 +5067,6 @@ bool GLCanvas3D::is_camera_rotate(const wxMouseEvent& evt, const std::map<MouseB
{
if (m_is_touchpad_navigation) {
return evt.Moving() && evt.AltDown() && !evt.ShiftDown();
} else if (m_cad_navigation) {
return evt.Dragging() && evt.MiddleIsDown(); // left-drag is the selection rubber band
} else {
return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Rotation, mappings);
}
@@ -5127,8 +5076,6 @@ bool GLCanvas3D::is_camera_pan(const wxMouseEvent& evt, const std::map<MouseButt
{
if (m_is_touchpad_navigation) {
return evt.Moving() && evt.ShiftDown() && !evt.AltDown();
} else if (m_cad_navigation) {
return evt.Dragging() && evt.RightIsDown(); // middle now orbits, so pan is right only
} else {
return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Pan, mappings);
;
@@ -6413,9 +6360,9 @@ void GLCanvas3D::_render_3d_navigator()
style.Colors[ImGuizmo::COLOR::DIRECTION_Z] = ImGuiWrapper::to_ImVec4(ColorRGBA::X());
style.Colors[ImGuizmo::COLOR::TEXT] = m_is_dark ? ImVec4(224 / 255.f, 224 / 255.f, 224 / 255.f, 1.f) : ImVec4(.2f, .2f, .2f, 1.0f);
style.Colors[ImGuizmo::COLOR::FACE] = m_is_dark ? ImVec4(0.23f, 0.23f, 0.23f, 1.f) : ImVec4(0.77f, 0.77f, 0.77f, 1);
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_X], "Y"); // ORCA use uppercase to match text on tranform widgets
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_Y], "Z"); // ORCA use uppercase to match text on tranform widgets
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_Z], "X"); // ORCA use uppercase to match text on tranform widgets
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_X], _u8L_CONTEXT("Y", "Axis").c_str()); // ORCA use uppercase to match text on tranform widgets
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_Y], _u8L_CONTEXT("Z", "Axis").c_str()); // ORCA use uppercase to match text on tranform widgets
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_Z], _u8L_CONTEXT("X", "Axis").c_str()); // ORCA use uppercase to match text on tranform widgets
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_FRONT], _u8L_CONTEXT("Front", "Camera View").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_BACK], _u8L_CONTEXT("Back", "Camera View").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_TOP], _u8L_CONTEXT("Top", "Camera View").c_str());
@@ -7025,7 +6972,7 @@ void GLCanvas3D::_switch_toolbars_icon_filename()
m_main_toolbar.init(background_data);
m_assemble_view_toolbar.init(background_data);
m_separator_toolbar.init(background_data);
wxGetApp().plater()->get_collapse_toolbar().init(background_data);
collapse_toolbar().init(background_data);
// main toolbar
{
@@ -7453,7 +7400,17 @@ bool GLCanvas3D::_init_view_toolbar()
bool GLCanvas3D::_init_collapse_toolbar()
{
return wxGetApp().plater()->init_collapse_toolbar();
if (m_collapse_toolbar == nullptr)
return wxGetApp().plater()->init_collapse_toolbar();
// Its owner laid it out; the background needs the GL context.
BackgroundTexture::Metadata background_data;
background_data.filename = m_is_dark ? "toolbar_background_dark.png" : "toolbar_background.png";
background_data.left = 16;
background_data.top = 16;
background_data.right = 16;
background_data.bottom = 16;
return m_collapse_toolbar->init(background_data);
}
bool GLCanvas3D::_set_current()
@@ -8851,7 +8808,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
else {
m_volumes.set_clipping_plane(m_camera_clipping_plane.get_data());
}
if (m_canvas_type == CanvasAssembleView)
if (m_canvas_type == CanvasAssembleView || !m_sinking_contours_enabled)
m_volumes.set_show_sinking_contours(false);
else
m_volumes.set_show_sinking_contours(!m_gizmos.is_hiding_instances());
@@ -8867,10 +8824,11 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
const bool realistic_mode = _is_realistic_view_enabled();
const bool realistic_phong = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_PHONG);
const std::string shader_name = (realistic_mode && realistic_phong) ? "phong" : "gouraud";
const std::string shader_name = (m_studio_lighting || (realistic_mode && realistic_phong)) ? "phong" : "gouraud";
GLShaderProgram* shader = wxGetApp().get_shader(shader_name);
if (shader == nullptr && shader_name != "gouraud")
shader = wxGetApp().get_shader("gouraud");
const bool studio = m_studio_lighting && shader != nullptr && shader->get_name() == "phong";
ECanvasType canvas_type = this->m_canvas_type;
bool partly_inside_enable = canvas_type == ECanvasType::CanvasAssembleView ? false : true;
// The edited printer's per-extruder printable heights feed the object shader's
@@ -8883,6 +8841,13 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
const bool phong_ssao = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SSAO);
shader->set_uniform("enable_ssao", phong_ssao);
// Set on every use: the program is shared, so a canvas that leaves it unset would inherit
// the last canvas's choice.
shader->set_uniform("lighting_model", studio ? 1 : 0);
if (studio) {
const Transform3d& view = wxGetApp().plater()->get_camera().get_view_matrix();
shader->set_uniform("world_up_eye", Vec3f((view.matrix().block<3, 3>(0, 0) * Vec3d::UnitZ()).cast<float>()));
}
// Object-on-object and self shadows: sample the depth map built in _render_shadow_map_pass().
// shadow_intensity == 0 disables the effect entirely (unchanged behavior when off / unsupported).
@@ -9210,11 +9175,12 @@ void GLCanvas3D::_render_sequential_clearance()
void GLCanvas3D::_check_and_update_toolbar_icon_scale()
{
// Update collapse toolbar
GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
collapse_toolbar.set_enabled(wxGetApp().plater()->get_sidebar_docking_state() != Sidebar::None);
GLToolbar& collapse_toolbar = this->collapse_toolbar();
collapse_toolbar.set_enabled(collapse_side() != CollapseSide::None);
// Don't update a toolbar scale, when we are on a Preview
if (wxGetApp().plater()->is_preview_shown()) {
// Don't update a toolbar scale, when we are on a Preview. Asked of this canvas, not of the
// plater: the Design tab's canvas is on screen while the plater's current view may be Preview.
if (m_canvas_type == CanvasPreview) {
IMSlider *m_layers_slider = get_gcode_viewer().get_layers_slider();
IMSlider *m_moves_slider = get_gcode_viewer().get_moves_slider();
float sc = get_scale();
@@ -9286,7 +9252,9 @@ void GLCanvas3D::_check_and_update_toolbar_icon_scale()
// set minimum scale as a auto scale for the toolbars
float new_scale = std::min(new_h_scale, new_v_scale);
new_scale /= get_scale();
if (fabs(new_scale - scale) > 0.05) // scale is changed by 5% and more
// Only a canvas with toolbars to fit sets the shared size: a collapse button alone (the Design
// tab's canvas) always fits at the largest.
if (items_cnt > collapse_toolbar.get_visible_items_cnt() && fabs(new_scale - scale) > 0.05) // scale is changed by 5% and more
wxGetApp().set_auto_toolbar_icon_scale(new_scale);
}
@@ -9368,7 +9336,7 @@ size_t GLCanvas3D::_overlay_signature(const ImDrawData* draw_data) const
size_t hash = ImGuiWrapper::draw_data_signature(draw_data);
for (size_t state_hash : { m_main_toolbar.get_state_hash(), m_separator_toolbar.get_state_hash(),
m_assemble_view_toolbar.get_state_hash(),
wxGetApp().plater()->get_collapse_toolbar().get_state_hash(),
collapse_toolbar().get_state_hash(),
m_gizmos.get_overlay_state_hash() })
boost::hash_combine(hash, state_hash);
return hash;
@@ -9697,7 +9665,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
int item_count = m_sel_plate_toolbar.m_items.size() + (m_sel_plate_toolbar.show_stats_item ? 1 : 0);
float window_height_calc = (item_count * (button_height + (margin_size + button_margin) * 2.0f) + (item_count - 1) * ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().WindowPadding.y * 2.0f);
bool show_scroll = m_sel_plate_toolbar.is_display_scrollbar && (window_height_calc > window_height_max);
float scrollbar_size = 10.0f * f_scale ;
float scrollbar_size = 14.0f * f_scale;
float window_height = std::min(window_height_calc, window_height_max);
float window_width = button_width + (margin_size + button_margin + ImGui::GetStyle().WindowPadding.x) * 2 + (show_scroll ? scrollbar_size : 0);
@@ -9723,7 +9691,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, show_scroll ? (4.0f * f_scale) : (button_radius + margin_size + frame_padding + ImGui::GetStyle().WindowPadding.x));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, show_scroll ? (6.f * f_scale) : (button_radius + margin_size + frame_padding + ImGui::GetStyle().WindowPadding.x));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, button_radius + margin_size);
imgui.set_next_window_pos(canvas_w * 0 + 5.0f * f_scale, canvas_h * 0 + y_offset, ImGuiCond_Always, 0, 0); // ORCA Add slight gap on left edge so toolbar looks like floating and it creates separation with sidebar
@@ -10202,7 +10170,7 @@ void GLCanvas3D::_render_canvas_toolbar()
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding , ImVec2(4.f, 10.f) * sc);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing , ImVec2(0.f, 8.f ) * sc);
if (ImGui::BeginPopup("CanvasToolbarMenu")) {
if (ImGui::BeginPopup("CanvasToolbarMenu", ImGuiWindowFlags_NoMove)) {
ImGui::PushItemFlag(ImGuiItemFlags_SelectableDontClosePopup, true);
Plater* p = wxGetApp().plater();
@@ -10275,6 +10243,7 @@ void GLCanvas3D::_render_canvas_toolbar()
[this, p]{p->toggle_show_wireframe(); m_dirty = true;}
);
// TRN View toggle that draws models see-through, so hidden parts show.
create_menu_item( _utf8(L("X-Ray")),
m_canvas_type != ECanvasType::CanvasPreview, // not work on preview
p->is_show_xray(),
@@ -10438,18 +10407,17 @@ void GLCanvas3D::_render_collapse_toolbar() const
if (!m_collapse_toolbar_enabled)
return;
auto& plater = *wxGetApp().plater();
const auto sidebar_docking_dir = plater.get_sidebar_docking_state();
if (sidebar_docking_dir == Sidebar::None) {
const CollapseSide side = collapse_side();
if (side == CollapseSide::None) {
return;
}
GLToolbar& collapse_toolbar = plater.get_collapse_toolbar();
GLToolbar& collapse_toolbar = this->collapse_toolbar();
const Size cnv_size = get_canvas_size();
const float top = 0.5f * (float)cnv_size.get_height();
const float left = sidebar_docking_dir == Sidebar::Right ? 0.5f * (float) cnv_size.get_width() - (float) collapse_toolbar.get_width() :
-0.5f * (float) cnv_size.get_width();
const float left = side == CollapseSide::Right ? 0.5f * (float) cnv_size.get_width() - (float) collapse_toolbar.get_width() :
-0.5f * (float) cnv_size.get_width();
collapse_toolbar.set_position(top, left);
collapse_toolbar.render(*this);
@@ -11849,7 +11817,7 @@ bool GLCanvas3D::_deactivate_layersediting_menu()
bool GLCanvas3D::_deactivate_collapse_toolbar_items()
{
GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
GLToolbar& collapse_toolbar = this->collapse_toolbar();
if (collapse_toolbar.is_item_pressed("print")) {
collapse_toolbar.force_left_action(collapse_toolbar.get_item_id("print"), *this);
return true;
+18 -6
View File
@@ -18,6 +18,7 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/Line.hpp"
#include <stddef.h>
#include <functional>
#include <memory>
#include <chrono>
#include <cstdint>
@@ -599,6 +600,9 @@ private:
mutable float m_sc{1};
mutable float m_paint_toolbar_width;
bool m_collapse_toolbar_enabled{true};
// The collapse button of a sidebar other than Prepare's, from set_collapse_toolbar().
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.
@@ -606,6 +610,9 @@ private:
// 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};
// Design tab: draw the outline where a volume crosses the bed. GLVolume::SinkingContours slices
// the plater model's mesh by the volume's ids, so a canvas over its own Model must turn it off.
bool m_sinking_contours_enabled{true};
// Design tab: CAD grid drawn on the bed plane in place of the plate's corner-origin grid.
// Two GLModels (10 mm minor / 50 mm major) generated from the bed centre so a line passes
// exactly through the modeling origin; built once and rebuilt only when the bed shape changes.
@@ -622,6 +629,9 @@ private:
//BBS: add canvas type for assemble view usage
ECanvasType m_canvas_type;
// Objects drawn with the phong shader's studio lighting whatever the realistic-view settings
// (the Design tab's canvas). Off for every canvas of the slicer, which render as before.
bool m_studio_lighting{false};
std::array<ClippingPlane, 2> m_clipping_planes;
ClippingPlane m_camera_clipping_plane;
bool m_use_clipping_planes;
@@ -659,11 +669,6 @@ private:
std::array<unsigned int, 2> m_old_size{ 0, 0 };
bool m_is_touchpad_navigation{ false };
// CAD navigation (Design tab only): left-drag is a selection rubber band, so orbit moves
// to middle-drag and pan to right-drag — the Onshape/SolidWorks mapping. Off everywhere
// else, so Prepare/Preview keep the mouse the user already learned.
bool m_cad_navigation{ false };
// Screen is only refreshed from the OnIdle handler if it is dirty.
bool m_dirty;
// A frame is needed, and only for the overlay.
@@ -866,6 +871,7 @@ public:
void set_context(wxGLContext* context) { m_context = context; }
void set_type(ECanvasType type) { m_canvas_type = type; }
void set_studio_lighting(bool on) { m_studio_lighting = on; }
ECanvasType get_canvas_type() { return m_canvas_type; }
wxGLCanvas* get_wxglcanvas() { return m_canvas; }
@@ -1014,10 +1020,15 @@ public:
void enable_return_toolbar(bool enable);
void enable_separator_toolbar(bool enable);
void enable_collapse_toolbar(bool enable);
// A canvas beside a sidebar other than Prepare's shows that sidebar's collapse button: `toolbar`,
// set up with setup_collapse_toolbar(), on the edge `side` reports. Call before the canvas is
// 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_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; }
@@ -1199,7 +1210,6 @@ public:
bool clicked_button_matches_action(const wxMouseEvent& evt, MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const;
bool is_camera_rotate(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
bool is_camera_pan(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
void set_cad_navigation(bool b) { m_cad_navigation = b; }
Size get_canvas_size() const;
Vec2d get_local_mouse_position() const;
@@ -1381,6 +1391,8 @@ private:
// BBS
//bool _init_view_toolbar();
bool _init_collapse_toolbar();
GLToolbar& collapse_toolbar() const;
CollapseSide collapse_side() const;
bool _set_current();
bool _set_shown_canvas_current();
+23
View File
@@ -13,6 +13,7 @@
#include "libslic3r/Utils.hpp"
#include <cassert>
#include <cstddef>
#include <functional>
#include <vector>
#include <utility>
#include <wx/event.h>
@@ -1675,5 +1676,27 @@ float GLToolbar::get_scaled_icon_size()
return m_layout.icons_size * m_layout.scale;
}
bool setup_collapse_toolbar(GLToolbar& toolbar, std::function<void()> toggle)
{
toolbar.set_layout_type(GLToolbar::Layout::Vertical);
toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right);
toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top);
toolbar.set_border(4.0f);
toolbar.set_separator_size(4);
toolbar.set_gap_size(2);
toolbar.del_all_item();
GLToolbarItem::Data item;
item.name = "collapse_sidebar";
// set collapse svg name
item.icon_filename = "collapse.svg";
item.sprite_id = 0;
item.left.action_callback = std::move(toggle);
return toolbar.add_item(item);
}
} // namespace GUI
} // namespace Slic3r
+8
View File
@@ -457,6 +457,14 @@ private:
bool update_items_enabled_state();
};
// The canvas edge a sidebar collapse button sits on: the side its sidebar is docked on, or none
// while that sidebar floats.
enum class CollapseSide { None, Left, Right };
// Lays `toolbar` out as a sidebar collapse button that runs `toggle`: Prepare's, and those of
// other sidebars (GLCanvas3D::set_collapse_toolbar()).
bool setup_collapse_toolbar(GLToolbar& toolbar, std::function<void()> toggle);
} // namespace GUI
} // namespace Slic3r
+1 -1
View File
@@ -8243,7 +8243,7 @@ bool GUI_App::load_language(wxString language, bool initial)
message += _L("\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n");
#endif
if (initial)
message + "\n\nApplication will close.";
message += "\n\n" + _L("Application will close.");
wxMessageBox(message, _L("Orca Slicer - Switching language failed"), wxOK | wxICON_ERROR);
if (initial)
std::exit(EXIT_FAILURE);
+13 -3
View File
@@ -2664,6 +2664,11 @@ void ObjectList::load_shape_object(const std::string &type_name)
}
void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center)
{
load_mesh_object(std::vector<std::pair<const TriangleMesh*, wxString>>{{&mesh, name}}, name, center);
}
void ObjectList::load_mesh_object(const std::vector<std::pair<const TriangleMesh*, wxString>> &parts, const wxString &name, bool center)
{
// Add mesh to model as a new object
Model& model = wxGetApp().plater()->model();
@@ -2673,14 +2678,19 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name
#endif /* _DEBUG */
std::vector<size_t> object_idxs;
auto bb = mesh.bounding_box();
BoundingBoxf3 bb;
ModelObject* new_object = model.add_object();
new_object->name = into_u8(name);
new_object->add_instance(); // each object should have at least one instance
ModelVolume* new_volume = new_object->add_volume(mesh);
// add_volume() centres each part on its own geometry, so the part offsets carry the
// meshes' placement relative to each other.
for (const auto& [mesh, part_name] : parts) {
bb.merge(mesh->bounding_box());
ModelVolume* new_volume = new_object->add_volume(*mesh);
new_volume->name = into_u8(part_name);
}
new_object->sort_volumes(true);
new_volume->name = into_u8(name);
// set a default extruder value, since user can't add it manually
// BBS
new_object->config.set_key_value("extruder", new ConfigOptionInt(1));
+2
View File
@@ -315,6 +315,8 @@ public:
void load_generic_subobject(const std::string& type_name, const ModelVolumeType type);
void load_shape_object(const std::string &type_name);
void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true);
// One object holding one part per mesh; the parts keep their placement relative to each other.
void load_mesh_object(const std::vector<std::pair<const TriangleMesh*, wxString>> &parts, const wxString &name, bool center = true);
// BBS
void switch_to_object_process();
bool del_object(const int obj_idx, bool refresh_immediately = true);
+11 -5
View File
@@ -2938,10 +2938,10 @@ void ObjectTablePanel::load_data()
m_object_grid->SetColLabelValue(ObjectGridTable::col_printable, _L("Printable"));
m_object_grid->SetColLabelValue(ObjectGridTable::col_printable_reset, "");
m_object_grid->SetColLabelValue(ObjectGridTable::col_plate_index, wxString::Format("%S%S", _L("Plate"), wxString::FromUTF8("\u2191\u2193")));
m_object_grid->SetColLabelValue(ObjectGridTable::col_plate_index, wxString::Format("%S %S", _L("Plate"), wxString::FromUTF8("\u2191\u2193")));
/*m_object_grid->SetColLabelValue(ObjectGridTable::col_assemble_name, L("Module"));*/
m_object_grid->SetColLabelValue(ObjectGridTable::col_name, wxString::Format("%S%S", _L("Name"), wxString::FromUTF8("\u2191\u2193")));
m_object_grid->SetColLabelValue(ObjectGridTable::col_filaments, wxString::Format("%S%S", _L("Filament"), wxString::FromUTF8("\u2191\u2193")));
m_object_grid->SetColLabelValue(ObjectGridTable::col_name, wxString::Format("%S %S", _L("Name"), wxString::FromUTF8("\u2191\u2193")));
m_object_grid->SetColLabelValue(ObjectGridTable::col_filaments, wxString::Format("%S %S", _L("Filament"), wxString::FromUTF8("\u2191\u2193")));
m_object_grid->SetColLabelValue(ObjectGridTable::col_filaments_reset, "");
m_object_grid->SetColLabelValue(ObjectGridTable::col_layer_height, _L("Layer height"));
m_object_grid->SetColLabelValue(ObjectGridTable::col_layer_height_reset, "");
@@ -2956,8 +2956,8 @@ void ObjectTablePanel::load_data()
m_object_grid->SetColLabelValue(ObjectGridTable::col_speed_perimeter, _L("Outer wall speed"));
m_object_grid->SetColLabelValue(ObjectGridTable::col_speed_perimeter_reset, "");
m_object_grid->SetLabelFont(Label::Head_13);
m_object_grid->SetLabelTextColour(StateColor::darkModeColorFor(wxColour("#303A3C")));
m_object_grid->SetLabelBackgroundColour( wxColour("#FFFFFF"));
m_object_grid->SetLabelTextColour(StateColor::darkModeColorFor(wxColour("#363636")));
m_object_grid->SetLabelBackgroundColour( StateColor::darkModeColorFor(wxColour("#D9D9D9")));
#else
m_object_grid->HideColLabels();
#endif
@@ -2996,6 +2996,12 @@ void ObjectTablePanel::load_data()
//m_object_grid->SetSelectionForeground(wxColour(0xDB,0xFD,0xE7));
//m_object_grid->SetSelectionBackground(*wxWHITE);
m_object_grid->SetDefaultCellBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
m_object_grid->SetSelectionBackground(StateColor::darkModeColorFor(wxColour("#BFE1DE"))); // its not fully working since background of control's covers cell
m_object_grid->SetCellHighlightColour(StateColor::darkModeColorFor(wxColour("#009688")));
m_object_grid->SetCellHighlightPenWidth(FromDIP(1));
m_object_grid->SetCellHighlightROPenWidth(FromDIP(1)); // Highlight for read-only cells
for (int col = 0; col < cols; col++)
{
ObjectGridTable::ObjectGridCol* grid_col = m_object_grid_table->get_grid_col(col);
+36
View File
@@ -665,6 +665,42 @@ void RemoveInputBorder(wxWindow* win)
);
#endif
}
void SetGaugeColor(wxGauge* gauge, const wxString& barColor, const wxString& troughColor)
{
GtkWidget* widget = gauge->GetHandle(); // native GtkWidget*
if (!widget) return;
#if GTK_CHECK_VERSION(3, 0, 0)
// GTK3+: use CSS provider
GtkCssProvider* provider = gtk_css_provider_new();
wxString css = wxString::Format(
"progressbar trough { background-color: %s; }"
"progressbar progress { background-color: %s; background-image: none; }",
troughColor,
barColor
);
#if GTK_CHECK_VERSION(4, 0, 0)
// GTK4
gtk_css_provider_load_from_data(provider, css.utf8_str(), -1);
#else
// GTK3
gtk_css_provider_load_from_data(provider, css.utf8_str(), -1, nullptr);
#endif
GtkStyleContext* ctx = gtk_widget_get_style_context(widget);
gtk_style_context_add_provider(
ctx,
GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
);
g_object_unref(provider);
#endif
}
#endif // __WXGTK__
#ifdef __linux__
+5
View File
@@ -37,6 +37,10 @@
#include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#ifdef __WXGTK__
#include "wx/gauge.h"
#endif
class wxCheckBox;
class wxTopLevelWindow;
@@ -495,6 +499,7 @@ void set_window_corner_radius(wxWindow* win, int radius);
#ifdef __WXGTK__
void RemoveButtonBorder(wxWindow* win); // for wxButton/wxBitmapToggleButton based controls (SwitchButton, CheckBox)
void RemoveInputBorder(wxWindow* win); // for TextCtrl based controls (TextInput, ComboBox, SpinInput..)
void SetGaugeColor(wxGauge* gauge, const wxString& barColor, const wxString& troughColor);
#endif
#if defined(__WXOSX__) || defined(__linux__)
@@ -1055,7 +1055,7 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
ImGui::PushStyleColor(ImGuiCol_Border , ImGui::ColorConvertFloat4ToU32(ImGuiWrapper::COL_ORCA));
if (ImGui::BeginPopup(pop_id.c_str())) {
if (ImGui::BeginPopup(pop_id.c_str(), ImGuiWindowFlags_NoMove)) {
m_imgui->text(_L("To:"));
+37 -27
View File
@@ -30,7 +30,11 @@ GLGizmoPrimitive::GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_f
bool GLGizmoPrimitive::on_init() { return true; }
std::string GLGizmoPrimitive::on_get_name() const { return _u8L("Primitive"); }
bool GLGizmoPrimitive::on_is_activable() const { return true; }
// Part of the experimental CAD feature: built into every CAD build, but offered in the Prepare
// toolbar only when that feature is switched on in Preferences — switched off, Prepare must be
// exactly what it is without it.
bool GLGizmoPrimitive::on_is_activable() const { return wxGetApp().is_enable_cad_feature(); }
bool GLGizmoPrimitive::on_is_selectable() const { return wxGetApp().is_enable_cad_feature(); }
void GLGizmoPrimitive::on_render() {}
void GLGizmoPrimitive::on_set_state()
{ if (m_state == EState::On) { m_params = PrimitiveParams{}; m_preview_dirty = true; } }
@@ -90,14 +94,16 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoTitleBar);
if (ImGui::CollapsingHeader("Shape", ImGuiTreeNodeFlags_DefaultOpen)) {
static const char* names[] = {"Box", "Cylinder", "Sphere", "Cone", "Torus"};
if (ImGui::CollapsingHeader(_u8L("Shape").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
const std::string names_s[] = {_u8L("Box"), _u8L("Cylinder"), _u8L("Sphere"), _u8L("Cone"), _u8L("Torus")};
const char* names[] = {names_s[0].c_str(), names_s[1].c_str(), names_s[2].c_str(), names_s[3].c_str(), names_s[4].c_str()};
int cur = (int)m_params.type;
if (ImGui::Combo("##type", &cur, names, (int)PrimitiveType::COUNT)) {
m_params.type = (PrimitiveType)cur;
m_preview_dirty = true;
}
ImGui::Text("Quick:");
// TRN Primitive gizmo: label in front of the preset cube size buttons
ImGui::TextUnformatted(_u8L("Quick:").c_str());
ImGui::SameLine();
if (ImGui::SmallButton("10mm")) apply_preset("10mm cube", 10, 10, 10);
ImGui::SameLine();
@@ -108,7 +114,7 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::CollapsingHeader("Dimensions", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::CollapsingHeader(_u8L("Dimensions").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
auto dim = [&](const char* label, double& val, double step=0.5, double fast=5.0) {
ImGui::SetNextItemWidth(130);
if (ImGui::InputDouble(label, &val, step, fast, "%.1f mm")) m_preview_dirty = true;
@@ -116,25 +122,27 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
};
switch (m_params.type) {
case PrimitiveType::Box:
dim("Width (X)", m_params.box_w);
dim("Depth (Y)", m_params.box_d);
dim("Height (Z)", m_params.box_h);
dim(_u8L("Width (X)").c_str(), m_params.box_w);
dim(_u8L("Depth (Y)").c_str(), m_params.box_d);
dim(_u8L("Height (Z)").c_str(), m_params.box_h);
break;
case PrimitiveType::Cylinder:
dim("Radius", m_params.cyl_radius);
dim("Height", m_params.cyl_height);
dim(_u8L("Radius").c_str(), m_params.cyl_radius);
dim(_u8L("Height").c_str(), m_params.cyl_height);
break;
case PrimitiveType::Sphere:
dim("Radius", m_params.sph_radius);
dim(_u8L("Radius").c_str(), m_params.sph_radius);
break;
case PrimitiveType::Cone:
dim("Bottom R", m_params.cone_r1);
dim("Top R", m_params.cone_r2);
dim("Height", m_params.cone_height);
// TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
dim(_u8L("Bottom R").c_str(), m_params.cone_r1);
dim(_u8L("Top R").c_str(), m_params.cone_r2);
dim(_u8L("Height").c_str(), m_params.cone_height);
break;
case PrimitiveType::Torus:
dim("Major R", m_params.torus_r1);
dim("Minor R", m_params.torus_r2, 0.1, 1.0);
// TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
dim(_u8L("Major R").c_str(), m_params.torus_r1);
dim(_u8L("Minor R").c_str(), m_params.torus_r2, 0.1, 1.0);
break;
default: break;
}
@@ -142,26 +150,28 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::CollapsingHeader("Fillet / Chamfer")) {
ImGui::Checkbox("Enable", &m_params.dressup_enabled);
if (ImGui::CollapsingHeader(_u8L("Fillet / Chamfer").c_str())) {
ImGui::Checkbox(_u8L("Enable").c_str(), &m_params.dressup_enabled);
if (m_params.dressup_enabled) {
static const char* dn[] = {"Fillet", "Chamfer"};
const std::string dn_s[] = {_u8L("Fillet"), _u8L("Chamfer")};
const char* dn[] = {dn_s[0].c_str(), dn_s[1].c_str()};
int du = (int)m_params.dressup_type;
ImGui::SetNextItemWidth(100);
if (ImGui::Combo("##dtype", &du, dn, 2)) { m_params.dressup_type = (DressUpType)du; m_preview_dirty = true; }
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
const std::string fn_s[] = {_u8L("All edges"), _u8L("Top edges"), _u8L("Bottom edges"), _u8L("Lateral edges")};
const char* fn[] = {fn_s[0].c_str(), fn_s[1].c_str(), fn_s[2].c_str(), fn_s[3].c_str()};
int fg = (int)m_params.dressup_faces;
ImGui::SetNextItemWidth(140);
if (ImGui::Combo("Edges", &fg, fn, 4)) { m_params.dressup_faces = (FaceGroup)fg; m_preview_dirty = true; }
if (ImGui::Combo(_u8L("Edges").c_str(), &fg, fn, 4)) { m_params.dressup_faces = (FaceGroup)fg; m_preview_dirty = true; }
if (m_params.dressup_type == DressUpType::Fillet) {
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble("Radius", &m_params.dressup_radius, 0.1, 1.0, "%.1f mm")) {
if (ImGui::InputDouble(_u8L("Radius").c_str(), &m_params.dressup_radius, 0.1, 1.0, "%.1f mm")) {
if (m_params.dressup_radius < 0.1) m_params.dressup_radius = 0.1;
m_preview_dirty = true;
}
} else {
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble("Distance", &m_params.dressup_chamfer_dist, 0.1, 1.0, "%.1f mm")) {
if (ImGui::InputDouble(_u8L("Distance").c_str(), &m_params.dressup_chamfer_dist, 0.1, 1.0, "%.1f mm")) {
if (m_params.dressup_chamfer_dist < 0.1) m_params.dressup_chamfer_dist = 0.1;
m_preview_dirty = true;
}
@@ -171,9 +181,9 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::CollapsingHeader("Quality")) {
if (ImGui::CollapsingHeader(_u8L("Quality").c_str())) {
ImGui::SetNextItemWidth(130);
if (ImGui::InputDouble("Mesh resolution", &m_params.linear_deflection, 0.001, 0.1, "%.3f mm")) {
if (ImGui::InputDouble(_u8L("Mesh resolution").c_str(), &m_params.linear_deflection, 0.001, 0.1, "%.3f mm")) {
if (m_params.linear_deflection < 0.001) m_params.linear_deflection = 0.001;
if (m_params.linear_deflection > 1.0) m_params.linear_deflection = 1.0;
m_preview_dirty = true;
@@ -182,10 +192,10 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::Button("Add Shape", {-1, 28}))
if (ImGui::Button(_u8L("Add Shape").c_str(), {-1, 28}))
apply_primitive();
if (ImGui::Button("Close", {-1, 0}))
if (ImGui::Button(_u8L("Close").c_str(), {-1, 0}))
m_parent.reset_all_gizmos();
GizmoImguiEnd();
@@ -24,6 +24,7 @@ protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
bool on_is_selectable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
+5 -5
View File
@@ -1351,25 +1351,25 @@ void GLGizmoSVG::draw_window()
assert(m_volume_id.valid());
if (m_volume == nullptr ||
m_volume_id.invalid()) {
ImGui::Text("Not valid state please report reproduction steps on github");
ImGui::Text("%s", _u8L("Not valid state please report reproduction steps on github").c_str());
return;
}
assert(m_volume->emboss_shape.has_value());
if (!m_volume->emboss_shape.has_value()) {
ImGui::Text("No embossed file");
ImGui::Text("%s", _u8L("No embossed file").c_str());
return;
}
assert(m_volume->emboss_shape->svg_file.has_value());
if (!m_volume->emboss_shape->svg_file.has_value()){
ImGui::Text("Missing svg file in embossed shape");
ImGui::Text("%s", _u8L("Missing svg file in embossed shape").c_str());
return;
}
assert(m_volume->emboss_shape->svg_file->file_data != nullptr);
if (m_volume->emboss_shape->svg_file->file_data == nullptr){
ImGui::Text("Missing data of svg file");
ImGui::Text("%s", _u8L("Missing data of svg file").c_str());
return;
}
@@ -2143,7 +2143,7 @@ std::string volume_name(const EmbossShape &shape)
std::string file_name = get_file_name(shape.svg_file->path);
if (!file_name.empty())
return file_name;
return "SVG shape";
return _u8L("SVG shape");
}
CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager& raycaster, ModelVolumeType volume_type)
+62 -51
View File
@@ -3,6 +3,7 @@
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/GUI/format.hpp"
#include "libslic3r/Model.hpp"
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepPrimAPI_MakeRevol.hxx>
@@ -34,8 +35,6 @@
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Selection.hpp"
#define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str()
namespace Slic3r {
namespace GUI {
@@ -44,7 +43,11 @@ GLGizmoSketch::GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filenam
bool GLGizmoSketch::on_init() { return true; }
std::string GLGizmoSketch::on_get_name() const { return _u8L("Sketch"); }
bool GLGizmoSketch::on_is_activable() const { return true; }
// Part of the experimental CAD feature: built into every CAD build, but offered in the Prepare
// toolbar only when that feature is switched on in Preferences — switched off, Prepare must be
// exactly what it is without it.
bool GLGizmoSketch::on_is_activable() const { return wxGetApp().is_enable_cad_feature(); }
bool GLGizmoSketch::on_is_selectable() const { return wxGetApp().is_enable_cad_feature(); }
void GLGizmoSketch::on_render() {}
void GLGizmoSketch::on_set_state() { if (m_state == EState::On) clear_all(); }
bool GLGizmoSketch::on_mouse(const wxMouseEvent&) { return false; }
@@ -229,11 +232,11 @@ void GLGizmoSketch::draw_canvas()
TopoDS_Shape GLGizmoSketch::build_combined_shape()
{
if (m_profiles.empty() || !m_profiles[0].closed)
throw std::runtime_error("No outer profile");
throw std::runtime_error(_u8L("No outer profile"));
TopoDS_Wire outer_wire = m_profiles[0].to_occt_wire(m_plane);
BRepBuilderAPI_MakeFace face_maker(outer_wire);
if (!face_maker.IsDone()) throw std::runtime_error("Failed to make outer face");
if (!face_maker.IsDone()) throw std::runtime_error(_u8L("Failed to make outer face"));
for (size_t i = 1; i < m_profiles.size(); ++i) {
if (!m_profiles[i].closed) continue;
@@ -241,7 +244,7 @@ TopoDS_Shape GLGizmoSketch::build_combined_shape()
face_maker.Add(inner);
}
face_maker.Build();
if (!face_maker.IsDone()) throw std::runtime_error("Failed to build face with holes");
if (!face_maker.IsDone()) throw std::runtime_error(_u8L("Failed to build face with holes"));
TopoDS_Face face = face_maker.Face();
@@ -251,7 +254,7 @@ TopoDS_Shape GLGizmoSketch::build_combined_shape()
gp_Dir xd(m_plane.x_axis.x(), m_plane.x_axis.y(), m_plane.x_axis.z());
gp_Ax1 axis(o, xd);
BRepPrimAPI_MakeRevol rev(face, axis, m_sp.revolve_deg * M_PI / 180.0);
if (!rev.IsDone()) throw std::runtime_error("Revolve failed");
if (!rev.IsDone()) throw std::runtime_error(_u8L("Revolve failed"));
shape = rev.Shape();
} else {
shape = SketchEngine::make_extrude_face(face, m_plane, m_sp.extrude_len, m_sp.extrude_sym);
@@ -276,8 +279,9 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoTitleBar);
if (ImGui::CollapsingHeader(UL("Profile"), ImGuiTreeNodeFlags_DefaultOpen)) {
static const char* names[] = {"Line", "Rectangle", "Circle", "Polygon"};
if (ImGui::CollapsingHeader(_u8L("Profile").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
const std::string names_s[] = {_u8L("Line"), _u8L("Rectangle"), _u8L("Circle"), _u8L("Polygon")};
const char* names[] = {names_s[0].c_str(), names_s[1].c_str(), names_s[2].c_str(), names_s[3].c_str()};
int cur = (int)m_tool;
if (ImGui::Combo("##shape", &cur, names, (int)SketchTool::COUNT)) {
m_tool = (SketchTool)cur;
@@ -285,39 +289,41 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
}
ImGui::SameLine();
if (m_imgui->button("+##newprofile")) m_active_profile = -1;
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Start new profile (for holes)"));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Start new profile (for holes)").c_str());
if (m_tool == SketchTool::Rectangle) {
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("W", &m_rect_w,1,10,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Width").c_str(), &m_rect_w,1,10,"%.0f")) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("H", &m_rect_h,1,10,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Height").c_str(), &m_rect_h,1,10,"%.0f")) build_preset_profile();
} else if (m_tool == SketchTool::Circle) {
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_circle_r,1,5,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Radius").c_str(), &m_circle_r,1,5,"%.0f")) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Seg", &m_circle_seg,8,64)) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt(_u8L("Segments").c_str(), &m_circle_seg,8,64)) build_preset_profile();
} else if (m_tool == SketchTool::Polygon) {
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Sides", &m_poly_sides,3,12)) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt(_u8L("Sides").c_str(), &m_poly_sides,3,12)) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_poly_r,1,5,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble((_u8L("Radius") + "##poly").c_str(), &m_poly_r,1,5,"%.0f")) build_preset_profile();
} else {
ImGui::Text("%s", UL("Click on canvas to draw"));
ImGui::Text("%s", _u8L("Click on canvas to draw").c_str());
}
ImGui::Checkbox(UL("Snap to grid"), &m_snap_grid);
ImGui::Checkbox(_u8L("Snap to grid").c_str(), &m_snap_grid);
ImGui::SameLine();
ImGui::SetNextItemWidth(80); ImGui::InputFloat("Step", &m_grid_step, 1, 5, "%.0f mm");
ImGui::SetNextItemWidth(80); ImGui::InputFloat(_u8L("Grid step").c_str(), &m_grid_step, 1, 5, "%.0f mm");
draw_canvas();
if (!m_profiles.empty()) {
ImGui::Text("%s: %zu", UL("Profiles"), m_profiles.size());
ImGui::Text("%s: %zu", _u8L("Profiles").c_str(), m_profiles.size());
for (int i = 0; i < (int)m_profiles.size(); ++i) {
auto& p = m_profiles[i];
ImGui::PushID(i);
bool outer = (i == 0);
ImVec4 col = outer ? ImVec4(0,1,0,1) : ImVec4(1,0.3f,0.3f,1);
const char* label = outer ? "Outer" : "Hole";
ImGui::TextColored(col, "%s %d: %zu pts %s", label, i+1, p.points.size(), p.closed ? "CLOSED" : "");
const std::string label = outer ? _u8L("Outer") : _u8L("Hole");
const std::string state = p.closed ? _u8L("closed") : std::string();
// TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
ImGui::TextColored(col, "%s", GUI::format(_u8L("%1% %2%: %3% points %4%"), label, i + 1, p.points.size(), state).c_str());
ImGui::SameLine();
if (ImGui::SmallButton("X")) delete_profile(i);
ImGui::PopID();
@@ -330,59 +336,64 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
bool is_revolve = false;
bool has_sel = false;
if (ImGui::CollapsingHeader(UL("Operation"), ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::CollapsingHeader(_u8L("Operation").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
static int pi = 0;
if (ImGui::Combo(UL("Plane"), &pi, "XY (Top)\0XZ (Front)\0YZ (Side)\0"))
const std::string plane_names[] = {_u8L("XY (Top)"), _u8L("XZ (Front)"), _u8L("YZ (Side)")};
const char* planes[] = {plane_names[0].c_str(), plane_names[1].c_str(), plane_names[2].c_str()};
if (ImGui::Combo(_u8L("Plane").c_str(), &pi, planes, 3))
m_plane = (pi==0) ? SketchPlane::XY() : (pi==1) ? SketchPlane::XZ() : SketchPlane::YZ();
is_revolve = (m_sp.revolve_deg > 0 && m_sp.revolve_deg < 360);
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble(UL("Revolve deg"), &m_sp.revolve_deg, 15, 90, "%.0f")) {
if (ImGui::InputDouble(_u8L("Revolve deg").c_str(), &m_sp.revolve_deg, 15, 90, "%.0f")) {
if (m_sp.revolve_deg > 360) m_sp.revolve_deg = 360;
if (m_sp.revolve_deg < 0) m_sp.revolve_deg = 0;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Set to 0 for extrude, >0 for revolve"));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set to 0 for extrude, >0 for revolve").c_str());
if (!is_revolve) {
ImGui::SetNextItemWidth(100);
ImGui::InputDouble(UL("Length"), &m_sp.extrude_len, 0.5, 5, "%.1f mm");
ImGui::InputDouble(_u8L("Length").c_str(), &m_sp.extrude_len, 0.5, 5, "%.1f mm");
ImGui::SameLine();
ImGui::Checkbox(UL("Symmetric"), &m_sp.extrude_sym);
ImGui::Checkbox(_u8L("Symmetric").c_str(), &m_sp.extrude_sym);
}
has_sel = !m_parent.get_selection().is_empty();
if (has_sel) {
if (ImGui::Checkbox(UL("Pocket (cut)"), &m_sp.is_pocket))
if (ImGui::Checkbox(_u8L("Pocket (cut)").c_str(), &m_sp.is_pocket))
if (m_sp.is_pocket) m_sp.dressup_enabled = false;
} else m_sp.is_pocket = false;
}
ImGui::Separator();
if (!m_sp.is_pocket && ImGui::CollapsingHeader(UL("Fillet / Chamfer"))) {
ImGui::Checkbox(UL("Enable"), &m_sp.dressup_enabled);
if (!m_sp.is_pocket && ImGui::CollapsingHeader(_u8L("Fillet / Chamfer").c_str())) {
ImGui::Checkbox(_u8L("Enable").c_str(), &m_sp.dressup_enabled);
if (m_sp.dressup_enabled) {
static const char* dn[] = {"Fillet", "Chamfer"};
const std::string dn_s[] = {_u8L("Fillet"), _u8L("Chamfer")};
const char* dn[] = {dn_s[0].c_str(), dn_s[1].c_str()};
int du = (int)m_sp.dressup_type;
ImGui::SetNextItemWidth(100);
if (ImGui::Combo("##dtype", &du, dn, 2)) m_sp.dressup_type = (DressUpType)du;
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
const std::string fn_s[] = {_u8L("All edges"), _u8L("Top edges"), _u8L("Bottom edges"), _u8L("Lateral edges")};
const char* fn[] = {fn_s[0].c_str(), fn_s[1].c_str(), fn_s[2].c_str(), fn_s[3].c_str()};
int fg = (int)m_sp.dressup_faces;
ImGui::SetNextItemWidth(140);
ImGui::Combo(UL("Edges"), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg;
ImGui::Combo(_u8L("Edges").c_str(), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg;
ImGui::SetNextItemWidth(100);
if (m_sp.dressup_type == DressUpType::Fillet)
ImGui::InputDouble(UL("Radius"), &m_sp.dressup_radius, 0.1, 1, "%.1f mm");
ImGui::InputDouble(_u8L("Radius").c_str(), &m_sp.dressup_radius, 0.1, 1, "%.1f mm");
else
ImGui::InputDouble(UL("Distance"), &m_sp.dressup_chamfer_dist, 0.1, 1, "%.1f mm");
ImGui::InputDouble(_u8L("Distance").c_str(), &m_sp.dressup_chamfer_dist, 0.1, 1, "%.1f mm");
}
}
ImGui::Separator();
bool ok = has_closed_profile();
if (ok) ImGui::TextColored({0,1,0,1}, "%zu %s", m_profiles.size(), UL("closed profile(s)"));
else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", UL("Draw a closed profile to enable"));
if (ok) ImGui::TextColored({0,1,0,1}, "%s", wxString::Format(_L_PLURAL("%zu closed profile", "%zu closed profiles",
unsigned(m_profiles.size())), m_profiles.size()).ToUTF8().data());
else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", _u8L("Draw a closed profile to enable").c_str());
auto btn = [&](const char* label, bool enabled) {
if (!enabled) { ImGui::PushItemFlag(ImGuiItemFlags_Disabled,true); ImGui::PushStyleColor(ImGuiCol_Button,{0.25f,0.25f,0.25f,1}); }
@@ -392,14 +403,14 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
};
if (m_sp.is_pocket && has_sel) {
if (btn(_u8L("Pocket (Cut)").c_str(), ok)) apply_pocket();
if (btn(_u8L("Pocket (cut)").c_str(), ok)) apply_pocket();
} else if (is_revolve) {
if (btn(_u8L("Revolve").c_str(), ok)) apply_revolve();
} else {
if (btn(_u8L("Extrude").c_str(), ok)) apply_extrude();
if (btn(_u8L_CONTEXT("Extrude", "Design").c_str(), ok)) apply_extrude();
}
if (ImGui::Button(_u8L("Clear All").c_str(), {-1,0})) clear_all();
if (ImGui::Button(_u8L("Clear all").c_str(), {-1,0})) clear_all();
if (ImGui::Button(_u8L("Close").c_str(), {-1,0})) m_parent.reset_all_gizmos();
GizmoImguiEnd();
@@ -411,7 +422,7 @@ void GLGizmoSketch::apply_extrude()
try {
TopoDS_Shape shape = build_combined_shape();
TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection);
if (mesh.its.indices.empty()) throw std::runtime_error("Empty result");
if (mesh.its.indices.empty()) throw std::runtime_error(_u8L("Empty result"));
wxGetApp().plater()->take_snapshot("Sketch Extrude");
ModelObject* mo = wxGetApp().model().add_object();
mo->name = "Extrusion";
@@ -420,7 +431,7 @@ void GLGizmoSketch::apply_extrude()
wxGetApp().plater()->update();
clear_all();
} catch (const std::exception& e) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Extrude: ")+e.what());
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, GUI::format(_u8L("Extrude: %1%"), e.what()));
}
}
@@ -429,7 +440,7 @@ void GLGizmoSketch::apply_revolve()
try {
TopoDS_Shape shape = build_combined_shape();
TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection);
if (mesh.its.indices.empty()) throw std::runtime_error("Empty result");
if (mesh.its.indices.empty()) throw std::runtime_error(_u8L("Empty result"));
wxGetApp().plater()->take_snapshot("Sketch Revolve");
ModelObject* mo = wxGetApp().model().add_object();
mo->name = "Revolve";
@@ -438,7 +449,7 @@ void GLGizmoSketch::apply_revolve()
wxGetApp().plater()->update();
clear_all();
} catch (const std::exception& e) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Revolve: ")+e.what());
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, GUI::format(_u8L("Revolve: %1%"), e.what()));
}
}
@@ -447,29 +458,29 @@ void GLGizmoSketch::apply_pocket()
try {
Selection& sel = m_parent.get_selection();
int obj_idx = sel.get_object_idx();
if (obj_idx < 0) throw std::runtime_error("No object selected");
if (obj_idx < 0) throw std::runtime_error(_u8L("No object selected"));
ModelObject* mo = wxGetApp().model().objects[obj_idx];
TopoDS_Wire outer = m_profiles[0].to_occt_wire(m_plane);
BRepBuilderAPI_MakeFace fm(outer);
if (!fm.IsDone()) throw std::runtime_error("Face failed");
if (!fm.IsDone()) throw std::runtime_error(_u8L("Face failed"));
for (size_t i = 1; i < m_profiles.size(); ++i)
if (m_profiles[i].closed) fm.Add(m_profiles[i].to_occt_wire(m_plane));
fm.Build();
if (!fm.IsDone()) throw std::runtime_error("Face with holes failed");
if (!fm.IsDone()) throw std::runtime_error(_u8L("Face with holes failed"));
TopoDS_Shape tool = SketchEngine::make_extrude_face(fm.Face(), m_plane, m_sp.extrude_len + 5.0, false);
TriangleMesh tool_mesh = SketchEngine::tessellate(tool, m_sp.linear_deflection);
if (tool_mesh.its.indices.empty()) throw std::runtime_error("Tool mesh empty");
if (tool_mesh.its.indices.empty()) throw std::runtime_error(_u8L("Tool mesh empty"));
wxGetApp().plater()->take_snapshot("Sketch Pocket");
mo->add_volume(std::move(tool_mesh), ModelVolumeType::NEGATIVE_VOLUME)->set_new_unique_id();
mo->ensure_on_bed();
wxGetApp().plater()->update();
clear_all();
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, UL("Pocket added (negative volume)"));
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("Pocket added (negative volume)").c_str());
} catch (const std::exception& e) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Pocket: ")+e.what());
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, GUI::format(_u8L("Pocket: %1%"), e.what()));
}
}
+1
View File
@@ -28,6 +28,7 @@ protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
bool on_is_selectable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
@@ -411,7 +411,7 @@ bool GLGizmoTextureDisplacement::on_init()
m_desc["circle"] = _L("Circle");
m_desc["sphere"] = _L("Sphere");
m_desc["remove_layer"] = _L("Remove");
m_desc["bake"] = _L("Bake");
m_desc["bake"] = _L_CONTEXT("Bake", "Texture Displacement");
return true;
}
@@ -3509,7 +3509,7 @@ void GLGizmoTextureDisplacement::set_layer_texture(TextureDisplacementLayer &lay
void GLGizmoTextureDisplacement::import_custom_texture(TextureDisplacementLayer &layer)
{
const wxString wildcard = "Images (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";
const wxString wildcard = _L("Images (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp");
wxFileDialog dialog(nullptr, _L("Choose a texture image (height map)"), wxEmptyString, wxEmptyString, wildcard,
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dialog.ShowModal() != wxID_OK)
@@ -6001,8 +6001,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards;
// at 0.5 mid-grey is neutral and darker texels cut inwards.
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad);
hover_tip(_u8L("Which grey stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-grey stays put, so darker greys cut in and lighter ones "
hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
"still push out - one image both embosses and engraves.\n\n"
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
"cut can pass through the other side."));
@@ -6045,18 +6045,18 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const bool has_color = decode_height_texture(layer).has_color();
bool color_enabled = layer.color_enabled && has_color;
m_imgui->disabled_begin(!has_color);
if (m_imgui->bbl_checkbox(_L("Colours"), color_enabled)) {
if (m_imgui->bbl_checkbox(_L("Colors"), color_enabled)) {
layer.color_enabled = color_enabled;
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
m_imgui->tooltip(has_color ?
_u8L("Prints the painted area in the texture's colours as well as its "
"relief. Each colour is matched to the nearest of your loaded "
_u8L("Prints the painted area in the texture's colors as well as its "
"relief. Each color is matched to the nearest of your loaded "
"filaments; anything you did not paint keeps the object's own.") :
_u8L("This texture is a grayscale height map, so it has no colours to "
"apply. Import a colour image to use this."),
_u8L("This texture is a grayscale height map, so it has no colors to "
"apply. Import a color image to use this."),
wrap_w);
// The rest of colour belongs to the whole stack, not to this layer, so it only appears once -
@@ -6065,13 +6065,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
TextureDisplacementOptions &opts = mv->texture_displacement_options;
if (m_imgui->bbl_checkbox(_L("Mix filaments"), opts.color_mix_enabled))
m_preview_params_dirty = true;
hover_tip(_u8L("Interleaves two filaments to fake the colours in between, so a handful "
"of filaments can cover a photo or a gradient. An image of flat colours "
hover_tip(_u8L("Interleaves two filaments to fake the colors in between, so a handful "
"of filaments can cover a photo or a gradient. An image of flat colors "
"prints the same either way. Off uses one filament per area."));
if (opts.color_mix_enabled) {
slider_label(_L("Mix by"));
const std::string mix_z = _u8L("Layers");
const std::string mix_xy = _u8L("Surface");
const std::string mix_xy = _u8L_CONTEXT("Surface", "Texture Displacement");
const std::string mix_auto = _u8L("Automatic");
const char *mix_items[] = { mix_z.c_str(), mix_xy.c_str(), mix_auto.c_str() };
int mix_mode = int(opts.color_mix_mode);
@@ -6087,12 +6087,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"any angle but can read as texture rather than as a blend.\n"
"Automatic: layers on upright faces; flat-facing faces take the nearer "
"single filament, since a checkerboard there shows as a pattern."));
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colours from %2% filaments"),
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("Cleans up single stray triangles of the wrong colour, which detail finer "
hover_tip(_u8L("Cleans up single stray triangles of the wrong color, which detail finer "
"than the mesh leaves behind. Raise it if the result looks speckled, "
"lower it if small features are being swallowed."));
}
@@ -6102,7 +6102,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
{
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("%s", _u8L("Mapping").c_str());
ImGui::TextDisabled("%s", _u8L_CONTEXT("Mapping", "Texture Displacement").c_str());
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
struct MappingIcon
@@ -6116,9 +6116,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Triplanar - projects the texture from all three axes at once and blends between them, so a "
"patch wrapping around a sharp edge has no seam.") },
{ "menu_obj_cylinder.svg", _L("Cylindrical"),
_L("Cylindrical - wraps the texture around the painted area's own centre, for round shapes.") },
_L("Cylindrical - wraps the texture around the painted area's own center, for round shapes.") },
{ "menu_obj_sphere.svg", _L("Spherical"),
_L("Spherical - wraps the texture around the painted area's own centre in both directions.") },
_L("Spherical - wraps the texture around the painted area's own center in both directions.") },
{ "texture_displacement_map_unwrap.svg", _L("Unwrap (LSCM)"),
_L("Unwrap - flattens the painted area and maps the texture onto it with as little stretching as "
"possible. The area is cut into pieces at its sharp edges first (see Seam angle), so each "
@@ -6261,9 +6261,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::TextDisabled("%s", _u8L("Blend").c_str());
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
const std::string blend_add = _u8L("Add");
const std::string blend_subtract = _u8L("Subtract");
const std::string blend_multiply = _u8L("Multiply");
const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement");
const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement");
const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement");
const std::string blend_divide = _u8L("Divide");
const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() };
// The first layer has nothing before it to combine with - build_texture_displacement() makes it
@@ -6535,11 +6535,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Only worth showing when a layer is actually colouring: with no colour there is no boundary for it
// to refine and the control would do nothing whatever it is set to.
if (mv != nullptr && any_layer_colors(*mv)) {
if (float_row("##subdiv_color", _L("Colour detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
preview_live();
hover_tip(_u8L("Triangle size where two colours meet. Each triangle prints in one filament, so a "
"colour edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of colour. 0 turns it off."));
hover_tip(_u8L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
"color edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of color. 0 turns it off."));
}
ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str());
@@ -6657,7 +6657,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"on a layer is a different thing: it blurs the image before it is used."));
if (opts.smooth_enabled) {
float percent = opts.smooth_strength * 100.f;
if (float_row("##dispsmooth", _L("Strength"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) {
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) {
opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f);
m_preview_params_dirty = true;
}
@@ -6699,16 +6699,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
hover_tip(_u8L("Bake by moving the vertices the mesh already has, after preparing it (remesh, "
"adaptive subdivision, and a cut along sharp steps in the texture). Keeps the "
"topology, which is what colours need. The default pipeline instead refines, "
"topology, which is what colors need. The default pipeline instead refines, "
"cleans up sliver triangles, displaces and simplifies in one run; nothing needs "
"preparing first, but it does not produce colours yet."));
"preparing first, but it does not produce colors yet."));
}
if (opts.pipeline_v2) {
// -1 is "auto": the row shows the recommendation, greyed; editing it makes it a fixed value.
const bool auto_budget = opts.v2_max_triangles_k < 0;
int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k;
m_imgui->disabled_begin(auto_budget);
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? "%d k (auto)" : "%d k", 0.f)) {
// TRN Slider value: %d is the triangle budget in thousands
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f)) {
opts.v2_max_triangles_k = shown_k;
m_preview_params_dirty = true;
}
@@ -6827,7 +6828,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm;
m_imgui->disabled_begin(busy || auto_res);
if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? "%.2f mm (auto)" : "%.2f mm",
if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? _u8L("%.2f mm (auto)").c_str() : "%.2f mm",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
opts.v2_refine_mm = shown;
m_preview_params_dirty = true;
+7 -6
View File
@@ -10,6 +10,7 @@
#include <boost/log/trivial.hpp>
#include "CloudProvider.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "slic3r/Utils/Http.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/BBLNetworkPlugin.hpp"
@@ -248,10 +249,10 @@ std::shared_ptr<HttpServer::Response> HttpServer::auth_handle_request(const std:
GUI::wxGetApp().CallAfter([] { wxGetApp().ShowUserLogin(false); });
}
const std::string title = login_ok ? "Authentication complete" : "Authentication failed";
const std::string title = login_ok ? _u8L("Authentication complete") : _u8L("Authentication failed");
const std::string message = login_ok
? "You can return to OrcaSlicer. This window will close automatically."
: "Something went wrong. Please return to OrcaSlicer and try again.";
? _u8L("You can return to OrcaSlicer. This window will close automatically.")
: _u8L("Something went wrong. Please return to OrcaSlicer and try again.");
const std::string html =
"<html><head><meta charset=\"utf-8\">"
"<style>body{font-family:Arial,sans-serif;background:#f7f7f7;color:#222;margin:32px;}"
@@ -434,9 +435,9 @@ void HttpServer::ResponseRedirect::write_response(std::stringstream& ssOut)
"<style>body{font-family:Arial,sans-serif;background:#f7f7f7;color:#222;margin:32px;}"
"a.button{display:inline-block;padding:10px 16px;margin-top:12px;background:#0f8bff;color:#fff;text-decoration:none;border-radius:6px;}"
"</style></head><body><div class=\"container\">"
"<h2>Authentication complete</h2>"
"<p>You can return to OrcaSlicer. If your browser does not redirect automatically, use the button below.</p>"
"<a class=\"button\" href=\"" + location_str + "\">Continue</a>"
"<h2>" + _u8L("Authentication complete") + "</h2>"
"<p>" + _u8L("You can return to OrcaSlicer. If your browser does not redirect automatically, use the button below.") + "</p>"
"<a class=\"button\" href=\"" + location_str + "\">" + _u8L("Continue") + "</a>"
"<script>setTimeout(function(){try{window.close();}catch(e){}},1500);</script>"
"</div></body></html>";
ssOut << "HTTP/1.1 302 Found" << std::endl;
+2 -2
View File
@@ -1524,7 +1524,7 @@ void IMSlider::render_add_menu()
ImGui::OpenPopup("slider_add_menu_popup");
m_show_menu = false;
}
if (ImGui::BeginPopup("slider_add_menu_popup")) {
if (ImGui::BeginPopup("slider_add_menu_popup", ImGuiWindowFlags_NoMove)) {
bool menu_item_enable = m_draw_mode != dmSequentialFffPrint;
bool hovered = false;
{
@@ -1579,7 +1579,7 @@ void IMSlider::render_edit_menu(const TickCode& tick)
ImGui::OpenPopup("slider_edit_menu_popup");
m_show_menu = false;
}
if (ImGui::BeginPopup("slider_edit_menu_popup")) {
if (ImGui::BeginPopup("slider_edit_menu_popup", ImGuiWindowFlags_NoMove)) {
switch (tick.type)
{
case CustomGCode::PausePrint:
+1 -1
View File
@@ -608,7 +608,7 @@ void ArrangeJob::check_unprintable()
#endif
if (it->poly.area() < 0.001) {
auto msg = (boost::format(
_utf8("Object %s has zero size and can't be arranged."))
_u8L("Object %s has zero size and can't be arranged."))
% _utf8(it->name)).str();
m_plater->get_notification_manager()->push_notification(NotificationType::BBLPlateInfo,
NotificationManager::NotificationLevel::WarningNotificationLevel, msg);
+6 -6
View File
@@ -318,7 +318,7 @@ void CreateVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) {
if (!::finalize(canceled, eptr, *m_input.base))
return;
if (m_result.its.empty())
return create_message("Can't create empty volume.");
return create_message(_u8L("Can't create empty volume."));
create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, *m_input.base, m_input.gizmo);
}
@@ -376,7 +376,7 @@ void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr)
// only for sure
if (m_result.empty())
return create_message("Can't create empty object.");
return create_message(_u8L("Can't create empty object."));
GUI_App &app = wxGetApp();
Plater *plater = app.plater();
@@ -995,7 +995,7 @@ TriangleMesh create_mesh(DataBase &input, const Fnc& was_canceled, Job::Ctl& ctl
return {};
// only info
ctl.call_on_main_thread([]() {
create_message("It is used default volume for embossed text, try to change text or font to fix it.");
create_message(_u8L("It is used default volume for embossed text, try to change text or font to fix it."));
});
}
@@ -1056,7 +1056,7 @@ void update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3
{
// for sure that some object will be created
if (mesh.its.empty())
return create_message("Empty mesh can't be created.");
return create_message(_u8L("Empty mesh can't be created."));
Plater *plater = wxGetApp().plater();
// Check gizmo is still open otherwise job should be canceled
@@ -1115,10 +1115,10 @@ void create_volume(TriangleMesh &&mesh,
// Parent object for text volume was propably removed.
// Assumption: User know what he does, so text volume is no more needed.
if (obj == nullptr)
return create_message("Bad object to create volume.");
return create_message(_u8L("Bad object to create volume."));
if (mesh.its.empty())
return create_message("Can't create empty volume.");
return create_message(_u8L("Can't create empty volume."));
plater->take_snapshot(_u8L("Add Emboss text Volume"));
+1 -1
View File
@@ -487,7 +487,7 @@ ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut
m_status_colour = m_status->GetForegroundColour();
sizer->Add(m_status, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(20));
auto dlg_btns = new DialogButtons(this, {"Unbind", "OK", "Cancel"}, "", 1 /*left_aligned*/);
auto dlg_btns = new DialogButtons(this, {L("Unbind"), "OK", "Cancel"}, "", 1 /*left_aligned*/);
dlg_btns->GetFIRST()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
m_chord = KeyChord{};
m_conflicts.clear();
+5
View File
@@ -58,6 +58,11 @@ public:
// virtual from the book's ShowWithEffect() only for wxSHOW_EFFECT_NONE, the default.
bool Show(bool show = true) override
{
// Built before this page is shown, as an idle prebuild builds it. On MSW every control
// created or moved inside a shown window re-clips and repaints its shown siblings, so
// building a large panel into a shown page took seconds.
if (show && wxGetTopLevelParent(this)->IsShown())
this->ensure();
const bool changed = wxPanel::Show(show);
if (show) {
// The book shows its first page as it is inserted, before startup has chosen the
+75 -8
View File
@@ -1263,6 +1263,10 @@ void MainFrame::shutdown()
m_plugin_pages.shutdown();
if (m_plater != nullptr)
m_plater->remove_dock_panes();
#ifdef SLIC3R_CAD
if (DesignPanel* design = DesignPanel::if_built())
design->shutdown();
#endif
#ifdef __WXGTK__
// Edge panels are child windows — wxWidgets destroys them automatically.
m_edge_bottom = nullptr;
@@ -1386,6 +1390,23 @@ void MainFrame::show_option(bool show)
}
}
void MainFrame::set_undo_redo_enabled(bool undo, bool redo)
{
#ifndef __APPLE__
m_topbar->EnableUndoRedo(undo, redo);
#else
(void) undo; (void) redo; // macOS has no top bar; Edit asks the tab when it opens
#endif
}
#ifdef SLIC3R_CAD
DesignPanel* MainFrame::shown_design_panel() const
{
DesignPanel* design = DesignPanel::if_built();
return (design != nullptr && m_design_page != nullptr && m_design_page->IsShownOnScreen()) ? design : nullptr;
}
#endif
void MainFrame::init_tabpanel() {
// wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on
// Windows 10 with multiple high resolution displays connected.
@@ -1451,6 +1472,11 @@ void MainFrame::init_tabpanel() {
m_topbar->DisableUndoRedoItems();
}
#endif
#ifdef SLIC3R_CAD
// Design keeps its own history, and the top bar's Undo/Redo drive it while it is shown.
if (m_design_page != nullptr && panel == m_design_page)
DesignPanel::ensure()->update_undo_redo_buttons();
#endif
if (panel)
panel->SetFocus();
@@ -1477,8 +1503,9 @@ void MainFrame::init_tabpanel() {
#ifdef SLIC3R_CAD
// The experimental feature is off by default, and when it is off the page is never
// created, so the tab does not appear at all (the preference takes effect on the next
// start, like the other feature toggles).
if (wxGetApp().is_enable_cad_feature()) {
// start, like the other feature toggles). Nor in the G-code viewer, which has no Design tab to put
// it in — and no business opening a control socket onto one.
if (wxGetApp().is_enable_cad_feature() && wxGetApp().is_editor()) {
// Experimental and heavy enough that building it unasked would cost more than it saves.
m_design_page = new LazyPage<DesignPanel>(this, TAB_ID_DESIGN, -1);
m_lazy_pages.push_back(m_design_page);
@@ -2669,6 +2696,9 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect)
MonitorPanel::when_built([](MonitorPanel& monitor) { monitor.msw_rescale(); });
MultiMachinePage::when_built([](MultiMachinePage& multi_machine) { multi_machine.msw_rescale(); });
CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.msw_rescale(); });
#ifdef SLIC3R_CAD
DesignPanel::when_built([](DesignPanel& design) { design.msw_rescale(); });
#endif
// BBS
#if 0
@@ -2733,6 +2763,9 @@ void MainFrame::on_sys_color_changed()
wxGetApp().plater()->sys_color_changed();
MonitorPanel::when_built([](MonitorPanel& monitor) { monitor.on_sys_color_changed(); });
CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.on_sys_color_changed(); });
#ifdef SLIC3R_CAD
DesignPanel::when_built([](DesignPanel& design) { design.on_sys_color_changed(); });
#endif
// update Tabs
for (auto tab : wxGetApp().tabs_list)
tab->sys_color_changed();
@@ -3071,12 +3104,28 @@ void MainFrame::init_menubar_as_editor()
#ifndef __APPLE__
// BBS undo
append_shortcut_item(editMenu, Shortcut::Undo, true, _L("Undo"),
_L("Undo"), [this](wxCommandEvent&) { m_plater->undo(); },
"menu_undo", nullptr, [this](){return m_plater->can_undo(); }, this);
_L("Undo"), [this](wxCommandEvent&) {
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(false); return; }
#endif
m_plater->undo(); },
"menu_undo", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(false);
#endif
return m_plater->can_undo(); }, this);
// BBS redo
append_shortcut_item(editMenu, Shortcut::Redo, true, _L("Redo"),
_L("Redo"), [this](wxCommandEvent&) { m_plater->redo(); },
"menu_redo", nullptr, [this](){return m_plater->can_redo(); }, this);
_L("Redo"), [this](wxCommandEvent&) {
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(true); return; }
#endif
m_plater->redo(); },
"menu_redo", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(true);
#endif
return m_plater->can_redo(); }, this);
editMenu->AppendSeparator();
// BBS Cut TODO
append_shortcut_item(editMenu, Shortcut::Cut, true, _L("Cut"),
@@ -3123,8 +3172,15 @@ void MainFrame::init_menubar_as_editor()
if (handle_key_event(e)) {
return;
}
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(false); return; }
#endif
m_plater->undo(); },
"", nullptr, [this](){return m_plater->can_undo(); }, this);
"", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(false);
#endif
return m_plater->can_undo(); }, this);
// BBS redo
append_shortcut_item(editMenu, Shortcut::Redo, false, _L("Redo"),
_L("Redo"), [this, handle_key_event](wxCommandEvent&) {
@@ -3135,8 +3191,15 @@ void MainFrame::init_menubar_as_editor()
if (handle_key_event(e)) {
return;
}
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(true); return; }
#endif
m_plater->redo(); },
"", nullptr, [this](){return m_plater->can_redo(); }, this);
"", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(true);
#endif
return m_plater->can_redo(); }, this);
editMenu->AppendSeparator();
// BBS Cut TODO
append_shortcut_item(editMenu, Shortcut::Cut, false, _L("Cut"),
@@ -3332,6 +3395,10 @@ void MainFrame::init_menubar_as_editor()
viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"),
[this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this,
[this]() {
#ifdef SLIC3R_CAD
if (shown_design_panel() != nullptr)
return true;
#endif
return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled();
},
this);
+5
View File
@@ -507,7 +507,12 @@ public:
// through LazyInstance's statics, and show_device() only moves pages in and out of the book.
#ifdef SLIC3R_CAD
LazyPage<DesignPanel>* m_design_page { nullptr };
// The Design panel when its tab is the one on screen, else null. Edit > Undo/Redo act on
// the tab that is shown: its own history when that is Design, the plater's otherwise.
DesignPanel* shown_design_panel() const;
#endif
// The top bar's Undo/Redo, for a tab that keeps its own history (Design).
void set_undo_redo_enabled(bool undo, bool redo);
//BBS: GUI refactor
LazyPage<MonitorPanel>* m_monitor_page{ nullptr };
+1
View File
@@ -34,6 +34,7 @@
#include <boost/nowide/utf8_codecvt.hpp>
#include <map>
#include "libslic3r/Utils.hpp"
#include "libslic3r/Thread.hpp"
#include <cstring>
#include <boost/thread/lock_types.hpp>
#include <memory>
+2 -2
View File
@@ -1676,7 +1676,7 @@ void MixedFilamentDialog::update_ok_button_state()
wxString parts;
for (auto it = type_groups.begin(); it != type_groups.end(); ++it) {
if (!parts.empty())
parts += _L(" and ");
parts += "; ";
wxString slots;
for (size_t j = 0; j < it->second.size(); ++j) {
if (!slots.empty()) slots += ", ";
@@ -1684,7 +1684,7 @@ void MixedFilamentDialog::update_ok_button_state()
}
parts += wxString::Format(_L("Slot %s (%s)"), slots, wxString::FromUTF8(it->first));
}
m_type_mismatch_msg = parts + " " + _L("cannot be mixed. Please select the same filament type.");
m_type_mismatch_msg = wxString::Format(_L("Different filament types cannot be mixed: %s. Please select the same filament type."), parts);
} else {
m_type_mismatch_msg.clear();
}
+2 -1
View File
@@ -493,7 +493,8 @@ MultiMachineManagerPage::MultiMachineManagerPage(wxWindow* parent)
page_num_enter_evt();
});
m_page_num_enter = new Button(m_flipping_panel, _("Go"));
// TRN Button that jumps to the page number typed next to it
m_page_num_enter = new Button(m_flipping_panel, _L("Go"));
m_page_num_enter->SetMinSize(wxSize(FromDIP(25), FromDIP(25)));
m_page_num_enter->SetMaxSize(wxSize(FromDIP(25), FromDIP(25)));
m_page_num_enter->SetBackgroundColor(ctrl_bg);
+1 -1
View File
@@ -1166,7 +1166,7 @@ CloudTaskManagerPage::CloudTaskManagerPage(wxWindow* parent)
page_num_enter_evt();
});
m_page_num_enter = new Button(m_flipping_panel, _("Go"));
m_page_num_enter = new Button(m_flipping_panel, _L("Go"));
m_page_num_enter->SetMinSize(wxSize(FromDIP(25), FromDIP(25)));
m_page_num_enter->SetMaxSize(wxSize(FromDIP(25), FromDIP(25)));
m_page_num_enter->SetBackgroundColor(ctrl_bg);
+3 -3
View File
@@ -128,7 +128,7 @@ void NetworkPluginDownloadDialog::create_missing_plugin_ui()
main_sizer->AddSpacer(15);
auto dlg_btns = new DialogButtons(this,
{"Download and Install", "Skip for Now"},
{L("Download and Install"), L("Skip for Now")},
_L("Download and Install") // Primary button
);
@@ -190,7 +190,7 @@ void NetworkPluginDownloadDialog::create_update_available_ui(const std::string&
main_sizer->AddSpacer(10);
auto dlg_btns = new DialogButtons(this,
{"Update Now", "Remind Later", "Skip Version"},
{L("Update Now"), L("Remind Later"), L("Skip Version")},
_L("Update Now")
);
@@ -319,7 +319,7 @@ NetworkPluginRestartDialog::NetworkPluginRestartDialog(wxWindow* parent)
main_sizer->AddSpacer(15);
auto dlg_btns = new DialogButtons(this,
{"Restart Now", "Restart Later"},
{L("Restart Now"), L("Restart Later")},
_L("Restart Now") // Primary button
);
+5 -4
View File
@@ -273,7 +273,8 @@ void NetworkTestDialog::start_all_job_sequence()
void NetworkTestDialog::start_test_url(TestJob job, wxString name, wxString url)
{
m_in_testing[job] = true;
wxString info = wxString::Format("test %s start...", name);
// TRN %s is the name of the tested website
wxString info = wxString::Format(_L("test %s start..."), name);
update_status(job, info);
@@ -295,16 +296,16 @@ void NetworkTestDialog::start_test_url(TestJob job, wxString name, wxString url)
}
})
.on_ip_resolve([this,name,job](std::string ip) {
wxString ip_report = wxString::Format("test %s ip resolved = %s", name, ip);
wxString ip_report = wxString::Format(_L("test %s ip resolved = %s"), name, ip);
update_status(job, ip_report);
})
.on_error([this,name,job](std::string body, std::string error, unsigned int status) {
wxString info = wxString::Format("status=%u, body=%s, error=%s", status, body, error);
this->update_status(job, wxString::Format("test %s failed", name));
this->update_status(job, wxString::Format(_L("test %s failed"), name));
this->update_status(-1, info);
}).perform_sync();
if (result == 0) {
update_status(job, wxString::Format("test %s ok", name));
update_status(job, wxString::Format(_L("test %s ok"), name));
}
m_in_testing[job] = false;
}
+3 -5
View File
@@ -1272,7 +1272,7 @@ bool NotificationManager::ExportFinishedNotification::on_text_click()
}
void NotificationManager::ExportFinishedNotification::on_eject_click()
{
NotificationData data{ get_data().type, get_data().level , 0, _utf8("Ejecting.") };
NotificationData data{ get_data().type, get_data().level , 0, _u8L("Ejecting.") };
m_eject_pending = true;
m_multiline = false;
update(data);
@@ -1671,9 +1671,7 @@ void NotificationManager::PrintHostUploadNotification::render_bar(ImGuiWrapper&
{
ProgressBarNotification::render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
float uploaded = m_file_size * m_percentage;
std::stringstream stream;
stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "% - " << uploaded << " of " << m_file_size << "MB uploaded";
text = stream.str();
text = into_u8(wxString::Format(_L("%d%% - %.2f of %.2fMB uploaded"), (int)(m_percentage * 100), uploaded, m_file_size));
ImGui::SetCursorPosX(m_left_indentation);
ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4));
break;
@@ -3595,7 +3593,7 @@ void NotificationManager::bbl_show_bed_filament_incompatible_notification(const
wxGetApp().open_browser_with_warning_dialog(bed_filament_compatibility_wiki);
return false;
};
push_notification_data({ NotificationType::BBLBedFilamentIncompatible,NotificationLevel::ErrorNotificationLevel,0,_u8L("Error:") + "\n" + text,"Click for more.",callback }, 0);
push_notification_data({ NotificationType::BBLBedFilamentIncompatible,NotificationLevel::ErrorNotificationLevel,0,_u8L("Error:") + "\n" + text,_u8L("Click for more."),callback }, 0);
}
void NotificationManager::bbl_close_bed_filament_incompatible_notification()
+5 -4
View File
@@ -134,13 +134,13 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent
m_name = "Settings to modified";
}
else if (type == itInstanceRoot) {
m_name = _(_devL("Instances"));
m_name = _L("Instances");
m_extruder = parent->m_extruder;
}
else if (type == itInstance)
{
m_idx = parent->GetChildCount();
m_name = wxString::Format(_(_devL("Instance %d")), m_idx + 1);
m_name = wxString::Format(_L("Instance %d"), m_idx + 1);
m_extruder = parent->GetParent()->m_extruder;
set_icons();
}
@@ -398,10 +398,11 @@ void ObjectDataViewModelNode::SetIdx(const int &idx)
// update name if this node is instance
if (m_type == itInstance) {
if (m_plate_idx > 0) {
m_name = wxString::Format(_(_devL("[P%d]Instance %d")), m_plate_idx, m_idx + 1);
// TRN Object list item: plate number, then instance number
m_name = wxString::Format(_L("[P%d]Instance %d"), m_plate_idx, m_idx + 1);
}
else {
m_name = wxString::Format(_(_devL("Instance %d")), m_idx + 1);
m_name = wxString::Format(_L("Instance %d"), m_idx + 1);
}
}
}
+2 -2
View File
@@ -83,7 +83,7 @@ PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent) :
m_presets = tab->get_presets();
const Preset &sel_preset = m_presets->get_selected_preset();
std::string suffix = _u8L_CONTEXT(L_CONTEXT("Copy", "PresetName"), "PresetName");
std::string preset_name = sel_preset.is_default ? "Untitled" : sel_preset.is_system ? (boost::format(("%1% - %2%")) % sel_preset.name % suffix).str() : sel_preset.name;
std::string preset_name = sel_preset.is_default ? _u8L("Untitled") : sel_preset.is_system ? (boost::format(("%1% - %2%")) % sel_preset.name % suffix).str() : sel_preset.name;
auto input_sizer = new wxBoxSizer(wxVERTICAL);
@@ -306,7 +306,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
result = host->test(msg); // using test with special input because I don't want to create the generate_auth_creds func for every printer
// Prompt user to approve access on the machine.
show_info(this, "API Key created. Go to the physical printer and hit \"authorize\" on the screen, then run \"Test\" again.\n"+msg, "API Key created.");
show_info(this, _L("API Key created. Go to the physical printer and hit \"authorize\" on the screen, then run \"Test\" again.") + "\n" + msg, _L("API Key created."));
}
+65 -130
View File
@@ -191,6 +191,7 @@
#ifdef __WXGTK__
#include "LinuxDisplayBackend.hpp"
#endif
#include "AuiMgr.hpp"
#include "AuiPaneLayout.hpp"
#include "GUI_Utils.hpp"
#include "GUI_Factories.hpp"
@@ -936,7 +937,7 @@ struct Sidebar::priv
// that Orca's sidebar has no counterpart for, so these are parented to p->scrolled.
wxPanel* m_btn_add_mixed_filament{nullptr}; // "+ Add Mixed Filament" full-width button
wxPanel* m_panel_mixed_title{nullptr}; // title row: "Mixed Filament" + add/del buttons
wxStaticText* m_text_mixed_title{nullptr};
StaticLine* m_text_mixed_title{nullptr};
ScalableButton* m_btn_mixed_add{nullptr};
ScalableButton* m_btn_mixed_del{nullptr};
wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows
@@ -3000,6 +3001,7 @@ Sidebar::Sidebar(Plater *parent)
});
// "Variant", not "Nozzle": picking one switches the printer preset (see panel_nozzle_dia).
// TRN Sidebar label of the printer variant (nozzle) selector; picking one switches the printer preset.
p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Variant"), LB_PROPAGATE_MOUSE_EVENT);
p->label_nozzle_title->SetFont(Label::Body_10);
@@ -3032,7 +3034,7 @@ Sidebar::Sidebar(Plater *parent)
p->combo_nozzle_dia->Bind(wxEVT_SET_FOCUS, [nozzle_focus_bg](auto& e) {nozzle_focus_bg(true ); e.Skip();});
p->combo_nozzle_dia->Bind(wxEVT_KILL_FOCUS, [nozzle_focus_bg](auto& e) {nozzle_focus_bg(false); e.Skip();});
p->label_nozzle_type = new Label(p->panel_nozzle_dia, "Brass", LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL);
p->label_nozzle_type = new Label(p->panel_nozzle_dia, _L("Brass"), LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL);
p->label_nozzle_type->SetFont(Label::Body_10);
p->label_nozzle_type->SetMinSize(FromDIP(wxSize(56, -1)));
p->label_nozzle_type->SetMaxSize(FromDIP(wxSize(56, -1)));
@@ -3436,10 +3438,10 @@ Sidebar::Sidebar(Plater *parent)
p->m_panel_mixed_title->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
{
auto* title_sizer = new wxBoxSizer(wxHORIZONTAL);
p->m_text_mixed_title = new wxStaticText(p->m_panel_mixed_title, wxID_ANY, _L("Mixed Filament"));
p->m_text_mixed_title = new StaticLine(p->m_panel_mixed_title, false, _L("Mixed Filament"));
p->m_text_mixed_title->SetFont(::Label::Head_14);
title_sizer->Add(p->m_text_mixed_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin()));
title_sizer->AddStretchSpacer();
p->m_text_mixed_title->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
title_sizer->Add(p->m_text_mixed_title, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin()));
p->m_btn_mixed_del = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_filament");
p->m_btn_mixed_del->SetToolTip(_L("Remove last mixed filament"));
@@ -3579,6 +3581,7 @@ Sidebar::Sidebar(Plater *parent)
search_sizer->Fit(p->m_search_bar);
p->m_object_list = new ObjectList(p->scrolled);
p->m_object_list->SetBackgroundColour(*wxWHITE);
p->m_object_list->Bind(wxCUSTOMEVT_EXIT_SEARCH, [this](wxCommandEvent&) {
#ifdef __WXGTK__
this->p->m_search_item->Enable(true);
@@ -4306,7 +4309,7 @@ void Sidebar::update_mixed_filament_list()
wxWindowUpdateLocker noUpdates(this);
const wxColour mc_bg = StateColor::darkModeColorFor(*wxWHITE);
const wxColour mc_border = StateColor::darkModeColorFor(wxColour("#CECECE"));
const wxColour mc_border = StateColor::darkModeColorFor(wxColour("#DBDBDB")); // same color with filament combo box border
const wxColour mc_text = StateColor::darkModeColorFor(wxColour("#262E30"));
const wxColour mc_dim = StateColor::darkModeColorFor(wxColour("#ACACAC"));
@@ -4420,8 +4423,9 @@ void Sidebar::update_mixed_filament_list()
for (size_t i = 0; i < mixed_indices.size(); ++i) {
size_t cfg_idx = mixed_indices[i];
auto* combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL);
combo_and_btn_sizer->Add(FromDIP(10), 0, 0, 0, 0);
if (i == 0 || i % 2 == 0) // dont add left margin for right column items so all horizontal margins will be equal like on filaments section
combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin()));
// Parse components and ratios from config strings (supports 2-N components)
std::vector<unsigned int> comp_ids;
@@ -4511,10 +4515,12 @@ void Sidebar::update_mixed_filament_list()
dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2,
(sz.GetHeight() - txt_sz.GetHeight()) / 2);
});
combo_and_btn_sizer->Add(grad_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
grad_panel->Bind(wxEVT_LEFT_UP, [this, i](wxMouseEvent&) { edit_mixed_filament(i); }); // ORCA also open edit color dialog with clicking swatch
combo_and_btn_sizer->Add(grad_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box))
} else {
combo_and_btn_sizer->Add(make_swatch_panel(p->m_panel_mixed_content, mix_col, mix_num),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
auto swatch_panel = make_swatch_panel(p->m_panel_mixed_content, mix_col, mix_num);
swatch_panel->Bind(wxEVT_LEFT_UP, [this, i](wxMouseEvent&) { edit_mixed_filament(i); }); // ORCA also open edit color dialog with clicking swatch
combo_and_btn_sizer->Add(swatch_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box))
}
auto* content_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY);
@@ -4738,9 +4744,9 @@ void Sidebar::update_mixed_filament_list()
PopupMenu(&menu);
});
combo_and_btn_sizer->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4));
combo_and_btn_sizer->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box))
combo_and_btn_sizer->Add(FromDIP(16), 0, 0, 0, 0);
combo_and_btn_sizer->Add(FromDIP(SidebarProps::ContentMargin()), 0, 0, 0, 0);
int side = i % 2;
auto* col = (side == 0) ? left_col : right_col;
@@ -7108,28 +7114,6 @@ enum ExportingStatus{
EXPORTING_TO_LOCAL
};
// TODO: listen on dark ui change
class FloatFrame : public wxAuiFloatingFrame
{
public:
FloatFrame(wxWindow* parent, wxAuiManager* ownerMgr, const wxAuiPaneInfo& pane) : wxAuiFloatingFrame(parent, ownerMgr, pane)
{
wxGetApp().UpdateFrameDarkUI(this);
}
};
class AuiMgr : public wxAuiManager
{
public:
AuiMgr() : wxAuiManager(){}
virtual wxAuiFloatingFrame* CreateFloatingFrame(wxWindow* parent, const wxAuiPaneInfo& p) override
{
return new FloatFrame(parent, this, p);
}
};
// Plater / private
struct Plater::priv
{
@@ -7615,7 +7599,6 @@ struct Plater::priv
//BBS: change dark/light mode
void on_change_color_mode(SimpleEvent& evt);
void on_apple_change_color_mode(wxSysColourChangedEvent& evt);
void apply_color_mode();
void on_update_geometry(Vec3dsEvent<2>&);
void on_3dcanvas_mouse_dragging_started(SimpleEvent&);
void on_3dcanvas_mouse_dragging_finished(SimpleEvent&);
@@ -7826,20 +7809,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
{
m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1";
#ifdef __WXGTK__
const bool disable_wayland_floating = Slic3r::GUI::is_running_on_wayland();
#endif
m_aui_mgr.SetManagedWindow(q);
m_aui_mgr.SetDockSizeConstraint(1, 1);
#ifdef __WXGTK__
if (disable_wayland_floating)
m_aui_mgr.SetFlags(m_aui_mgr.GetFlags() & ~wxAUI_MGR_ALLOW_FLOATING);
#endif
//m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_PANE_BORDER_SIZE, 0);
//m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_SASH_SIZE, 2);
m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, 18);
m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_GRADIENT_TYPE, wxAUI_GRADIENT_NONE);
m_aui_mgr.init(q);
this->q->SetFont(Slic3r::GUI::wxGetApp().normal_font());
@@ -7921,13 +7891,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
update();
// Orca: Make sidebar dockable
m_aui_mgr.AddPane(sidebar, wxAuiPaneInfo()
.Name("sidebar")
.Left()
.CloseButton(false)
.TopDockable(false)
.BottomDockable(false)
.BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit())));
m_aui_mgr.AddPane(sidebar, AuiMgr::sidebar_pane_info());
// UV editor pane for GLGizmoTextureDisplacement's LSCM unwrap preview - a resizable/dockable
// sibling of "sidebar"/"main" like everything else registered on this same AUI manager, not a
@@ -7979,8 +7943,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
if (!layout.empty()) {
bool removed_floating_state = false;
#ifdef __WXGTK__
if (disable_wayland_floating)
layout = sanitize_window_layout_for_wayland(layout, &removed_floating_state);
layout = sanitize_window_layout_for_wayland(layout, &removed_floating_state);
#endif
if (!m_aui_mgr.LoadPerspective(layout, false)) {
@@ -8002,12 +7965,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
// Keep tracking the current sidebar size, by storing it using `best_size`, which will be stored
// in the config and re-applied when the app is opened again.
this->sidebar->Bind(wxEVT_IDLE, [&sidebar](wxIdleEvent& e) {
if (sidebar.IsShown() && sidebar.IsDocked() && sidebar.rect.GetWidth() > 0) {
sidebar.BestSize(sidebar.rect.GetWidth(), sidebar.best_size.GetHeight());
}
e.Skip();
});
m_aui_mgr.track_docked_size(this->sidebar);
// Hide sidebar initially, will re-show it after initialization when we got proper window size
sidebar.Hide();
@@ -8231,8 +8189,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership
q->Layout();
apply_color_mode();
set_current_panel(wxGetApp().is_editor() ? static_cast<wxPanel*>(view3D) : static_cast<wxPanel*>(preview));
// updates camera type from .ini file
@@ -8711,18 +8667,7 @@ void Plater::priv::add_dock_pane(wxWindow* window, const std::string& name, cons
info.Show(dock_pane_visible(dock_pane, info));
m_aui_mgr.AddPane(window, info);
// wxAUI does not record a dragged sash in best_size, so track the docked size like the sidebar
// does, for the saved layout.
window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) {
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) {
const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM;
pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(),
horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight());
}
evt.Skip();
});
m_aui_mgr.track_docked_size(window);
m_aui_mgr.Update();
}
@@ -10275,10 +10220,23 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
// a user sees after opening a design they spent an hour on — it reads as "your work is gone"
// when the recipe has in fact just been loaded and the Design tab will rehydrate it. Count a
// recipe that came from THESE files as geometry.
if (tolal_model_count <= 0 && !loaded_cad_recipe && !q->m_exported_file) {
//
// Only where the Design tab can actually show it, though. With the CAD feature switched off
// (or not built) the recipe is still carried through to the next save, but nothing will
// display it, so an empty plate needs saying — and saying why.
bool cad_can_show = false;
#ifdef SLIC3R_CAD
cad_can_show = wxGetApp().is_enable_cad_feature();
#endif
if (tolal_model_count <= 0 && !q->m_exported_file && (!loaded_cad_recipe || !cad_can_show)) {
dlg.Hide();
if (!is_user_cancel) {
MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING);
const wxString text = loaded_cad_recipe
? _L("This project contains a model made in the Design tab and no other geometry. "
"Enable \"CAD feature (experimental)\" in Preferences and restart to see and edit it; "
"it is kept when the project is saved.")
: _L("The file does not contain any geometry data.");
MessageDialog msg(wxGetApp().mainframe, text, _L("Warning"), wxYES | wxICON_WARNING);
if (msg.ShowModal() == wxID_YES) {}
}
}
@@ -13827,7 +13785,7 @@ void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) {
assemble_view->get_canvas3d()->on_change_color_mode(m_is_dark);
}
apply_color_mode();
m_aui_mgr.apply_color_mode();
}
void Plater::priv::on_change_color_mode(SimpleEvent& evt) {
@@ -13838,19 +13796,7 @@ void Plater::priv::on_change_color_mode(SimpleEvent& evt) {
assemble_view->get_canvas3d()->on_change_color_mode(m_is_dark);
if (m_send_to_sdcard_dlg) m_send_to_sdcard_dlg->on_change_color_mode();
apply_color_mode();
}
void Plater::priv::apply_color_mode()
{
const bool is_dark = wxGetApp().dark_mode();
wxColour orca_color = wxColour(59, 68, 70);//wxColour(ColorRGBA::ORCA().r_uchar(), ColorRGBA::ORCA().g_uchar(), ColorRGBA::ORCA().b_uchar());
orca_color = is_dark ? StateColor::darkModeColorFor(orca_color) : StateColor::lightModeColorFor(orca_color);
wxColour sash_color = is_dark ? wxColour(38, 46, 48) : wxColour(206, 206, 206);
m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, sash_color);
m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE);
m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_SASH_COLOUR, sash_color);
m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165));
m_aui_mgr.apply_color_mode();
}
static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) {
@@ -14506,26 +14452,9 @@ bool Plater::priv::init_collapse_toolbar()
if (!collapse_toolbar.init(background_data))
return false;
collapse_toolbar.set_layout_type(GLToolbar::Layout::Vertical);
collapse_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right);
collapse_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top);
collapse_toolbar.set_border(4.0f);
collapse_toolbar.set_separator_size(4);
collapse_toolbar.set_gap_size(2);
collapse_toolbar.del_all_item();
GLToolbarItem::Data item;
item.name = "collapse_sidebar";
// set collapse svg name
item.icon_filename = "collapse.svg";
item.sprite_id = 0;
item.left.action_callback = []() {
wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed());
};
if (!collapse_toolbar.add_item(item))
if (!setup_collapse_toolbar(collapse_toolbar, []() {
wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed());
}))
return false;
// Now "collapse" sidebar to current state. This is done so the tooltip
@@ -16574,7 +16503,7 @@ void Plater::calib_pa(const Calib_Params& params)
return;
}
}
const auto calib_pa_name = wxString::Format(L"Pressure Advance Test");
const auto calib_pa_name = _L("Pressure Advance Test");
new_project(false, false, calib_pa_name);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
@@ -17052,11 +16981,9 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) {
return;
wxString calib_name;
if (is_linear) {
calib_name = L"Orca YOLO Flow Calibration";
if (pass == 2)
calib_name += L" - Perfectionist version";
calib_name = pass == 2 ? _L("Orca YOLO Flow Calibration - Perfectionist version") : _L("Orca YOLO Flow Calibration");
} else
calib_name = wxString::Format(L"Flowrate Test - Pass%d", pass);
calib_name = wxString::Format(_L("Flowrate Test - Pass%d"), pass);
if (new_project(false, false, calib_name) == wxID_CANCEL)
return;
@@ -17098,7 +17025,7 @@ void Plater::calib_temp(const Calib_Params& params) {
constexpr double base_temp_tower_block_height = 10.0;
constexpr int base_temp_tower_temp_step = 5;
const auto calib_temp_name = wxString::Format(L"Nozzle temperature test");
const auto calib_temp_name = _L("Nozzle temperature test");
new_project(false, false, calib_temp_name);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Temp_Tower)
@@ -17375,7 +17302,7 @@ void Plater::_calib_temp_belt_sectioned(const Calib_Params& params, double belt_
void Plater::calib_max_vol_speed(const Calib_Params& params)
{
const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test");
const auto calib_vol_speed_name = _L("Max volumetric speed test");
new_project(false, false, calib_vol_speed_name);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Vol_speed_Tower)
@@ -17456,7 +17383,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params)
void Plater::calib_retraction(const Calib_Params& params)
{
const auto calib_retraction_name = wxString::Format(L"Retraction");
const auto calib_retraction_name = _L("Retraction");
new_project(false, false, calib_retraction_name);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Retraction_tower)
@@ -17519,7 +17446,7 @@ void Plater::calib_retraction(const Calib_Params& params)
void Plater::calib_VFA(const Calib_Params& params)
{
const auto calib_vfa_name = wxString::Format(L"VFA test");
const auto calib_vfa_name = _L("VFA test");
new_project(false, false, calib_vfa_name);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_VFA_Tower)
@@ -17603,7 +17530,7 @@ void Plater::calib_VFA(const Calib_Params& params)
void Plater::calib_input_shaping_freq(const Calib_Params& params)
{
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test");
const auto calib_input_shaping_name = _L("Input shaping Frequency test");
new_project(false, false, calib_input_shaping_name);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Input_shaping_freq)
@@ -17668,7 +17595,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
void Plater::calib_input_shaping_damp(const Calib_Params& params)
{
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test");
const auto calib_input_shaping_name = _L("Input shaping Damping test");
new_project(false, false, calib_input_shaping_name);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Input_shaping_damp)
@@ -17732,7 +17659,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
void Plater::Calib_Cornering(const Calib_Params& params)
{
const auto Calib_Cornering = wxString::Format(L"Cornering test");
const auto Calib_Cornering = _L("Cornering test");
new_project(false, false, Calib_Cornering);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Cornering)
@@ -17932,7 +17859,7 @@ void Plater::load_gcode(const wxString& filename)
if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) {
MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("Does not contain valid G-code."),
wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal();
set_project_filename(DEFAULT_PROJECT_NAME);
set_project_filename(_L(DEFAULT_PROJECT_NAME));
} else {
set_project_filename(filename);
}
@@ -18288,7 +18215,7 @@ ProjectDropDialog::ProjectDropDialog(const std::string &filename)
// Orca: hide the "Don't show again" checkbox, people keeps accidentally checked this then forgot
// wxBoxSizer *m_sizer_left = new wxBoxSizer(wxHORIZONTAL);
//
// auto dont_show_again = create_remember_checkbox(_L("Remember my choice."), this, _L("This option can be changed later in preferences, under 'Load Behaviour'."));
// auto dont_show_again = create_remember_checkbox(_L("Remember my choice."), this, _L("This option can be changed later in preferences, under 'Load Behavior'."));
// m_sizer_left->Add(dont_show_again, 0, wxALL, 5);
//
// m_sizer_bottom->Add(m_sizer_left, 0, wxEXPAND, 5);
@@ -18757,7 +18684,15 @@ bool Plater::is_sidebar_collapsed() const { return p->sidebar_layout.is_collapse
void Plater::collapse_sidebar(bool collapse) { p->collapse_sidebar(collapse); }
Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_sidebar_docking_state(); }
void Plater::reset_window_layout() { p->reset_window_layout(); }
void Plater::reset_window_layout()
{
p->reset_window_layout();
#ifdef SLIC3R_CAD
// The Design tab docks its own sidebar.
if (DesignPanel* design = DesignPanel::if_built())
design->reset_window_layout();
#endif
}
void Plater::add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close)
+2 -1
View File
@@ -17,6 +17,7 @@
#include <atomic>
#include <memory>
#include "slic3r/GUI/Event.hpp"
#include "slic3r/GUI/I18N.hpp"
#include <string>
#include "slic3r/GUI/ObjectDataViewModel.hpp"
#include "slic3r/GUI/SettingsIndex.hpp"
@@ -140,7 +141,7 @@ wxDECLARE_EVENT(EVT_NOTICE_CHILDE_SIZE_CHANGED, SimpleEvent);
wxDECLARE_EVENT(EVT_NOTICE_FULL_SCREEN_CHANGED, IntEvent);
using ColorEvent = Event<wxColour>;
wxDECLARE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent);
const wxString DEFAULT_PROJECT_NAME = "Untitled";
const wxString DEFAULT_PROJECT_NAME = L("Untitled");
class SidebarProps
{
+18 -18
View File
@@ -265,25 +265,25 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
available_actions.can_toggle = !is_loading && (has_local || available_actions.toggle_installs_cloud_plugin);
auto add_action = [&available_actions](const char* id, const char* label, bool enabled = true, bool danger = false) {
available_actions.context_actions.push_back(PluginContextAction{id, label, enabled, danger});
available_actions.context_actions.push_back(PluginContextAction{id, _u8L(label), enabled, danger});
};
// Owned cloud plugins fall through to the local delete: it removes the installed package only.
// Deleting a plugin from the cloud is a plugin hub operation and is never offered here.
if (is_cloud && !is_orphaned && !is_mine) {
add_action("unsubscribe_plugin", "Unsubscribe", true, true);
add_action("unsubscribe_plugin", L("Unsubscribe"), true, true);
} else if (has_local) {
add_action("delete_plugin", "Delete", true, true);
add_action("delete_plugin", L("Delete"), true, true);
}
add_action("open_folder", "Show in folder", has_local);
add_action("open_folder", L("Show in folder"), has_local);
if (!is_orphaned) {
if (is_cloud) {
add_action("reinstall_plugin", "Reinstall");
add_action("reinstall_plugin", L("Reinstall"));
} else {
add_action("reload_plugin", "Reload");
add_action("clear_cache_reload_plugin", "Delete cache and reload");
add_action("reload_plugin", L("Reload"));
add_action("clear_cache_reload_plugin", L("Delete cache and reload"));
}
}
@@ -811,7 +811,7 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
if (!available_actions.can_toggle) {
if (dialog_item.unauthorized && available_actions.toggle_installs_cloud_plugin == false && row_data.has_local_package() == false) {
const std::string install_error = "Unauthorized cloud plugins cannot be installed.";
const std::string install_error = _u8L("Unauthorized cloud plugins cannot be installed.");
manager.set_plugin_error(plugin_key, install_error);
show_status(from_u8(install_error), "warn");
}
@@ -1071,7 +1071,7 @@ void PluginsDialog::run_script_plugin_capability(const std::string& plugin_key,
ExecutionResult result;
auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error;
const std::string normalized_error = plugin_error.empty() ? _u8L("Script plugin failed.") : plugin_error;
if (!manager.set_plugin_error(plugin_key, normalized_error))
BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key;
@@ -1262,34 +1262,34 @@ void PluginsDialog::reload_local_plugin(const std::string& plugin_key, bool clea
if (clear_cache) {
PluginDescriptor descriptor;
if (!manager.try_get_plugin_descriptor(plugin_key, descriptor))
return {false, "Plugin not found."};
return {false, _u8L("Plugin not found.")};
boost::filesystem::path resolved_root;
std::string resolve_error;
if (!resolve_allowed_plugin_root(descriptor, {get_orca_plugins_dir()},
"Refusing to clear a plugin cache outside the local plugin directory.",
_u8L("Refusing to clear a plugin cache outside the local plugin directory."),
resolved_root, resolve_error))
return {false, resolve_error};
cache_dir = resolved_root / "__whl_extracted__";
}
if (!manager.unload_plugin(plugin_key))
return {false, "Failed to unload plugin."};
return {false, _u8L("Failed to unload plugin.")};
if (clear_cache) {
boost::system::error_code ec;
boost::filesystem::remove_all(cache_dir, ec);
if (ec)
return {false, "Failed to clear plugin cache: " + ec.message()};
return {false, GUI::format(_u8L("Failed to clear plugin cache: %1%"), ec.message())};
}
manager.load_plugin(plugin_key, false);
std::string error;
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
return {false, error.empty() ? "Plugin failed to load." : error};
return {false, error.empty() ? _u8L("Plugin failed to load.") : error};
if (!was_loaded && !manager.unload_plugin(plugin_key))
return {false, "Plugin reloaded, but failed to restore the inactive state."};
return {false, _u8L("Plugin reloaded, but failed to restore the inactive state.")};
return {true, {}};
},
@@ -1297,7 +1297,7 @@ void PluginsDialog::reload_local_plugin(const std::string& plugin_key, bool clea
} catch (const std::exception& ex) {
reload_result = {false, ex.what()};
} catch (...) {
reload_result = {false, "Unknown plugin reload error."};
reload_result = {false, _u8L("Unknown plugin reload error.")};
}
if (!reload_result.first) {
@@ -1344,14 +1344,14 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
manager.load_plugin(plugin_key);
std::string error;
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
return {false, error.empty() ? "Plugin failed to load." : error};
return {false, error.empty() ? _u8L("Plugin failed to load.") : error};
return {true, {}};
},
_L("Reloading plugin"), _L("Reloading plugin"));
} catch (const std::exception& ex) {
reload_result = {false, ex.what()};
} catch (...) {
reload_result = {false, "Unknown plugin reload error."};
reload_result = {false, _u8L("Unknown plugin reload error.")};
}
if (!reload_result.first) {
+18 -17
View File
@@ -120,9 +120,10 @@ static DWORD execute_process_winapi(const std::wstring& command_line)
if (!::CreateProcessW(nullptr /* lpApplicationName */, (LPWSTR) command_line.c_str(), nullptr /* lpProcessAttributes */,
nullptr /* lpThreadAttributes */, false /* bInheritHandles */,
CREATE_UNICODE_ENVIRONMENT /* | CREATE_NEW_CONSOLE */ /* dwCreationFlags */, (LPVOID) envstr.c_str(),
nullptr /* lpCurrentDirectory */, &startup_info, &process_info))
throw Slic3r::RuntimeError(std::string("Failed starting the script ") + boost::nowide::narrow(command_line) +
", Win32 error: " + std::to_string(int(::GetLastError())));
nullptr /* lpCurrentDirectory */, &startup_info, &process_info)) {
const int error = int(::GetLastError());
throw Slic3r::RuntimeError(Slic3r::format(_u8L("Failed starting the script %1%, Win32 error: %2%"), boost::nowide::narrow(command_line), error));
}
::WaitForSingleObject(process_info.hProcess, INFINITE);
ULONG rc = 0;
::GetExitCodeProcess(process_info.hProcess, &rc);
@@ -141,14 +142,14 @@ static int run_script(const std::string& script, const std::string& gcode, std::
LPWSTR* szArglist = CommandLineToArgvW(boost::nowide::widen(script).c_str(), &nArgs);
if (szArglist == nullptr || nArgs <= 0) {
// CommandLineToArgvW failed. Maybe the command line escapment is invalid?
throw Slic3r::RuntimeError(std::string("Post processing script ") + script + " on file " + gcode +
" failed. CommandLineToArgvW() refused to parse the command line path.");
throw Slic3r::RuntimeError(Slic3r::format(_u8L("Post processing script %1% on file %2% failed. CommandLineToArgvW() refused to parse the command line path."),
script, gcode));
}
std::wstring command_line;
std::wstring command = szArglist[0];
if (!boost::filesystem::exists(boost::filesystem::path(command)))
throw Slic3r::RuntimeError(std::string("The configured post-processing script does not exist: ") + boost::nowide::narrow(command));
throw Slic3r::RuntimeError(Slic3r::format(_u8L("The configured post-processing script does not exist: %1%"), boost::nowide::narrow(command)));
if (boost::iends_with(command, L".pl")) {
// This is a perl script. Run it through the perl interpreter.
// The current process may be slic3r.exe or slic3r-console.exe.
@@ -159,7 +160,7 @@ static int run_script(const std::string& script, const std::string& gcode, std::
boost::filesystem::path path_perl = path_exe.parent_path() / "perl" / "perl.exe";
if (!boost::filesystem::exists(path_perl)) {
LocalFree(szArglist);
throw Slic3r::RuntimeError(std::string("Perl interpreter ") + path_perl.string() + " does not exist.");
throw Slic3r::RuntimeError(Slic3r::format(_u8L("Perl interpreter %1% does not exist."), path_perl.string()));
}
// Replace it with the current perl interpreter.
quote_argv_winapi(boost::nowide::widen(path_perl.string()), command_line);
@@ -294,14 +295,14 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
exec_result = cap->execute(ctx);
} catch (const std::exception& ex) {
const std::string msg =
(boost::format("Post-processing plugin %1% raised an exception.\nError: %2%") % ref.capability_name % ex.what()).str();
(boost::format(_u8L("Post-processing plugin %1% raised an exception.\nError: %2%")) % ref.capability_name % ex.what()).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
if (exec_result.status == PluginResult::RecoverableError || exec_result.status == PluginResult::FatalError) {
const std::string msg =
(boost::format("Post-processing plugin %1% failed.\nError: %2%") % ref.capability_name % exec_result.message).str();
(boost::format(_u8L("Post-processing plugin %1% failed.\nError: %2%")) % ref.capability_name % exec_result.message).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
@@ -370,7 +371,7 @@ bool run_post_process_scripts(
std::string error_message;
if (copy_file(src_path, path, error_message, false) != SUCCESS)
throw Slic3r::RuntimeError(
Slic3r::format("Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%", src_path,
Slic3r::format(_u8L("Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"), src_path,
error_message));
} else {
// Don't make a copy of the G-code before running the post-processing script.
@@ -390,7 +391,7 @@ bool run_post_process_scripts(
auto gcode_file = boost::filesystem::path(path);
if (!boost::filesystem::exists(gcode_file))
throw Slic3r::RuntimeError(std::string("Post-processor can't find exported gcode file"));
throw Slic3r::RuntimeError(_u8L("Post-processor can't find exported gcode file"));
// Store print configuration into environment variables.
config.setenv_();
@@ -431,11 +432,11 @@ bool run_post_process_scripts(
const int result = run_script(script, gcode_file.string(), std_err);
if (result != 0) {
const std::string msg = std_err.empty() ?
(boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") %
(boost::format(_u8L("Post-processing script %1% on file %2% failed.\nError code: %3%")) %
script % path % result)
.str() :
(boost::format(
"Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") %
_u8L("Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%")) %
script % path % result % std_err)
.str();
BOOST_LOG_TRIVIAL(error) << msg;
@@ -476,19 +477,19 @@ bool run_post_process_scripts(
new_output_name = outpath.string();
} else {
if (!op.is_absolute() || !op.has_filename())
throw Slic3r::RuntimeError("Unable to parse desired new path from output name file");
throw Slic3r::RuntimeError(_u8L("Unable to parse desired new path from output name file"));
}
if (!fs::exists(fs::path(new_output_name).parent_path()))
throw Slic3r::RuntimeError(
Slic3r::format("Output directory does not exist: %1%", fs::path(new_output_name).parent_path().string()));
Slic3r::format(_u8L("Output directory does not exist: %1%"), fs::path(new_output_name).parent_path().string()));
}
BOOST_LOG_TRIVIAL(trace) << "Post-processing script changed the file name from " << output_name << " to "
<< new_output_name;
output_name = new_output_name;
} catch (const std::exception& err) {
throw Slic3r::RuntimeError(Slic3r::format("run_post_process_scripts: Failed reading a file %1% "
"carrying the final name / path of a G-code file: %2%",
throw Slic3r::RuntimeError("run_post_process_scripts: " +
Slic3r::format(_u8L("Failed reading a file %1% carrying the final name / path of a G-code file: %2%"),
path_output_name, err.what()));
}
remove_output_name_file();
+26 -22
View File
@@ -10,7 +10,6 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include <wx/gdicmn.h>
#include <wx/arrstr.h>
#include "slic3r/GUI/Widgets/Label.hpp"
@@ -1716,7 +1715,7 @@ void PreferencesDialog::create_items()
std::vector<wxString> projectLoadSettingsBehaviourOptions = {_L("Load All"), _L("Ask When Relevant"), _L("Always Ask"), _L("Load Geometry Only")};
std::vector<string> projectLoadSettingsConfigOptions = { OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL, OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT, OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY };
auto item_project_load = create_item_combobox(_L("Load behaviour"), _L("Should printer/filament/process settings be loaded when opening a 3MF file?"), SETTING_PROJECT_LOAD_BEHAVIOUR, projectLoadSettingsBehaviourOptions, projectLoadSettingsConfigOptions);
auto item_project_load = create_item_combobox(_L("Load behavior"), _L("Should printer/filament/process settings be loaded when opening a 3MF file?"), SETTING_PROJECT_LOAD_BEHAVIOUR, projectLoadSettingsBehaviourOptions, projectLoadSettingsConfigOptions);
g_sizer->Add(item_project_load);
auto item_backup = create_item_backup(_L("Auto backup"), _L("Backup your project periodically to help with restoring from an occasional crash."));
@@ -1802,6 +1801,7 @@ void PreferencesDialog::create_items()
auto item_speed_dial_recents = create_item_spinctrl(
_L("Recent actions"),
"",
// TRN Unit shown after the number of recent actions, as in "5 actions".
_L("actions"),
_L("How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions."),
SETTING_SPEED_DIAL_RECENT_COUNT,
@@ -1815,13 +1815,6 @@ void PreferencesDialog::create_items()
"parametrically. This feature is experimental and still under development."),
"enable_cad_feature", _L("(Requires restart)"));
g_sizer->Add(item_cad_feature);
auto item_auto_close_sketch_loops = create_item_checkbox(_L("Auto-close sketch loops"),
_L("Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. "
"Off: only exactly coincident endpoints join, so a loop with a tiny gap is "
"shown as open instead of being closed for you."),
"auto_close_sketch_loops");
g_sizer->Add(item_auto_close_sketch_loops);
#endif
#if 0
@@ -1859,7 +1852,7 @@ void PreferencesDialog::create_items()
g_sizer->AddGrowableCol(0, 1);
//// CONTROL > Behaviour
g_sizer->Add(create_item_title(_L("Behaviour")), 1, wxEXPAND);
g_sizer->Add(create_item_title(_L("Behavior")), 1, wxEXPAND);
std::vector<wxString> FlushOptionLabels = {_L("All"),_L("Color"),_L("None")};
std::vector<std::string> FlushOptionValues = { "all","color change","disabled" };
@@ -1908,14 +1901,21 @@ void PreferencesDialog::create_items()
if (wxGetApp().is_enable_cad_feature()) {
auto item_connector_face_glyph = create_item_checkbox(_L("Draw mate connectors as a face"),
_L("In the Design tab, draw a mate connector as a small face instead of the conventional "
"disc with a roll quadrant. A face's orientation is read without being learned. "
"disc with a roll quadrant. A face shows its orientation at a glance, without learning the disc convention. "
"Turn this off for the conventional CAD representation."), "design_connector_face_glyph");
g_sizer->Add(item_connector_face_glyph);
}
// Push the weld preference into the kernel now so toggling it takes effect without
// a restart (the sketch tool also re-pushes on activation, see DesignSketchTool::begin).
Slic3r::set_sketch_auto_close(wxGetApp().is_auto_close_sketch_loops());
// Saved WITH each design (it decides which loops are closed, i.e. what solid a project
// rebuilds into), so it applies to designs started from now on; an open design keeps
// the rule it was made with.
auto item_auto_close_sketch_loops = create_item_checkbox(_L("Auto-close sketch loops"),
_L("Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. "
"Off: only exactly coincident endpoints join, so a loop with a tiny gap is "
"shown as open instead of being closed for you. Saved with each design; "
"applies to designs started after the change."),
"auto_close_sketch_loops");
g_sizer->Add(item_auto_close_sketch_loops);
}
#endif
std::vector<wxString> ButtonDragActions = {_L("None"), _L("Pan"), _L("Rotate")};
@@ -2000,7 +2000,11 @@ void PreferencesDialog::create_items()
);
g_sizer->Add(item_realistic_ssao);
std::vector<wxString> ShadowsLabels = { _L("Off"), _L("Static"), _L("Orbit") };
std::vector<wxString> ShadowsLabels = { _L("Off"),
// TRN Realistic-view shadow mode: the light stays fixed in the scene.
_L("Static"),
// TRN Realistic-view shadow mode: the light turns with the camera.
_L("Orbit") };
std::vector<std::string> ShadowsValues = { "off", "static", "orbit" };
auto item_realistic_shadows = create_item_combobox(
_L("Shadows"),
@@ -2306,18 +2310,18 @@ void PreferencesDialog::create_items()
auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets");
g_sizer->Add(item_show_unsupported);
auto item_plugin_printer_agents = create_item_checkbox(
_L("(Experimental) Use printer agents instead of print hosts"), _L(
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\nWhen disabled, OrcaSlicer uses the legacy print-host behavior."),
"use_printer_agents");
g_sizer->Add(item_plugin_printer_agents);
//// DEVELOPER > Experimental Features
g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND);
auto item_keep_painting = create_item_checkbox(_L("Keep painted feature after mesh change"), _L("Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\nHighly experimental! Slow and may create artifact."), "keep_painting");
g_sizer->Add(item_keep_painting);
auto item_plugin_printer_agents = create_item_checkbox(
_L("Use printer agents instead of print hosts"), _L(
"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\nWhen disabled, OrcaSlicer uses the legacy print-host behavior."),
"use_printer_agents");
g_sizer->Add(item_plugin_printer_agents);
//// DEVELOPER > Storage
g_sizer->Add(create_item_title(_L("Storage")), 1, wxEXPAND);
+2 -1
View File
@@ -2050,7 +2050,8 @@ void CrealityPrintHostSendDialog::init()
wxBitmap* bmp = get_extruder_color_icon(slot.color, "", icon_sz, icon_sz);
wxString label_str;
if (slot.box_id == 0)
label_str = wxString::Format("Ext - %s", slot.type.c_str());
// TRN External spool slot, followed by its filament type
label_str = wxString::Format(_L("Ext - %s"), slot.type.c_str());
else
label_str = wxString::Format("%s - %s", slot.tool_id.substr(1).c_str(), slot.type.c_str());
combo->Append(label_str, bmp ? *bmp : wxNullBitmap);
+1 -1
View File
@@ -276,7 +276,7 @@ private:
if (params.is_discarded())
params = json::object();
const std::string filter = json_string(params, "filter").empty() ? "All files (*.*)|*.*" : json_string(params, "filter");
const std::string filter = json_string(params, "filter").empty() ? _u8L("All files (*.*)|*.*") : json_string(params, "filter");
wxWindow* parent = owner().GetParent();
if (parent == nullptr)

Some files were not shown because too many files have changed in this diff Show More