mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 01:01:57 +00:00
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:
+1011
-380
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -77,6 +77,7 @@ public:
|
||||
|
||||
void EnableUndoRedoItems();
|
||||
void DisableUndoRedoItems();
|
||||
void EnableUndoRedo(bool undo, bool redo);
|
||||
|
||||
void SaveNormalRect();
|
||||
|
||||
|
||||
+247
-282
@@ -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.
|
||||
|
||||
@@ -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>&,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
+1971
-1493
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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 (;;) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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 +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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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:"));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user