Merge main

This commit is contained in:
Lam Wei Lun
2026-09-18 15:59:00 +08:00
2713 changed files with 238542 additions and 115600 deletions
+20
View File
@@ -315,6 +315,26 @@ void AppConfig::set_defaults()
if (get("zoom_to_mouse").empty())
set_bool("zoom_to_mouse", false);
#ifdef SLIC3R_CAD
// Experimental parametric Design tab. Off by default: the tab is not created at all
// until this is turned on, so nothing it builds reaches an unsuspecting user.
if (get("enable_cad_feature").empty())
set_bool("enable_cad_feature", false);
// Auto-weld sketch endpoints within kSketchJoinTol when building closed loops.
// Default ON: it is what the ~90% case wants; OFF makes the kernel demand an exact
// joint. The GUI pushes it into SketchEngine via set_sketch_auto_close().
if (get("auto_close_sketch_loops").empty())
set_bool("auto_close_sketch_loops", true);
// Design tab: draw a mate connector as a face rather than as the abstract disc + roll
// quadrant. Defaults ON — face orientation is hardwired perception, so the roll and the
// verse read without being learned, which no abstract glyph achieves. Turning it off
// restores the conventional CAD representation for users who expect it (x0kd).
if (get("design_connector_face_glyph").empty())
set_bool("design_connector_face_glyph", true);
#endif
//#ifdef SUPPORT_SHOW_HINTS
if (get("show_hints").empty())
set_bool("show_hints", false);
File diff suppressed because it is too large Load Diff
+776
View File
@@ -0,0 +1,776 @@
#ifndef slic3r_CadDocument_hpp_
#define slic3r_CadDocument_hpp_
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp" // FaceGroup
#include "libslic3r/Color.hpp" // ColorRGBA (per-body display colour override)
#include <TopoDS_Shape.hxx>
#include <TopoDS_Wire.hxx>
#include <cereal/cereal.hpp>
#include <cereal/types/vector.hpp>
#include <cereal/types/string.hpp>
#include <map>
#include <cereal/types/map.hpp>
#include <string>
#include <vector>
#include <utility>
namespace Slic3r {
enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut, Mirror, Axis, CoordSys, Helix, Transform, Thicken, Project, DeleteFace, Rib, SurfaceExtrude, SurfaceRevolve, ThickenSurface, SurfaceOffset, SurfaceLoft, SurfaceFill, Mate };
enum class SketchShape { Rectangle, Circle };
enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident };
enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge };
enum class CoordSysType { PointWorld, FaceAndDirection };
enum class BooleanMode { New, Add, Cut, Intersect };
enum class ExtrudeEnd { Blind, Symmetric, TwoSided, ThroughAll, UpToFace, UpToVertex };
// Serialize a TopoDS_Shape to/from a BRep string (declared before CadFeature so its
// inline cereal save()/load() can resolve these non-dependent calls).
std::string brep_to_string(const TopoDS_Shape& s);
TopoDS_Shape brep_from_string(const std::string& d);
struct CadFeature {
CadFeatureType type{CadFeatureType::Sketch};
std::string name;
bool enabled{true};
// Sketch params (centered on the plane origin)
SketchShape shape{SketchShape::Rectangle};
SketchPlane plane{SketchPlane::XY()};
double width{20};
double height{20};
double radius{10};
// Real 2D sketch geometry (Onshape-style). When non-empty this takes
// precedence over the shape/width/height/radius enum path in build_sketch_wire.
SketchProfile profile;
// Onshape-style multi-entity sketch geometry. When non-empty this takes
// precedence over both `profile` and the shape-enum path in build_sketch_wire.
std::vector<SketchEntity> entities;
// 2D geometric constraints on `profile` (point indices). Solved in place.
std::vector<SketchConstraintDef> constraints;
// Onshape-style constraints on `entities` (Fase 4.2). Solved in place against
// entity endpoints. Used when `entities` is non-empty (the legacy `constraints`
// vector applies only to the `profile` path).
std::vector<SketchEntityConstraintDef> entity_constraints;
// Imported rigid 2D art (Text glyphs / SVG vector paths) as filled regions.
// Each region: contour[0] = outer loop, contour[1..] = holes; points in
// plane (u,v) millimetres. Rendered as a sketch overlay and extruded via a
// faces-with-holes path (SketchEngine::make_extrude_regions) — deliberately
// NOT solver entities, so imported art contributes zero DoF and never
// pollutes the constraint solver / DoF readout. When non-empty it takes
// precedence over the entities/profile/shape paths in the Extrude case.
std::vector<std::vector<std::vector<Vec2d>>> imported_regions;
// Imported rigid 3D B-rep solid (STEP). When the feature type is Import this carries
// 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).
TopoDS_Shape imported_solid;
// Non-destructive placement transform for imported_regions (Text/SVG),
// applied at display + extrude time as
// p -> (p.x*import_scale_x + import_offset.x, p.y*import_scale_y + import_offset.y).
// Lets the art be moved / enlarged / stretched (independent X/Y) repeatedly
// without re-vectorising. Identity = no change.
Vec2d import_offset{0, 0};
double import_scale_x{1.0};
double import_scale_y{1.0};
// Text/SVG dropped ONTO a solid face (centred on it): the extrude then defaults to an
// inward Cut (engraving) targeting `import_face_body`. False = free art on a plane.
bool import_on_face{false};
int import_face_body{-1};
// Extrude params
int sketch_ref{-1}; // index into features[] of the consumed sketch
double distance{10};
bool symmetric{false};
BooleanMode mode{BooleanMode::New};
ExtrudeEnd extrude_end{ExtrudeEnd::Blind};
double distance2{0}; // second-side depth for TwoSided
double taper_deg{0}; // draft angle (C4-part2)
bool flip{false}; // reverse the extrude direction (negate plane normal)
int up_to_face{-1}; // target solid-face id for UpToFace (C4-part2)
int extrude_src_face{-1}; // global face id on the current body to extrude as a profile; -1 = use sketch wire
Vec3d up_to_point{0,0,0}; // target for UpToVertex (C4-part2)
// Multi-body target: which body (index into CadDocument::bodies) this feature acts on.
// -1 = auto (last body). A New extrude appends a fresh body; Add/Cut/Intersect, dress-up,
// hole and face-extrude(non-New) mutate bodies[target]; face-extrude reads its source
// face from bodies[target] too. The source-face owner for face-extrude lives here.
int target_body{-1};
// Dress-up params (Fillet/Chamfer) — applied to the current body in order
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
// Hole params (positioned circular cut into the current body)
double hole_diameter{5};
double hole_depth{10};
bool hole_through{true}; // true = symmetric through-cut, ignores hole_depth
double hole_x{0}; // position on the plane (plane u/x axis)
double hole_y{0}; // position on the plane (plane v/y axis)
// Hole standards library (extends the plain bore above).
// hole_style: 0 = simple, 1 = counterbore, 2 = countersink.
int hole_style{0};
double hole_cbore_diameter{0}; // counterbore cylinder diameter (mm), style==1
double hole_cbore_depth{0}; // counterbore depth from the entry face (mm), style==1
double hole_csink_diameter{0}; // countersink major diameter at entry face (mm), style==2
double hole_csink_angle{90}; // countersink included angle (degrees), style==2
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_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_x{0}; // axis position on the plane (u/x axis)
double thread_y{0}; // axis position on the plane (v/y axis)
// Shell params (hollow the current body to a wall thickness, removing one open face)
double shell_thickness{2}; // wall thickness (inward offset)
int shell_face{-1}; // global face id to remove (open the shell); -1 = none
// Draft params (taper a single solid face about a neutral plane = body bbox bottom, pull +Z)
int draft_face{-1}; // global face id to draft; -1 = none
double draft_angle{5}; // draft angle in degrees (signed: + leans the face inward)
// Revolve params (sweep a profile about an in-plane axis through the plane origin).
// Reuses sketch_ref / entities (profile), flip (direction), mode (boolean) and
// 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
// 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
// mode (boolean) and target_body.
int sweep_path_ref{-1}; // index into features[] of the path Sketch
// Loft: build a solid through 2+ closed profile Sketches (loft_profile_refs, in
// order, each on its own plane). loft_ruled=false → smooth sections, true → ruled.
// Reuses mode (boolean) and target_body.
std::vector<int> loft_profile_refs; // ordered indices into features[] of profile Sketches
bool loft_ruled{false};
// Pattern: replicate the target body, copies fused into it. pattern_circular=false
// → linear (pattern_count instances spaced pattern_spacing along plane axis
// pattern_dir: 0=X, 1=Y); true → circular (pattern_count instances over
// pattern_angle° total about the plane normal through the plane origin, so a seed
// offset from the origin orbits the axis). Reuses target_body + plane.
bool pattern_circular{false};
int pattern_count{3}; // total instances incl. the seed (>=1)
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)
// 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
// pattern_curve_entity of sketch pattern_curve_sketch, translated by (P_i - P_0).
int pattern_curve_sketch{-1}; // feature index of the Sketch holding the guide curve
int pattern_curve_entity{-1}; // entity index of the guide curve within that sketch
// Parametric bindings: field-member-name -> expression string. On recompute() each entry
// is evaluated against the document variables and written into the named numeric field
// BEFORE geometry runs. Empty (the common case) means the feature uses its literal fields.
std::map<std::string, std::string> expr;
// 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;
// plane_angle tilts plane_angle° about the base axis plane_axis (0=base X, 1=base Y).
int plane_base{0};
double plane_offset{20};
double plane_angle_tilt{0}; // degrees (named *_tilt to avoid clash w/ revolve)
int plane_axis{0}; // tilt axis: 0 = base X, 1 = base Y
PlaneType plane_type{PlaneType::Offset};
int plane_face_body{-1};
int plane_face{-1};
int plane_face2_body{-1};
int plane_face2{-1};
int plane_edge_body{-1};
int plane_edge{-1};
int plane_edge2_body{-1};
int plane_edge2{-1};
double plane_u_size{60};
double plane_v_size{60};
// Boolean: combine two EXISTING bodies. `mode` reuses BooleanMode (Add = union,
// Cut = subtract tool from target, Intersect = keep overlap; New unused). `target_body`
// is the body that survives (result written back to it); `bool_tool_body` is the other
// operand, consumed (erased) unless `bool_keep_tool`. `bool_tolerance` = OCCT fuzzy value
// (0 = exact). Per-face merge: when both bool_target_face/bool_tool_face are set, the tool
// is first snapped so those two faces are coincident (gap closed within bool_tolerance),
// then the boolean welds them and coplanar faces are unified into one clean face.
int bool_tool_body{-1};
bool bool_keep_tool{false};
double bool_tolerance{0.0};
int bool_target_face{-1}; // global face id on the target body to mate (-1 = none)
int bool_tool_face{-1}; // global face id on the tool body to mate (-1 = none)
// Cut: split one target body with a plane, keeping the upper half, lower half, or both.
// Reuses `plane` for the cut plane and `target_body` for which body is cut.
double cut_offset{0.0}; // offset along the cut-plane normal (mm)
bool cut_flip{false}; // flip the normal => swaps which side is "upper"
bool cut_keep_upper{true}; // keep the +normal half
bool cut_keep_lower{false}; // keep the -normal half (both => split into two bodies)
// Mirror: reflect a body about a plane. Reuses `plane` (mirror plane, as Cut does),
// `target_body` (body to mirror), and `mode` (New = separate mirrored copy,
// Add = fuse the mirror back into the source). mirror_keep_original decides whether
// the source body survives when mode is New.
bool mirror_keep_original{true};
// Datum axis: reference line (no solid). Construction params stored; resolve_datum_axes()
// computes the world-space origin + unit direction on demand.
AxisType axis_type{AxisType::TwoPoints};
Vec3d axis_p1{0, 0, 0};
Vec3d axis_p2{0, 0, 10};
int axis_body{-1};
int axis_face{-1};
int axis_edge{-1};
int axis_plane_a{-1};
int axis_plane_b{-1};
// Datum coordinate system (no solid). Stored as point + two orthonormal axes.
CoordSysType coordsys_type{CoordSysType::PointWorld};
Vec3d coordsys_point{0, 0, 0};
int coordsys_body{-1};
int coordsys_face{-1};
int coordsys_edge{-1};
Vec3d coordsys_x_hint{1, 0, 0};
// Fingerprint of the face this connector was bound to, for drift detection. -1 = not yet
// recorded (an old recipe, or a connector that has never resolved).
//
// Surface TYPE and EDGE COUNT specifically, because they survive every legitimate edit:
// Transform moves the body, Draft tilts the face, a dimension change resizes it, and none
// of those change either value. Centroid, area and normal all fail that test — see the
// issue. The cost is that a slide from one planar 4-edge face to another planar 4-edge face
// is invisible; a detector that never cries wolf is worth more here than a total one.
int coordsys_face_kind{-1}; // GeomAbs_SurfaceType as int
int coordsys_face_edges{-1}; // number of edges bounding the face
// Helix curve params (consumed as a sweep path to build springs/coils/augers).
// Axis = plane normal through plane origin. pitch = axial rise per full turn.
// left_handed flips the winding direction. taper_deg != 0 gives a conical helix.
double helix_radius{10};
double helix_pitch{5};
double helix_height{20};
bool helix_left_handed{false};
double helix_taper_deg{0};
// Transform feature: rigid move/rotate of an existing body. Rotation is applied
// first (about xf_axis through xf_pivot), then the translation.
Vec3d xf_translate{0, 0, 0};
Vec3d xf_axis{0, 0, 1};
Vec3d xf_pivot{0, 0, 0};
double xf_angle_deg{0};
bool xf_copy{false}; // true: keep the original, append the moved copy as a new body
// Thicken feature: offset one face of an existing body into a new thin solid body.
// The face belongs to `target_body`; the offset runs along the face normal.
int thicken_face{-1}; // global face id on the target body; -1 = invalid
double thicken_thickness{2}; // wall thickness (always used as |value|)
bool thicken_flip{false}; // true: offset against the face normal
// Cut-by-face: when cut_face >= 0, apply_cut derives the cut plane from this face
// (via SketchPlane::from_face) instead of the base `plane`. cut_offset / cut_flip
// still apply along the derived normal.
int cut_face_body{-1}; // body owning the face; -1 = the target body
int cut_face{-1}; // global face id to cut along; -1 = use `plane`
// Project feature: convert edges of an existing solid into sketch entities on `plane`.
int project_source_body{-1}; // body owning the edges; -1 = last body
std::vector<int> project_edges; // global edge ids to project; empty => use project_face
int project_face{-1}; // if project_edges empty, project every edge of this face
// Direct edit: faces to remove (global face indices into target_body's shape),
// healed via BRepAlgoAPI_Defeaturing.
std::vector<int> delete_faces;
// Rib: a thin wall grown from an open sketch line, fused to the body.
int rib_sketch_ref{-1}; // feature index of the Sketch holding the profile
int rib_entity{-1}; // index of the open Line entity within that sketch
double rib_thickness{2}; // wall thickness (mm), centred on the line
double rib_depth{10}; // extrude distance along the sketch-plane normal (mm)
// --- Mate (assembly) ---
// 0 Fastened — all 6 DOF fixed: B's frame is driven onto A's exactly.
// 1 Planar — z axes aligned, normal distance set to mate_offset; in-plane position free.
// 2 Revolute — axes collinear, position on the axis fixed; rotation about z free.
// 3 Slider — orientation fully fixed, perpendicular position fixed; axial slide free.
// 4 Cylindrical— axes collinear, perpendicular fixed; both spin and axial slide free.
// A "free" DOF is preserved from the body's current placement, not zeroed.
int mate_kind{0};
int mate_cs_a{-1}; // feature index of the FIXED CoordSys (mate connector A)
int mate_cs_b{-1}; // feature index of the CoordSys on the body that MOVES
double mate_offset{0}; // translation along A's z, mm
double mate_angle{0}; // rotation about A's z, degrees
bool mate_flip{false}; // oppose the two z axes (face-to-face)
template<class Archive>
void save(Archive& ar) const {
std::string brep = (type == CadFeatureType::Import) ? brep_to_string(imported_solid) : std::string();
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);
}
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);
imported_solid = brep_from_string(brep);
}
};
// Serialize a TopoDS_Shape to/from a BRep string for cereal persistence.
std::string brep_to_string(const TopoDS_Shape& s);
TopoDS_Shape brep_from_string(const std::string& d);
// One independent solid in a multi-body document.
struct CadBody {
TopoDS_Shape shape;
std::string name;
// The name the USER gave this body. A body is NOT its first feature: an Extrude, a Cut and
// a Fillet all land on the same body, so renaming `source_feature` renames one operation in
// the history, not the object — which is exactly the bug this field exists to end. `name`
// above is the DERIVED label (the maker's name, restamped every recompute) and stays that;
// this one is set only by a rename, carried across recompute() by body index, and written
// into the recipe so it survives save/load.
bool has_user_name{false};
std::string user_name;
// Per-body display colour override (Color tool). When has_color is false the GUI
// falls back to the auto body-index palette. Carried across recompute() by body index.
bool has_color{false};
ColorRGBA color;
// Index into `features` of the feature that CREATED this body, or -1. A body is a
// recomputed result, so without this there is no way back to its maker and "delete this
// body" cannot be expressed at all — the GUI could only answer "select the FEATURE that
// created this body". Stamped in one place, the recompute loop; see the note there for
// why a single "still unset?" test is sufficient and stays correct for new feature types.
int source_feature{-1};
};
// OCCT-only feature tree backing the Design tab. No GUI dependencies (lives in libslic3r).
class CadDocument {
public:
std::vector<CadFeature> features;
// 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;
// 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;
TopoDS_Shape body; // compound of all bodies (1 body => that body) — display/compat
TriangleMesh display_mesh; // tessellation of all bodies, concatenated (picking)
std::vector<TriangleMesh> display_body_meshes; // one mesh per body, in `bodies` order (per-body color)
std::vector<int> display_tri_face; // per-triangle face id WITHIN its source body
std::vector<int> display_tri_body; // per-triangle source body index (into bodies)
std::string error; // last recompute error ("" = ok)
// Mate diagnostics, refilled by every recompute(). Non-fatal by design: the
// document still evaluates — this only names what the user should look at.
// .first = index of the offending Mate feature, .second = human-readable reason.
// NOT "over-constraint" — this kernel has no solver, so there is no DOF analysis
// behind these; they are graph facts about which mate drives which body.
std::vector<std::pair<int, std::string>> mate_conflicts;
// 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.
Vec3d modeling_origin{Vec3d::Zero()};
// 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
// density as the identical body imported through Prepare. It was 0.01 linear — 3.3x coarser
// than anything else in the app, which is why curved faces read as faceted next to an
// imported part. Angular already matched. Same BRepMesh_IncrementalMesh call, same GLVolume
// path, same shaders: the renderer was never the difference, the mesh fed to it was.
double linear_deflection{0.003};
double angular_deflection{0.5};
int add_sketch(SketchShape shape, const SketchPlane& plane,
double width, double height, double radius,
const std::string& name);
int add_sketch_profile(const SketchProfile& profile, const SketchPlane& plane,
const std::string& name);
// Onshape-style multi-entity sketch: stores the entity list verbatim. When
// non-empty it takes precedence over profile/enum in build_sketch_wire.
int add_sketch_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, const std::string& name,
const std::vector<SketchEntityConstraintDef>& constraints = {});
// Project edges of source_body onto plane, producing a sketch feature whose
// entities are (re)derived on every recompute.
int add_project_edges(int source_body, const std::vector<int>& edge_ids, int face,
const SketchPlane& plane, const std::string& name);
// Onshape's "Use" / SolidWorks' "Convert Entities": project a body's edges onto the plane of
// an EXISTING sketch feature and append them to that sketch as CONSTRUCTION entities, so new
// geometry can be constrained to them. Returns the number of entities appended, or -1 if the
// sketch or body reference is invalid. Unlike add_project_edges this creates no feature: the
// references become part of the sketch that borrows them.
int project_edges_into_sketch(int sketch_feature, int source_body,
const std::vector<int>& edge_ids, int face);
// Append a bridging BSpline entity connecting endpoint `end_a` of entity `ent_a` to
// endpoint `end_b` of entity `ent_b`, both within sketch feature `sketch_ref`. Returns
// the new entity's index within that sketch's entities vector. Non-parametric: computed
// once from the current endpoints (does not auto-follow later solver moves).
int add_bridge(int sketch_ref, int ent_a, int end_a, int ent_b, int end_b,
const std::string& name);
// Solve features[index]'s sketch constraints, writing solved coordinates back
// into its profile.points. No-op (returns true) if the feature has no
// constraints. Returns false if index is invalid / not a Sketch / solve fails.
bool solve_sketch_feature(int index);
int add_extrude(int sketch_ref, double distance, bool symmetric,
BooleanMode mode, const std::string& name);
// Extrude a single loop given directly as entities (sketch_ref = -1, plane carried).
int add_extrude_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, double distance, bool symmetric,
BooleanMode mode, const std::string& name);
// Extrude an existing solid FACE (global face id on the body) as the profile.
int add_extrude_face(int src_face, double distance, bool symmetric,
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_chamfer(double distance, FaceGroup faces, const std::string& name);
int add_chamfer(double distance, int edge_id, const std::string& name);
int add_hole(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane,
const std::string& name);
int add_hole_styled(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane, int style,
double cbore_diameter, double cbore_depth,
double csink_diameter, double csink_angle,
const std::string& standard, const std::string& name);
int add_hole_standard(const std::string& designation, int style, bool through,
double depth, double x, double y,
const SketchPlane& plane, const std::string& name);
int add_thread(double radius, double pitch, double height, double depth,
bool internal, double x, double y, const SketchPlane& plane,
const std::string& name);
int add_revolve(int sketch_ref, double angle, int axis, bool flip,
BooleanMode mode, const std::string& name);
// Self-contained revolve of a single loop given directly as entities (sketch_ref=-1).
int add_revolve_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, double angle, int axis, bool flip,
BooleanMode mode, const std::string& name);
// Sweep the profile Sketch (profile_sketch_ref) along the path Sketch (path_sketch_ref).
int add_pattern(bool circular, int count, double spacing, int dir,
double angle_deg, int target_body, const std::string& name);
// Pattern `count` copies of `target` along entity `curve_entity` of sketch `curve_sketch`.
int add_pattern_on_curve(int count, int curve_sketch, int curve_entity, int target,
const std::string& name);
int add_sweep(int profile_sketch_ref, int path_sketch_ref, BooleanMode mode,
const std::string& name);
// Loft through the ordered profile Sketches (each a closed wire on its own plane).
int add_loft(const std::vector<int>& profile_refs, bool ruled, BooleanMode mode,
const std::string& name);
// Skin 2+ profile sketches open (no end caps) -> a sheet body.
int add_surface_loft(const std::vector<int>& profile_refs, bool ruled, const std::string& name);
// Fill sketch sketch_ref's closed boundary wire with a smooth face -> a one-face sheet body.
int add_surface_fill(int sketch_ref, const std::string& name);
int add_shell(double thickness, int face, int target_body, const std::string& name);
// Grow a thin rib wall (thickness, depth) from the open Line entity `entity` inside sketch
// feature `sketch_ref`, fused to `target_body`. Returns the new feature index.
int add_rib(int sketch_ref, int entity, double thickness, double depth,
int target_body, const std::string& name);
int add_draft(double angle, int face, int target_body, const std::string& name);
// Boolean between two existing bodies. op reuses BooleanMode (Add=union, Cut=subtract,
// Intersect=common; New invalid). target survives, tool is consumed unless keep_tool.
// tolerance = OCCT fuzzy value; target_face/tool_face (-1 = none) drive the per-face snap+merge.
int add_boolean(BooleanMode op, int target_body, int tool_body, bool keep_tool,
double tolerance, int target_face, int tool_face, const std::string& name);
// Plane Cut (Onshape split-by-plane): trim target_body by the plane (origin offset along
// its normal by `offset`, normal flipped iff `flip`). keep_upper/keep_lower select the
// +normal / -normal half; both => the body is split into two coexisting bodies.
int add_cut(const SketchPlane& plane, double offset, bool flip,
bool keep_upper, bool keep_lower, int target_body, const std::string& name);
// Split target_body along the plane of face `face` (owned by face_body, -1 = target).
// keep_upper/keep_lower select which half survives; both => split into two bodies.
int add_split_by_face(int target_body, int face_body, int face,
bool keep_upper, bool keep_lower, const std::string& name);
int add_mirror(const SketchPlane& plane, int target_body, BooleanMode mode,
const std::string& name);
// Rigid body transform: rotate `angle_deg` about `axis` through `pivot`, then translate.
// copy=true keeps the source body and appends the transformed one as a new body.
int add_transform(int target_body, const Vec3d& translate, const Vec3d& axis,
const Vec3d& pivot, double angle_deg, bool copy, const std::string& name);
// Offset face `face` of `target_body` by `thickness` along its normal, producing a new
// thin solid appended as a new body. flip=true offsets against the normal.
int add_thicken(int target_body, int face, double thickness, bool flip, const std::string& name);
// Thicken an entire SHEET body's shell into a solid.
int add_thicken_surface(int target_body, double thickness, bool flip, const std::string& name);
// Offset a SHEET body's shell by a signed distance, producing another SHEET body.
int add_surface_offset(int target_body, double offset, const std::string& name);
int add_delete_face(int target_body, const std::vector<int>& faces,
const std::string& name);
int add_surface_extrude(int sketch_ref, double distance, const std::string& name);
int add_surface_revolve(int sketch_ref, double angle_deg, int axis, const std::string& name);
// Datum plane: derived from base (0=XY/1=XZ/2=YZ/3+N=Nth earlier datum), offset
// along its normal, optional tilt about a base axis. Produces no solid.
int add_plane(int base, double offset, double angle_tilt, int axis,
const std::string& name);
// Datum axis: construction method axis_type determines which ref fields are read.
int add_axis(AxisType axis_type, const std::string& name);
// Datum coordinate system.
int add_coordsys(CoordSysType type, const Vec3d& point, const std::string& name);
int add_mate(int kind, int cs_a, int cs_b, double offset, double angle_deg, bool flip,
const std::string& name);
// Which mate types apply to a connector pair, as reported to the viewport palette.
struct MateOption {
int kind{0}; // 0..4, the five mate types in CadDocument.hpp:308-314
bool viable{true};
std::string reason; // empty when viable; why not, when not
};
// ALWAYS all five entries, ALWAYS in kind order. Never filtered: the caller dims what is
// not viable rather than hiding it, so the list must be stable in length and order between
// calls. Pure query over existing data — records nothing, mutates nothing.
std::vector<MateOption> mate_options(int cs_a, int cs_b) const;
int add_helix(const SketchPlane& plane, double radius, double pitch, double height,
bool left_handed, double taper_deg, const std::string& name);
// Build the helix wire from a Helix feature's params (exposed for tests).
TopoDS_Wire build_helix_wire(const CadFeature& f, std::string& err) const;
// Every datum plane currently in the recipe, in feature order, as (name, plane).
// Used by the GUI to populate plane pickers (after the 3 base planes).
std::vector<std::pair<std::string, SketchPlane>> resolve_datum_planes() const;
// World-space sketch plane lying on a body's PLANAR face, so a face picked in the viewport can
// be sketched on directly — no datum plane in between and nothing to choose from a list.
// Returns false when the indices don't resolve or the face isn't planar (a cylinder or a fillet
// has no single plane, and guessing one from a mid-parameter normal would silently sketch on a
// tangent). Same derivation the Coincident datum method uses, shared so the two cannot drift.
bool plane_of_face(int body_idx, int face_idx, SketchPlane& out) const;
// Resolved datum axes in feature order. axis_err is non-empty if construction failed.
struct DatumAxis { std::string name; Vec3d origin{0,0,0}; Vec3d direction{0,0,1};
std::string error; };
std::vector<DatumAxis> resolve_datum_axes() const;
// Resolved datum coordinate systems. X/Y unit, orthonormal (Z = X.cross(Y)).
struct DatumCoordSys { std::string name; Vec3d origin{0,0,0}; Vec3d x{1,0,0};
Vec3d y{0,1,0}; std::string error; };
std::vector<DatumCoordSys> resolve_datum_coordsys() const;
void clear();
bool recompute(); // replay features -> body + display_mesh; false on error
// CadRecipe serialization contract:
// - v1 blobs are deliberately not loadable; there is no migration path by design
// - append fields ONLY at the end of save/load, never reorder (golden fixture enforces this)
// Bumped every time the bodies are rebuilt, i.e. every time the face and edge MAPS change.
// Global face/edge ids are indices into TopExp::MapShapes and mean nothing across a rebuild,
// so any caller holding an id from an earlier state is holding a wrong one. This is the
// handle that lets it find out instead of silently addressing the wrong edge.
//
// Session-scoped and deliberately NOT serialized: an id is only meaningful within the run
// that produced it, so persisting the counter would imply a promise across loads that the
// ids themselves cannot keep.
uint64_t topo_generation{1};
// v5: every feature is length-framed, so a reader can stop early on an older file and skip
// the tail of a newer one. This is the LAST version that has to break anything — from here a
// new field only needs appending to save/load, with no bump and no orphaned projects.
// v6: no wire-format change — the bytes are v5's, and both are read by the same framed path.
// The stamp advances only to put a project-container change on the record; the 3MF backends
// own that story. v4 still opens through the pre-framing flat path.
static constexpr uint32_t ORCA_CAD_RECIPE_VERSION = 6;
std::string serialize_recipe() const;
bool deserialize_recipe(const std::string& blob);
// Export every body to a STEP file as native B-rep (not mesh). body_xforms is the
// per-body display transform (Move gizmo); when supplied the bodies are written at
// those positions so the STEP matches what Commit ships. false + err on failure.
bool export_step(const std::string& path,
const std::vector<Transform3d>& body_xforms,
std::string& err) const;
GeometryEngine::MassProps body_mass_properties(int body_index) const;
// One overlapping pair of solid bodies. Indices are into `bodies`, a_ < b_.
struct Interference { int body_a{-1}; int body_b{-1}; double volume{0}; };
// Every pair of solid bodies whose intersection encloses more than min_volume (mm^3).
// Reports only — mutates nothing, so mates and placements are unaffected by calling it.
// Sheet bodies are skipped: an intersection involving one encloses no volume.
std::vector<Interference> check_interference(double min_volume = 1e-6) const;
// ponytail: derived from the OCCT shape type; no stored flag, bodies aren't serialized anyway.
static bool is_sheet_shape(const TopoDS_Shape& s); // true if TopExp finds no TopAbs_SOLID
// Undo/redo of the feature recipe (Onshape-style Ctrl+Z). The caller marks a
// user-action boundary by calling checkpoint() BEFORE the mutation(s) for that
// action (add/delete/move/replace, or a direct features edit). undo()/redo() then
// restore the snapshot and recompute(). Because everything else (bodies/meshes/
// body) is derived by recompute(), snapshotting `features` alone is a complete,
// exact history; one checkpoint == one Ctrl+Z step.
void checkpoint(); // snapshot `features` for undo + invalidate redo
// Drop the most recent checkpoint. For a mutation that took a checkpoint, then failed
// and restored the pre-mutation state itself (the constraint paths reject an
// over-constrained addition this way): the snapshot now describes a state identical to
// the current one, and leaving it turns the next Ctrl+Z into a press that does nothing.
void abandon_checkpoint();
bool can_undo() const { return !m_undo.empty(); }
bool can_redo() const { return !m_redo.empty(); }
size_t undo_depth() const { return m_undo.size(); }
size_t redo_depth() const { return m_redo.size(); }
bool undo(); // restore the previous feature list + recompute(); false if no history
bool redo(); // re-apply the most recently undone change; false if none
// Feature-tree editing (Onshape-style). All are transactional: they snapshot
// features, mutate, recompute(), and roll back to the snapshot (re-recomputing)
// if the result is invalid — so a failed edit never leaves a broken body.
//
// remove_feature: erase features[index]; deleting a Sketch cascades to the
// Extrude(s) that consume it; surviving sketch_ref indices are remapped.
// move_feature: shift features[index] by delta (-1 up / +1 down), clamped;
// sketch_ref indices of the two swapped slots are remapped.
// replace_feature: overwrite features[index] with `edited` (its name and, for
// an Extrude, its sketch_ref are preserved from the original).
bool remove_feature(int index);
bool move_feature(int index, int delta);
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 ->
// features[sketch_idx], extrude params -> features[extrude_idx]), keeping
// each slot's name/type and the sketch_ref link. Transactional like above.
bool replace_sketch_extrude(int sketch_idx, int extrude_idx, const CadFeature& edited);
// Apply ONE candidate feature on top of the current committed body and
// 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.
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
// it overlaid on the moved body instead of floating back at the untransformed origin.
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh,
std::vector<TriangleMesh>& out_body_meshes, std::string& err) 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
// holes. Falls back to a face over build_sketch_wire() for the legacy profile/shape paths,
// which have no concept of a second loop.
TopoDS_Face build_sketch_face(const CadFeature& sketch) const;
// Apply a single feature to (result, have_body), throwing std::runtime_error on
// failure. `context` is the body whose faces/edges the feature reads (face-extrude
// source, up-to-face target, dress-up, hole) — it differs from `result` only when the
// feature builds a NEW body from an existing one (face-extrude New). Shared by route.
void apply_feature(TopoDS_Shape& result, bool& have_body,
const TopoDS_Shape& context, const CadFeature& f) const;
// Route one feature into the bodies list: resolve its target body, decide whether it
// 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;
// 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,
// which works on a single result shape). Throws std::runtime_error on a failed op.
void apply_boolean(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_cut(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_mirror(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_transform(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_thicken(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_thicken_surface(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_surface_offset(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_project(const std::vector<CadBody>& bodies, CadFeature& f) const;
static DatumCoordSys datum_frame(const std::vector<CadBody>& bodies, const CadFeature& f);
void apply_mate(std::vector<CadBody>& bodies, const CadFeature& f) const;
void detect_mate_conflicts(); // refills mate_conflicts from the feature list alone
// Undo/redo stacks of recipe snapshots. checkpoint() pushes onto m_undo and clears
// m_redo; undo()/redo() shuffle the current state between them. Capped so a long
// session can't grow unbounded.
//
// The snapshot MUST carry `variables` as well as `features`: a caller that sets a bad
// variable, sees recompute() fail and calls undo() to roll it back would otherwise be
// left with the bad variable still in the document, so every later recompute fails —
// the exact corruption the checkpoint/undo pattern exists to prevent. Not serialized,
// so this changes no on-disk format.
struct Snapshot {
std::vector<CadFeature> features;
std::map<std::string, std::string> variables;
};
std::vector<Snapshot> m_undo;
std::vector<Snapshot> m_redo;
static constexpr size_t k_undo_cap = 200;
};
} // namespace Slic3r
#endif // slic3r_CadDocument_hpp_
+711
View File
@@ -0,0 +1,711 @@
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <BRepMesh_IncrementalMesh.hxx>
#include <BRep_Tool.hxx>
#include <BRepAdaptor_Surface.hxx>
#include <BRepLProp_SLProps.hxx>
#include <gp_Cylinder.hxx>
#include <BRepFilletAPI_MakeFillet.hxx>
#include <BRepFilletAPI_MakeChamfer.hxx>
#include <stdexcept>
#include <TopExp_Explorer.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Edge.hxx>
#include <TopExp.hxx>
#include <TopTools.hxx>
#include <TopTools_IndexedMapOfShape.hxx>
#include <Poly_Triangulation.hxx>
#include <gp_Ax2.hxx>
#include <gp_Dir.hxx>
#include <gp_Pnt.hxx>
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <GeomLProp_SLProps.hxx>
#include <BRepAdaptor_Curve.hxx>
#include <gp_Circ.hxx>
#include <GCPnts_TangentialDeflection.hxx>
#include <STEPControl_Reader.hxx>
#include <IFSelect_ReturnStatus.hxx>
#include <Standard_Failure.hxx>
#include <BRepExtrema_DistShapeShape.hxx>
#include <BRepBuilderAPI_MakeVertex.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakeSolid.hxx>
#include <BRep_Builder.hxx>
#include <TopoDS_Shell.hxx>
#include <TopoDS_Vertex.hxx>
#include <ShapeUpgrade_UnifySameDomain.hxx>
#include <array>
#include <map>
#include <cmath>
namespace Slic3r {
// ---- STEP import (B-rep, not mesh) ----
std::vector<TopoDS_Shape> GeometryEngine::read_step_solids(const std::string& path, std::string& err)
{
err.clear();
std::vector<TopoDS_Shape> out;
try {
STEPControl_Reader reader;
if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) {
err = "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; }
// 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.GetMessageString() ? e.GetMessageString() : "OCCT failed to read STEP";
out.clear();
}
return out;
}
// ---- Mesh -> B-rep (faceted, shared topology by construction) ----
//
// Port of mesh2step's brep_build.py. Two properties are load-bearing and easy to lose:
//
// 1. The edge cache is keyed on the UNORDERED vertex-index pair, and a triangle that walks
// the edge backwards (i > j) gets edge.Reversed(). Consistently-wound meshes (STL/OBJ/3MF
// all are) walk every shared edge in opposite directions from its two adjacent triangles,
// so this reversal is exactly what leaves the faces coherently outward-oriented.
// 2. Degeneracy is split in two, deliberately. A triangle is dropped as sub-resolution noise
// only if its longest edge is below `tolerance` (an absolute floor), while sliver rejection
// is scale-INDEPENDENT (area < 1e-9 * longest_edge^2). Folding the two together under one
// `area < tolerance^2` test rejects legitimate thin CAD slivers whenever tolerance is coarse
// relative to them, turning a watertight input into a falsely-open shell — a real regression
// mesh2step hit on a 62k-triangle mechanical part.
TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
double tolerance,
double merge_angle_deg,
MeshBrepStats& stats)
{
stats = MeshBrepStats{};
stats.input_tris = int(its.indices.size());
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");
// 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.
std::map<std::array<long long, 3>, int> cell_to_new;
std::vector<int> old_to_new(its.vertices.size(), -1);
std::vector<Vec3d> verts;
verts.reserve(its.vertices.size());
for (size_t i = 0; i < its.vertices.size(); ++i) {
const Vec3d p = its.vertices[i].cast<double>();
const std::array<long long, 3> cell{ (long long) std::llround(p.x() / tolerance),
(long long) std::llround(p.y() / tolerance),
(long long) std::llround(p.z() / tolerance) };
auto ins = cell_to_new.emplace(cell, int(verts.size()));
if (ins.second)
verts.push_back(p);
old_to_new[i] = ins.first->second;
}
// 2. Reject degenerate triangles (see the two-part rule in the comment above).
std::vector<Vec3i32> tris;
tris.reserve(its.indices.size());
for (const Vec3i32& t : its.indices) {
const int a = old_to_new[t(0)], b = old_to_new[t(1)], c = old_to_new[t(2)];
if (a == b || b == c || a == c) { ++stats.degenerate_collapsed; continue; }
const Vec3d& pa = verts[a]; const Vec3d& pb = verts[b]; const Vec3d& pc = verts[c];
const double e0 = (pb - pa).norm(), e1 = (pc - pb).norm(), e2 = (pa - pc).norm();
const double longest = std::max(e0, std::max(e1, e2));
if (longest < tolerance) { ++stats.degenerate_collapsed; continue; }
const double area = 0.5 * (pb - pa).cross(pc - pa).norm();
if (area < 1e-9 * longest * longest) { ++stats.degenerate_sliver; continue; }
tris.emplace_back(a, b, c);
}
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)");
// 3. One face per triangle, sharing vertices and edges through the caches.
std::vector<TopoDS_Vertex> vertex_cache(verts.size());
std::vector<bool> vertex_made(verts.size(), false);
auto get_vertex = [&](int i) -> const TopoDS_Vertex& {
if (!vertex_made[i]) {
const Vec3d& p = verts[i];
vertex_cache[i] = BRepBuilderAPI_MakeVertex(gp_Pnt(p.x(), p.y(), p.z())).Vertex();
vertex_made[i] = true;
}
return vertex_cache[i];
};
std::map<std::pair<int, int>, TopoDS_Edge> edge_cache;
std::map<std::pair<int, int>, int> edge_usage;
auto get_edge = [&](int i, int j) -> TopoDS_Edge {
const std::pair<int, int> key = (i < j) ? std::make_pair(i, j) : std::make_pair(j, i);
++edge_usage[key];
auto it = edge_cache.find(key);
if (it == edge_cache.end())
it = edge_cache.emplace(key,
BRepBuilderAPI_MakeEdge(get_vertex(key.first), get_vertex(key.second)).Edge()).first;
return (i > j) ? TopoDS::Edge(it->second.Reversed()) : it->second;
};
BRep_Builder builder;
TopoDS_Shell shell;
builder.MakeShell(shell);
for (const Vec3i32& t : tris) {
try {
BRepBuilderAPI_MakeWire mk_wire(get_edge(t(0), t(1)), get_edge(t(1), t(2)), get_edge(t(2), t(0)));
if (!mk_wire.IsDone()) { ++stats.faces_failed; continue; }
BRepBuilderAPI_MakeFace mk_face(mk_wire.Wire());
if (!mk_face.IsDone()) { ++stats.faces_failed; continue; }
builder.Add(shell, mk_face.Face());
++stats.faces_built;
} catch (const Standard_Failure&) {
++stats.faces_failed;
}
}
// 4. Watertightness falls straight out of the usage counts the cache already gathered.
for (const auto& kv : edge_usage) {
if (kv.second == 1) ++stats.boundary_edges;
else if (kv.second >= 3) ++stats.nonmanifold_edges;
}
stats.unique_edges = int(edge_usage.size());
stats.watertight = stats.boundary_edges == 0 && stats.nonmanifold_edges == 0 && stats.unique_edges > 0;
TopoDS_Shape shape = shell;
if (stats.watertight && stats.faces_built > 0) {
BRepBuilderAPI_MakeSolid mk_solid(shell);
if (mk_solid.IsDone()) {
TopoDS_Solid solid = mk_solid.Solid();
GProp_GProps props;
BRepGProp::VolumeProperties(solid, props);
double vol = props.Mass();
if (vol < 0.0) { // inward-wound input
solid = TopoDS::Solid(solid.Reversed());
vol = -vol;
}
if (vol > 0.0) {
shape = solid;
stats.is_solid = true;
stats.volume = vol;
}
}
}
// 5. Optional coplanar merge. Faceted output is one planar face per triangle — exact, but
// you cannot meaningfully fillet or extrude a face that IS a single triangle. Merging
// coplanar neighbours is what turns the import into something the face/edge tools can
// actually operate on (a 12-triangle cube collapses to its 6 real faces).
if (merge_angle_deg > 0.0) {
try {
ShapeUpgrade_UnifySameDomain unifier(shape, true, true, true);
unifier.SetAngularTolerance(merge_angle_deg * M_PI / 180.0);
unifier.SetLinearTolerance(tolerance);
unifier.Build();
const TopoDS_Shape merged = unifier.Shape();
if (!merged.IsNull())
shape = merged;
} catch (const Standard_Failure&) {
// Merging is an optimisation, not a correctness step: keep the exact faceted shape.
}
}
stats.faces_final = face_count(shape);
return shape;
}
// ---- Primitive creation ----
TopoDS_Solid GeometryEngine::make_primitive(const PrimitiveParams& params)
{
switch (params.type) {
case PrimitiveType::Box:
return BRepPrimAPI_MakeBox(gp_Pnt(-params.box_w/2, -params.box_d/2, 0),
params.box_w, params.box_d, params.box_h).Solid();
case PrimitiveType::Cylinder:
return BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
params.cyl_radius, params.cyl_height).Solid();
case PrimitiveType::Sphere:
return BRepPrimAPI_MakeSphere(gp_Pnt(0,0,params.sph_radius), params.sph_radius).Solid();
case PrimitiveType::Cone:
return BRepPrimAPI_MakeCone(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
params.cone_r1, params.cone_r2, params.cone_height).Solid();
case PrimitiveType::Torus:
return BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0,0,params.torus_r2), gp_Dir(0,0,1)),
params.torus_r1, params.torus_r2).Solid();
default:
return BRepPrimAPI_MakeBox(gp_Pnt(-10,-10,0), 20,20,20).Solid();
}
}
// ---- Face classification ----
FaceGroup GeometryEngine::classify_face(const TopoDS_Face& face, const TopoDS_Shape& /*solid*/)
{
try {
BRepAdaptor_Surface surf(face);
if (surf.GetType() == GeomAbs_Plane) {
// Sample normal at center UV
double u = (surf.FirstUParameter() + surf.LastUParameter()) / 2.0;
double v = (surf.FirstVParameter() + surf.LastVParameter()) / 2.0;
gp_Pnt pt; gp_Vec du, dv;
surf.D1(u, v, pt, du, dv);
gp_Dir n = du.Crossed(dv);
if (face.Orientation() == TopAbs_REVERSED) n.Reverse();
if (n.Z() > 0.7) return FaceGroup::Top;
if (n.Z() < -0.7) return FaceGroup::Bottom;
return FaceGroup::Lateral;
}
} catch (...) {}
return FaceGroup::Lateral;
}
// ---- Edge collection ----
std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid, FaceGroup target)
{
std::vector<TopoDS_Edge> result;
if (target == FaceGroup::All) {
for (TopExp_Explorer exp(solid, TopAbs_EDGE); exp.More(); exp.Next())
result.push_back(TopoDS::Edge(exp.Current()));
return result;
}
// Build edge-to-face map once
TopTools_IndexedDataMapOfShapeListOfShape 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 TopTools_ListOfShape& faces = edgeFaceMap.FindFromKey(edge);
bool include = false;
for (auto it = faces.begin(); it != faces.end(); ++it) {
FaceGroup fg = classify_face(TopoDS::Face(*it), solid);
if (target == FaceGroup::Top && fg == FaceGroup::Top) { include = true; break; }
if (target == FaceGroup::Bottom && fg == FaceGroup::Bottom) { include = true; break; }
if (target == FaceGroup::Lateral && fg == FaceGroup::Lateral) { include = true; break; }
}
if (!include && target == FaceGroup::Top) {
for (auto it = faces.begin(); it != faces.end(); ++it) {
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Top) { include = true; break; }
}
}
if (!include && target == FaceGroup::Bottom) {
for (auto it = faces.begin(); it != faces.end(); ++it) {
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Bottom) { include = true; break; }
}
}
if (target == FaceGroup::Lateral && !include) {
int lateralCount = 0;
for (auto it = faces.begin(); it != faces.end(); ++it) {
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Lateral) ++lateralCount;
}
if (lateralCount >= 2) include = true;
}
if (include) result.push_back(edge);
}
return result;
}
// ---- Fillet/Chamfer ----
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces)
{
if (radius <= 0.001) return solid;
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) return solid;
BRepFilletAPI_MakeFillet fillet(solid);
for (const auto& edge : edges)
fillet.Add(radius, edge);
fillet.Build();
// 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");
return fillet.Shape();
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, FaceGroup faces)
{
if (distance <= 0.001) return solid;
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) return solid;
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");
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();
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id)
{
if (distance <= 0.001) return solid;
TopoDS_Edge edge = edge_by_index(solid, edge_id);
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
BRepFilletAPI_MakeChamfer mk(solid);
mk.Add(distance, edge);
mk.Build();
if (!mk.IsDone()) throw std::runtime_error("apply_chamfer: OCCT chamfer failed");
return mk.Shape();
}
// ---- Tessellation ----
TriangleMesh GeometryEngine::tessellate(const TopoDS_Shape& shape,
double linear_deflection,
double angular_deflection)
{
BRepMesh_IncrementalMesh mesh(shape, linear_deflection, false, angular_deflection, true);
int nbNodes = 0, nbTri = 0;
for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) {
TopLoc_Location loc;
Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(exp.Current()), loc);
if (!tri.IsNull()) { nbNodes += tri->NbNodes(); nbTri += tri->NbTriangles(); }
}
if (nbTri == 0 || nbNodes == 0) return TriangleMesh{};
stl_file stl;
stl.stats.type = inmemory;
stl.stats.number_of_facets = (uint32_t)nbTri;
stl.stats.original_num_facets = stl.stats.number_of_facets;
stl_allocate(&stl);
std::vector<Vec3f> pts; pts.reserve(nbNodes);
int ndOff = 0, trOff = 0;
for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) {
const TopoDS_Shape& F = exp.Current();
TopLoc_Location loc;
Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(F), loc);
if (tri.IsNull()) continue;
gp_Trsf T = loc.Transformation();
for (int i = 1; i <= tri->NbNodes(); ++i) {
gp_Pnt p = tri->Node(i); p.Transform(T);
pts.emplace_back(Vec3f(p.X(), p.Y(), p.Z()));
}
auto orient = exp.Current().Orientation();
int ids[3];
for (int i = 1; i <= tri->NbTriangles(); ++i) {
Poly_Triangle t = tri->Triangle(i); t.Get(ids[0], ids[1], ids[2]);
if (orient == TopAbs_REVERSED) std::swap(ids[1], ids[2]);
stl_facet f;
f.vertex[0] = pts[ids[0]+ndOff-1].cast<float>();
f.vertex[1] = pts[ids[1]+ndOff-1].cast<float>();
f.vertex[2] = pts[ids[2]+ndOff-1].cast<float>();
f.extra[0]=0; f.extra[1]=0;
stl_normal n; stl_calculate_normal(n,&f); stl_normalize_vector(n);
f.normal=n; stl.facet_start[trOff+i-1]=f;
}
ndOff += tri->NbNodes(); trOff += tri->NbTriangles();
}
TriangleMesh result; result.from_stl(stl); return result;
}
GeometryEngine::Deviation
GeometryEngine::surface_deviation(const TopoDS_Shape& candidate,
const TopoDS_Shape& reference,
double linear_deflection)
{
Deviation d;
if (candidate.IsNull() || reference.IsNull()) return d;
TriangleMesh mesh = tessellate(candidate, linear_deflection, 0.5);
const auto& verts = mesh.its.vertices;
if (verts.empty()) return d;
double sum = 0.0, sumsq = 0.0;
int n = 0;
for (const auto& v : verts) {
gp_Pnt p(v.x(), v.y(), v.z());
BRepExtrema_DistShapeShape dss(BRepBuilderAPI_MakeVertex(p).Vertex(), reference);
if (!dss.IsDone() || dss.NbSolution() < 1) continue;
double dist = dss.Value();
d.max_mm = std::max(d.max_mm, dist);
sum += dist; sumsq += dist * dist; ++n;
}
d.sample_count = n;
if (n > 0) { d.mean_mm = sum / n; d.rms_mm = std::sqrt(sumsq / n); }
return d;
}
GeometryEngine::MassProps GeometryEngine::mass_properties(const TopoDS_Shape& shape)
{
MassProps p;
if (shape.IsNull()) return p;
try {
// A sheet body (open shell, no solid) encloses nothing, and BRepGProp::VolumeProperties
// integrates the divergence theorem over whatever faces exist — on an open shell that is
// not a volume at all. It came back as 96000 with an inertia diagonal of
// [-4.2e7, -4.2e7, -6.9e7] for a 60x60x40 four-walled box: negative principal moments,
// which no real body can have. The old code then hid the only obvious tell by taking
// std::abs() of the mass. Report the honest answer instead — surface area is still
// meaningful, so this is not a failure, just not a solid.
p.is_solid = TopExp_Explorer(shape, TopAbs_SOLID).More();
if (!p.is_solid) {
GProp_GProps sonly;
BRepGProp::SurfaceProperties(shape, sonly);
p.surface_area = sonly.Mass();
p.valid = true; // the area IS trustworthy; volume/inertia stay zero
return p;
}
GProp_GProps vprops;
BRepGProp::VolumeProperties(shape, vprops);
double mass = vprops.Mass();
if (std::abs(mass) < 1e-30) return p;
p.volume = std::abs(mass);
p.center_of_mass = Vec3d(vprops.CentreOfMass().X(), vprops.CentreOfMass().Y(), vprops.CentreOfMass().Z());
gp_Mat mat = vprops.MatrixOfInertia();
p.inertia = {{
mat(1,1), mat(1,2), mat(1,3),
mat(2,1), mat(2,2), mat(2,3),
mat(3,1), mat(3,2), mat(3,3),
}};
GProp_GProps sprops;
BRepGProp::SurfaceProperties(shape, sprops);
p.surface_area = sprops.Mass();
p.valid = true;
} catch (const Standard_Failure&) {
// leave valid = false
}
return p;
}
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";
}
}
// ---- Topology accessors ----
int GeometryEngine::face_count(const TopoDS_Shape& shape)
{
int n = 0;
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next())
++n;
return n;
}
TopoDS_Face GeometryEngine::face_by_index(const TopoDS_Shape& shape, int index)
{
if (index < 0) return TopoDS_Face();
int ordinal = 0;
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next()) {
if (ordinal == index)
return TopoDS::Face(e.Current());
++ordinal;
}
return TopoDS_Face();
}
std::vector<TopoDS_Face> GeometryEngine::faces_of(const TopoDS_Shape& shape)
{
std::vector<TopoDS_Face> out;
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next())
out.push_back(TopoDS::Face(e.Current())); // same order as face_by_index
return out;
}
std::vector<TopoDS_Edge> GeometryEngine::edges_of(const TopoDS_Shape& shape)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map); // same order as edge_by_index
std::vector<TopoDS_Edge> out;
out.reserve(map.Extent());
for (int i = 1; i <= map.Extent(); ++i)
out.push_back(TopoDS::Edge(map(i)));
return out;
}
std::vector<TopoDS_Edge> GeometryEngine::edges_of_face(const TopoDS_Face& face)
{
std::vector<TopoDS_Edge> result;
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(face, TopAbs_EDGE, map);
for (int i = 1; i <= map.Extent(); ++i)
result.push_back(TopoDS::Edge(map(i)));
return result;
}
std::vector<Vec3d> GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, double chord_tol)
{
if (BRep_Tool::Degenerated(edge))
return {};
BRepAdaptor_Curve curve(edge);
GCPnts_TangentialDeflection disc(curve, 0.1, chord_tol);
std::vector<Vec3d> pts;
if (disc.NbPoints() >= 2) {
for (int i = 1; i <= disc.NbPoints(); ++i) {
gp_Pnt p = disc.Value(i);
pts.emplace_back(p.X(), p.Y(), p.Z());
}
} else {
gp_Pnt p0 = curve.Value(curve.FirstParameter());
gp_Pnt p1 = curve.Value(curve.LastParameter());
pts.emplace_back(p0.X(), p0.Y(), p0.Z());
pts.emplace_back(p1.X(), p1.Y(), p1.Z());
}
return pts;
}
Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face)
{
GProp_GProps props;
BRepGProp::SurfaceProperties(face, props);
gp_Pnt c = props.CentreOfMass();
return Vec3d(c.X(), c.Y(), c.Z());
}
Vec3d GeometryEngine::face_normal_world(const TopoDS_Face& face)
{
BRepAdaptor_Surface surf(face);
const double u = 0.5 * (surf.FirstUParameter() + surf.LastUParameter());
const double v = 0.5 * (surf.FirstVParameter() + surf.LastVParameter());
BRepLProp_SLProps props(surf, u, v, 1, 1e-6);
gp_Dir n(0.0, 0.0, 1.0);
if (props.IsNormalDefined()) n = props.Normal();
if (face.Orientation() == TopAbs_REVERSED) n.Reverse(); // outward (account for face winding)
return Vec3d(n.X(), n.Y(), n.Z());
}
GeometryEngine::CylinderFace GeometryEngine::cylinder_of_face(const TopoDS_Face& face)
{
CylinderFace cf;
if (face.IsNull()) return cf;
BRepAdaptor_Surface surf(face);
if (surf.GetType() != GeomAbs_Cylinder) return cf;
const gp_Cylinder cyl = surf.Cylinder();
const gp_Ax1 ax = cyl.Axis();
const Vec3d axis(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z());
const Vec3d apt (ax.Location().X(), ax.Location().Y(), ax.Location().Z());
cf.radius = cyl.Radius();
// Axial extent: V is the axial parameter on a cylinder; bound the face's two ends and
// order them so `axis` points base -> top.
const double umid = 0.5 * (surf.FirstUParameter() + surf.LastUParameter());
const gp_Pnt e0 = surf.Value(umid, surf.FirstVParameter());
const gp_Pnt e1 = surf.Value(umid, surf.LastVParameter());
double t0 = (Vec3d(e0.X(), e0.Y(), e0.Z()) - apt).dot(axis);
double t1 = (Vec3d(e1.X(), e1.Y(), e1.Z()) - apt).dot(axis);
if (t1 < t0) std::swap(t0, t1);
cf.base = apt + axis * t0;
cf.axis = axis;
cf.height = t1 - t0;
// Internal (bore) vs external: compare the face's outward normal at its centre to the
// outward radial direction. A bore's normal points toward the axis (dot < 0).
const gp_Pnt sp = surf.Value(umid, 0.5 * (surf.FirstVParameter() + surf.LastVParameter()));
const Vec3d S(sp.X(), sp.Y(), sp.Z());
const Vec3d axpt = cf.base + axis * (S - cf.base).dot(axis);
const Vec3d radial = (S - axpt).normalized();
cf.internal = face_normal_world(face).dot(radial) < 0.0;
cf.ok = true;
return cf;
}
GeometryEngine::CylinderFace GeometryEngine::circle_of_edge(const TopoDS_Edge& edge)
{
CylinderFace cf;
if (edge.IsNull()) return cf;
BRepAdaptor_Curve curve(edge);
if (curve.GetType() != GeomAbs_Circle) return cf;
const gp_Circ c = curve.Circle();
const gp_Ax1 ax = c.Axis();
cf.base = Vec3d(c.Location().X(), c.Location().Y(), c.Location().Z());
cf.axis = Vec3d(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z());
cf.radius = c.Radius();
cf.height = 0.0; // an edge carries no axial extent; the card keeps the current length
cf.internal = false; // ambiguous from an edge alone — default external, user can toggle
cf.ok = true;
return cf;
}
bool GeometryEngine::face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin,
const Vec3d& x_axis, const Vec3d& y_axis,
double& umin, double& umax, double& vmin, double& vmax)
{
umin = vmin = 1e30; umax = vmax = -1e30;
bool any = false;
for (TopExp_Explorer ex(face, TopAbs_VERTEX); ex.More(); ex.Next()) {
const gp_Pnt p = BRep_Tool::Pnt(TopoDS::Vertex(ex.Current()));
const Vec3d P(p.X(), p.Y(), p.Z());
const double u = (P - origin).dot(x_axis);
const double v = (P - origin).dot(y_axis);
umin = std::min(umin, u); umax = std::max(umax, u);
vmin = std::min(vmin, v); vmax = std::max(vmax, v);
any = true;
}
return any;
}
int GeometryEngine::edge_count(const TopoDS_Shape& shape)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map);
return map.Extent();
}
TopoDS_Edge GeometryEngine::edge_by_index(const TopoDS_Shape& shape, int index)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map);
if (index < 0 || index >= map.Extent())
return TopoDS_Edge();
return TopoDS::Edge(map(index + 1));
}
int GeometryEngine::edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map);
int idx = map.FindIndex(edge);
return (idx > 0) ? (idx - 1) : -1;
}
} // namespace Slic3r
+183
View File
@@ -0,0 +1,183 @@
#ifndef slic3r_GeometryEngine_hpp_
#define slic3r_GeometryEngine_hpp_
#include "libslic3r/TriangleMesh.hpp"
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeTorus.hxx>
#include <gp_Ax2.hxx>
#include <TopoDS_Solid.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Edge.hxx>
#include <vector>
#include <string>
namespace Slic3r {
enum class PrimitiveType { Box, Cylinder, Sphere, Cone, Torus, COUNT };
enum class DressUpType { Fillet, Chamfer };
enum class FaceGroup { Top, Bottom, Lateral, All };
struct PrimitiveParams {
PrimitiveType type{PrimitiveType::Box};
double box_w{20}, box_h{20}, box_d{20};
double cyl_radius{10}, cyl_height{20};
double sph_radius{10};
double cone_r1{10}, cone_r2{5}, cone_height{20};
double torus_r1{10}, torus_r2{3};
// Dress-up
bool dressup_enabled{false};
DressUpType dressup_type{DressUpType::Fillet};
FaceGroup dressup_faces{FaceGroup::All};
double dressup_radius{1.0}; // fillet radius
double dressup_chamfer_dist{1.0}; // chamfer distance (symmetric)
// Mesh quality
double linear_deflection{0.01};
double angular_deflection{0.5};
template<class Archive>
void serialize(Archive& ar) {
ar(type, box_w, box_h, box_d, cyl_radius, cyl_height, sph_radius,
cone_r1, cone_r2, cone_height, torus_r1, torus_r2,
dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist,
linear_deflection, angular_deflection);
}
};
class GeometryEngine
{
public:
static TopoDS_Solid make_primitive(const PrimitiveParams& params);
// Read a STEP file into its top-level solids (one TopoDS_Shape per solid; falls back to
// the whole shape if it contains no closed solids). Reuses OCCT's STEPControl_Reader,
// already linked via Format/STEP.cpp — no new dependency. err is set on failure (empty result).
static std::vector<TopoDS_Shape> read_step_solids(const std::string& path, std::string& err);
// Triangle mesh -> B-rep solid. Native port of mesh2step
// (github.com/tommasobbianchi/mesh2step): vertices and edges are SHARED across triangles
// at construction time (vertex cache by deduped index, edge cache by unordered index pair),
// so there is no BRepBuilderAPI_Sewing pass to reconstruct topology afterwards — which is
// both faster and what makes watertightness fall out of the edge-usage counts for free.
// Runs in-process on the OCCT kernel libslic3r already links: no STEP file is written or
// re-read (a faceted STEP of a 62k-triangle mesh is ~149 MB and takes OCCT's reader >300 s
// to parse back, so routing the Design tab through a file would hang the GUI).
struct MeshBrepStats {
int input_tris{0};
int kept_tris{0};
int degenerate_collapsed{0}; // <3 distinct vertices after tolerance quantization
int degenerate_sliver{0}; // 3 distinct vertices but near-collinear
int faces_built{0};
int faces_failed{0};
int unique_edges{0};
int boundary_edges{0}; // used by exactly 1 triangle -> open shell
int nonmanifold_edges{0}; // used by >=3 triangles
bool watertight{false}; // every edge used exactly twice
bool is_solid{false}; // watertight AND MakeSolid gave a positive volume
double volume{0.0};
int faces_final{0}; // after the optional coplanar merge
};
// tolerance: spatial quantization cell used ONLY for vertex dedup and as the
// sub-resolution floor below which a triangle is noise. Never a sew tolerance.
// merge_angle_deg > 0: run ShapeUpgrade_UnifySameDomain to merge coplanar neighbours into
// single faces (a 12-triangle cube -> 6 pickable faces). This is what makes the imported
// body editable with the face/edge tools; <= 0 keeps the exact one-face-per-triangle form.
// Never wraps a non-watertight shell as a fake solid: an open mesh comes back as a shell,
// with the reason (boundary / non-manifold edge counts) reported in stats.
static TopoDS_Shape mesh_to_brep(const indexed_triangle_set& its,
double tolerance,
double merge_angle_deg,
MeshBrepStats& stats);
struct MassProps {
double volume{0.0};
double surface_area{0.0};
Vec3d center_of_mass{Vec3d::Zero()};
std::array<double, 9> inertia{};
bool valid{false};
// False for a sheet body (an open shell with no solid). Volume and inertia are then
// meaningless and are reported as zero; surface_area stays meaningful. See the .cpp.
bool is_solid{false};
};
static MassProps mass_properties(const TopoDS_Shape& shape);
struct Deviation { double max_mm{0}; double mean_mm{0}; double rms_mm{0}; int sample_count{0}; };
static Deviation surface_deviation(const TopoDS_Shape& candidate,
const TopoDS_Shape& reference,
double linear_deflection = 0.5);
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
FaceGroup faces = FaceGroup::All);
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
int edge_id);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
FaceGroup faces = FaceGroup::All);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
int edge_id);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
double linear_deflection = 0.01,
double angular_deflection = 0.5);
static std::string primitive_name(PrimitiveType type);
// Topology accessors for in-viewport face/edge picking (Design tab). Face index is the
// TopExp_Explorer(shape, TopAbs_FACE) ordinal — identical to SketchEngine::tessellate's
// per-triangle face id, so a picked triangle's id maps back to a face here.
static TopoDS_Face face_by_index(const TopoDS_Shape& shape, int index); // null if out of range
static int face_count(const TopoDS_Shape& shape);
// Bulk enumeration in the SAME order as face_by_index / edge_by_index, so ids are
// interchangeable. Walking a body with the _by_index accessors is quadratic (each call
// rescans the shape — edge_by_index even rebuilds the whole indexed map), which cost
// ~15 s on a 4.7k-face imported solid; enumerate once instead.
static std::vector<TopoDS_Face> faces_of(const TopoDS_Shape& shape);
static std::vector<TopoDS_Edge> edges_of(const TopoDS_Shape& shape);
static std::vector<TopoDS_Edge> edges_of_face(const TopoDS_Face& face);
// Centre of mass (world) of a face — used to compute the extrude length for "up to face".
static Vec3d face_centroid_world(const TopoDS_Face& face);
// Outward unit normal of a face at its UV midpoint (orientation-aware) — for the shell gizmo.
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);
// 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);
static int edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge);
// Analysis of a cylindrical face for the Thread tool (a hole bore or a cylinder's lateral
// surface): axis (base at the lower axial end + unit direction), radius, axial extent, and
// whether it is a bore (face normal points toward the axis = internal thread). ok=false if
// the face is not a cylinder.
struct CylinderFace {
bool ok{false};
Vec3d base{0, 0, 0};
Vec3d axis{0, 0, 1};
double radius{0};
double height{0};
bool internal{false};
};
static CylinderFace cylinder_of_face(const TopoDS_Face& face);
// Circular edge (a cylinder's perimeter): base = circle centre, axis = circle normal,
// radius = circle radius, height = 0 (unknown from an edge), internal = false. ok=false if
// the edge is not a circle. Lets the Thread tool be driven by a picked circular rim.
static CylinderFace circle_of_edge(const TopoDS_Edge& edge);
// Plane-coordinate (u,v) bounding box of a face's vertices, measured from `origin` along
// `x_axis`/`y_axis`. Lets the Hole tool dimension the hole from the face SIDES (umin/vmin =
// two adjacent edges) instead of from the centre. Returns false if the face has no vertices.
static bool face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin,
const Vec3d& x_axis, const Vec3d& y_axis,
double& umin, double& umax, double& vmin, double& vmax);
private:
static std::vector<TopoDS_Edge> collect_edges(const TopoDS_Shape& solid, FaceGroup faces);
static FaceGroup classify_face(const TopoDS_Face& face, const TopoDS_Shape& solid);
};
} // namespace Slic3r
#endif // slic3r_GeometryEngine_hpp_
+307
View File
@@ -0,0 +1,307 @@
#include "libslic3r/CAD/SketchConstraints.hpp"
#include <Eigen/Dense>
#include <cmath>
namespace Slic3r {
int SketchConstraints::add_point(double x, double y)
{
m_vars.push_back(x);
m_vars.push_back(y);
return static_cast<int>(m_vars.size() / 2) - 1;
}
void SketchConstraints::set_point(int id, double x, double y)
{
size_t idx = 2 * id;
m_vars[idx] = x;
m_vars[idx + 1] = y;
}
Vec2d SketchConstraints::get_point(int id) const
{
size_t idx = 2 * id;
return Vec2d(m_vars[idx], m_vars[idx + 1]);
}
int SketchConstraints::point_count() const
{
return static_cast<int>(m_vars.size() / 2);
}
void SketchConstraints::fix_point(int id)
{
size_t idx = 2 * id;
Con c;
c.type = FIX_POINT;
c.a = id;
c.b = c.c = c.d = 0;
c.k0 = m_vars[idx];
c.k1 = m_vars[idx + 1];
m_cons.push_back(c);
}
void SketchConstraints::coincident(int a, int b)
{
Con c;
c.type = COINCIDENT;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::horizontal(int a, int b)
{
Con c;
c.type = HORIZONTAL;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::vertical(int a, int b)
{
Con c;
c.type = VERTICAL;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::distance(int a, int b, double d)
{
Con c;
c.type = DISTANCE;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = d; c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::lock_x(int id, double x)
{
Con c;
c.type = LOCK_X;
c.a = id;
c.b = c.c = c.d = 0;
c.k0 = x; c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::lock_y(int id, double y)
{
Con c;
c.type = LOCK_Y;
c.a = id;
c.b = c.c = c.d = 0;
c.k0 = y; c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::equal_length(int a, int b, int c, int d)
{
Con con;
con.type = EQUAL_LENGTH;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::parallel(int a, int b, int c, int d)
{
Con con;
con.type = PARALLEL;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::perpendicular(int a, int b, int c, int d)
{
Con con;
con.type = PERPENDICULAR;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::midpoint(int m, int a, int b)
{
Con con;
con.type = MIDPOINT;
con.a = m; con.b = a; con.c = b; con.d = -1;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::symmetric(int a, int b, int c, int d)
{
Con con;
con.type = SYMMETRIC;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::angle(int a, int b, int c, int d, double radians)
{
Con con;
con.type = ANGLE;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = radians; con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::point_line_distance(int p, int a, int b, double dist)
{
Con con;
con.type = PT_LINE_DIST;
con.a = p; con.b = a; con.c = b; con.d = -1;
con.k0 = dist; con.k1 = 0;
m_cons.push_back(con);
}
Eigen::VectorXd SketchConstraints::residuals(const std::vector<double>& v) const
{
auto X = [&](int i) { return v[2 * i]; };
auto Y = [&](int i) { return v[2 * i + 1]; };
std::vector<double> res;
for (const auto& c : m_cons) {
switch (c.type) {
case FIX_POINT:
res.push_back(X(c.a) - c.k0);
res.push_back(Y(c.a) - c.k1);
break;
case COINCIDENT:
res.push_back(X(c.a) - X(c.b));
res.push_back(Y(c.a) - Y(c.b));
break;
case HORIZONTAL:
res.push_back(Y(c.a) - Y(c.b));
break;
case VERTICAL:
res.push_back(X(c.a) - X(c.b));
break;
case DISTANCE:
res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) - c.k0);
break;
case LOCK_X:
res.push_back(X(c.a) - c.k0);
break;
case LOCK_Y:
res.push_back(Y(c.a) - c.k0);
break;
case EQUAL_LENGTH:
res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) -
std::hypot(X(c.c) - X(c.d), Y(c.c) - Y(c.d)));
break;
case PARALLEL:
res.push_back((X(c.b) - X(c.a)) * (Y(c.d) - Y(c.c)) -
(Y(c.b) - Y(c.a)) * (X(c.d) - X(c.c)));
break;
case PERPENDICULAR:
res.push_back((X(c.b) - X(c.a)) * (X(c.d) - X(c.c)) +
(Y(c.b) - Y(c.a)) * (Y(c.d) - Y(c.c)));
break;
case MIDPOINT:
res.push_back(X(c.a) - 0.5 * (X(c.b) + X(c.c)));
res.push_back(Y(c.a) - 0.5 * (Y(c.b) + Y(c.c)));
break;
case SYMMETRIC: {
const double abx = X(c.b) - X(c.a), aby = Y(c.b) - Y(c.a);
const double cdx = X(c.d) - X(c.c), cdy = Y(c.d) - Y(c.c);
res.push_back(abx * cdx + aby * cdy);
const double mx = 0.5 * (X(c.a) + X(c.b));
const double my = 0.5 * (Y(c.a) + Y(c.b));
res.push_back((mx - X(c.c)) * cdy - (my - Y(c.c)) * cdx);
break;
}
case ANGLE: {
const double ux = X(c.b) - X(c.a), uy = Y(c.b) - Y(c.a);
const double wx = X(c.d) - X(c.c), wy = Y(c.d) - Y(c.c);
const double cross = ux * wy - uy * wx;
const double dot = ux * wx + uy * wy;
res.push_back(std::atan2(cross, dot) - c.k0);
break;
}
case PT_LINE_DIST: {
const double bx = X(c.b), by = Y(c.b);
const double cx = X(c.c), cy = Y(c.c);
const double L = std::hypot(cx - bx, cy - by);
const double num = (X(c.a) - bx) * (cy - by) - (Y(c.a) - by) * (cx - bx);
res.push_back((L > 1e-12 ? std::abs(num) / L : 0.0) - c.k0);
break;
}
}
}
Eigen::VectorXd r(static_cast<Eigen::Index>(res.size()));
for (size_t i = 0; i < res.size(); ++i)
r(static_cast<Eigen::Index>(i)) = res[i];
return r;
}
Eigen::MatrixXd SketchConstraints::jacobian(const std::vector<double>& v) const
{
int m = static_cast<int>(residuals(v).size());
int n = static_cast<int>(v.size());
Eigen::MatrixXd J(m, n);
const double eps = 1e-7;
std::vector<double> vp = v;
std::vector<double> vm = v;
for (int j = 0; j < n; ++j) {
vp[j] = v[j] + eps;
vm[j] = v[j] - eps;
Eigen::VectorXd rp = residuals(vp);
Eigen::VectorXd rm = residuals(vm);
vp[j] = v[j];
vm[j] = v[j];
J.col(j) = (rp - rm) / (2.0 * eps);
}
return J;
}
bool SketchConstraints::solve(int max_iter, double tol)
{
if (m_cons.empty()) return true;
double lambda = 1e-3;
Eigen::VectorXd r = residuals(m_vars);
for (int it = 0; it < max_iter; ++it) {
double rn = r.norm();
if (rn < tol) return true;
Eigen::MatrixXd J = jacobian(m_vars);
Eigen::MatrixXd A = J.transpose() * J;
Eigen::VectorXd g = J.transpose() * r;
bool stepped = false;
for (int t = 0; t < 12; ++t) {
Eigen::MatrixXd Ad = A;
for (int i = 0; i < Ad.rows(); ++i)
Ad(i, i) += lambda * (1.0 + Ad(i, i));
Eigen::VectorXd dx = Ad.ldlt().solve(-g);
std::vector<double> cand = m_vars;
for (size_t i = 0; i < cand.size(); ++i)
cand[i] += dx[static_cast<Eigen::Index>(i)];
Eigen::VectorXd rc = residuals(cand);
if (rc.norm() < rn) {
m_vars = cand;
r = rc;
lambda = std::max(lambda * 0.4, 1e-12);
stepped = true;
break;
}
lambda *= 3.0;
}
if (!stepped) break;
}
return r.norm() < tol * 100;
}
double SketchConstraints::residual_norm() const
{
return residuals(m_vars).norm();
}
} // namespace Slic3r
+68
View File
@@ -0,0 +1,68 @@
#ifndef slic3r_SketchConstraints_hpp_
#define slic3r_SketchConstraints_hpp_
#include "libslic3r/Point.hpp"
#include <vector>
#include <Eigen/Dense>
namespace Slic3r {
class SketchConstraints {
public:
int add_point(double x, double y);
void set_point(int id, double x, double y);
Vec2d get_point(int id) const;
int point_count() const;
void fix_point(int id);
void coincident(int a, int b);
void horizontal(int a, int b);
void vertical(int a, int b);
void distance(int a, int b, double d);
void lock_x(int id, double x);
void lock_y(int id, double y);
void equal_length(int a, int b, int c, int d);
void parallel(int a, int b, int c, int d);
void perpendicular(int a, int b, int c, int d);
void midpoint(int m, int a, int b);
void symmetric(int a, int b, int c, int d);
void angle(int a, int b, int c, int d, double radians);
void point_line_distance(int p, int a, int b, double dist);
bool solve(int max_iter = 200, double tol = 1e-10);
double residual_norm() const;
private:
std::vector<double> m_vars;
enum ConType : int {
FIX_POINT = 0,
COINCIDENT,
HORIZONTAL,
VERTICAL,
DISTANCE,
LOCK_X,
LOCK_Y,
EQUAL_LENGTH,
PARALLEL,
PERPENDICULAR,
MIDPOINT,
SYMMETRIC,
ANGLE,
PT_LINE_DIST
};
struct Con {
int type;
int a, b, c, d;
double k0, k1;
};
std::vector<Con> m_cons;
Eigen::VectorXd residuals(const std::vector<double>& v) const;
Eigen::MatrixXd jacobian(const std::vector<double>& v) const;
};
} // namespace Slic3r
#endif // slic3r_SketchConstraints_hpp_
File diff suppressed because it is too large Load Diff
+373
View File
@@ -0,0 +1,373 @@
#ifndef slic3r_SketchEngine_hpp_
#define slic3r_SketchEngine_hpp_
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <gp_Pln.hxx>
#include <gp_Ax3.hxx>
#include <TopoDS_Wire.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Face.hxx>
#include <vector>
#include <utility>
namespace Slic3r {
struct SketchSegment {
enum Type { Line, Arc, Circle, Rectangle, Polygon };
Type type{Line};
Vec2d p0{0,0}, p1{0,0};
Vec2d center{0,0};
double radius{0}, start_angle{0}, end_angle{0};
std::vector<Vec2d> points;
template<class Archive>
void serialize(Archive& ar) { ar(type, p0, p1, center, radius, start_angle, end_angle, points); }
};
struct SketchEntity {
enum class Type { Line, Arc, Circle, Point, Ellipse, EllipseArc, BSpline };
Type type{Type::Line};
Vec2d p0{0,0}; // Line: start; Arc/EllipseArc: start; Circle/Point/Ellipse: center; BSpline: first pole
Vec2d p1{0,0}; // Line: end; Arc/EllipseArc: end; (unused for Circle/Point/Ellipse); BSpline: last pole
Vec2d center{0,0}; // Arc/Circle/Ellipse(Arc) center
double radius{0}; // Circle/Arc radius; Ellipse(Arc): semi-major axis (a)
double start_angle{0}; // Arc sweep start; Ellipse(Arc): parametric start angle (radians)
double end_angle{0}; // Arc sweep end; Ellipse(Arc): parametric end angle
bool construction{false};
double rminor{0}; // Ellipse(Arc): semi-minor axis (b)
double rotation{0}; // Ellipse(Arc): major-axis angle phi (radians, about center)
std::vector<Vec2d> ctrl; // BSpline: control points (poles); p0/p1 mirror first/last pole
template<class Archive>
void serialize(Archive& ar) {
// Append-only: rminor/rotation added for Ellipse(Arc) (P2 Tier-B.1); ctrl for BSpline (B.2).
ar(type, p0, p1, center, radius, start_angle, end_angle, construction, rminor, rotation, ctrl);
}
};
struct SketchPlane {
Vec3d origin{0,0,0};
Vec3d normal{0,0,1};
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 {}; }
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}}; }
Vec2d project(const Vec3d& ray_origin, const Vec3d& ray_dir) const;
Vec3d to_world(const Vec2d& pt) const;
template<class Archive>
void serialize(Archive& ar) { ar(origin, normal, x_axis, y_axis); }
};
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;
template<class Archive>
void serialize(Archive& ar) { ar(points, closed); }
};
// Two sketch endpoints this close are ONE joint. Shared deliberately by the viewport
// (region_loops / connected_loop / open-end detection) and by the kernel
// (entities_to_wires): the viewport is what shades a region closed and offers it for
// extrude, so the kernel MUST be able to build every loop the viewport shades. When
// these two numbers disagreed the viewport promised a closed region at 1e-3 and the
// kernel refused it at 1e-4, which extruded a solid the user never drew.
// Nothing legitimate in a mm-scale sketch is 1 um apart.
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.
double sketch_join_tol();
void set_sketch_auto_close(bool on);
enum class SketchConstraintType {
Fix, Coincident, Horizontal, Vertical, Distance,
LockX, LockY, EqualLength, Parallel, Perpendicular,
Concentric,
Tangent, Midpoint, Symmetric, Angle,
Radius, Diameter,
PointOnLine, // a point lies on a line (or at signed perpendicular distance `value`)
PointOnObject, // a point lies on an entity edge (line -> PT_ON_LINE, circle -> PT_ON_CIRCLE)
// Append-only: cereal serializes this enum positionally as its underlying int, so
// 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
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
};
// Constraint on a SketchProfile, referencing profile point indices (a,b,c,d).
// `value` carries the target for Distance/LockX/LockY (ignored otherwise).
struct SketchConstraintDef {
SketchConstraintType type{SketchConstraintType::Coincident};
int a{-1}, b{-1}, c{-1}, d{-1};
double value{0.0};
template<class Archive> void serialize(Archive& ar) { ar(type, a, b, c, d, value); }
};
// Which point of an entity a constraint reference names.
// P0 = SketchEntity::p0 (Line start / Point position)
// P1 = SketchEntity::p1 (Line end)
// Center = SketchEntity::center (Arc/Circle center)
enum class SketchPointRole { P0, P1, Center };
// Constraint on coexisting SketchEntity objects (Fase 4.2). Each reference is an
// (entity index, point role) pair. Point-form constraints
// (Fix/Coincident/Horizontal/Vertical/Distance/LockX/LockY) use refs A and B as
// individual points. Segment-form constraints (Parallel/Perpendicular/EqualLength)
// use entity indices `ea`/`eb` as whole line segments (their P0->P1); roles are
// ignored for those. `value` carries the target for Distance/LockX/LockY.
struct SketchEntityConstraintDef {
SketchConstraintType type{SketchConstraintType::Coincident};
int ea{-1}, eb{-1}; // entity indices
SketchPointRole ra{SketchPointRole::P0}; // role within ea
SketchPointRole rb{SketchPointRole::P0}; // role within eb
double value{0.0};
int ec{-1}; // third entity ref (Symmetric axis)
SketchPointRole rc{SketchPointRole::P0}; // role within ec
template<class Archive> void serialize(Archive& ar) { ar(type, ea, eb, ra, rb, value, ec, rc); }
};
// Implicit references every sketch has, addressable from a constraint's ea/eb/ec without
// existing as SketchEntity objects. NEGATIVE so they cannot collide with an entity index;
// -1 is already "unset" and stays that way. Values are serialized inside existing int
// fields, so they are append-only in spirit: never renumber these.
constexpr int kSketchRefOrigin = -2; // the sketch origin point (0,0)
constexpr int kSketchRefAxisX = -3; // the sketch X axis, through the origin, +X
constexpr int kSketchRefAxisY = -4; // the sketch Y axis, through the origin, +Y
inline bool is_sketch_ref(int ei) { return ei <= kSketchRefOrigin; }
// How many real endpoints a type exposes, and which roles they are. p1 is UNUSED for
// Circle/Point/Ellipse (SketchEntity::p1 above) and reads (0,0) — walking {P0,P1} blindly
// over those invents a phantom endpoint at the origin, which for a pair of Points always
// wins a closest-pair search at distance 0 and binds a role the solver silently refuses.
int sketch_entity_ends(const SketchEntity& e, std::pair<SketchPointRole, Vec2d> out[2]);
bool sketch_closest_ends(const SketchEntity& A, const SketchEntity& B,
SketchPointRole& ra, SketchPointRole& rb, Vec2d& pa, Vec2d& pb);
// 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 {
None, NeedOneEntity, NeedTwoEntities, NeedALine, NeedTwoLines,
NeedTwoRounds, NeedTangentPair, NeedJoinablePoints, NeedMeasurablePoints,
// The following are not in the GUI's current switch but are the faithful outcomes of
// its remaining branches; they need a reason too or the caller cannot tell them apart.
NeedPointAndLine, // Midpoint: one Point + one Line
NeedTwoPointsOrLines, // Symmetric / SymmetricAboutX/Y: two Points or two Lines
NeedAxisLine, // Symmetric: e2 must be a Line to act as the axis
NeedRound, // Radius/Diameter: a Circle or Arc
Unsupported // entity-constraint path has no binding for this type
};
struct ConstraintPlan {
enum class Kind { Reject, Apply, AskValue };
Kind kind{Kind::Reject};
ConstraintReject reason{ConstraintReject::None};
// Apply/AskValue only: the defs to commit. One element for every ordinary type, TWO for
// Symmetric/SymmetricAboutX/Y on two lines (P0/P0 and P1/P1), matching the GUI's builds.
std::vector<SketchEntityConstraintDef> defs{};
double prefill{0.0}; // AskValue only: the value to show pre-filled
};
// Pure: no wx, no translation, no UI. The caller maps `reason` to a localized string.
// e2 is the axis-line pick Symmetric needs (def.ec); every other type ignores it.
ConstraintPlan plan_entity_constraint(const std::vector<SketchEntity>& ents,
int e0, int e1, int e2, SketchConstraintType type);
// Solve a bare entity list in place against entity-form constraints. Shared by
// CadDocument::solve_sketch_feature (committed features) and the in-session GUI
// sketch tool (live solving as dimensions/constraints are added). Returns true on
// convergence; an empty constraint list is a no-op that returns true.
bool solve_sketch_entities(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints);
struct SketchParams {
// Extrude/Revolve
double extrude_len{10}; bool extrude_sym{false}; double extrude_taper{0};
double revolve_deg{360};
bool is_pocket{false}; // cut into selected object instead of new
// Dress-up
bool dressup_enabled{false};
DressUpType dressup_type{DressUpType::Fillet};
FaceGroup dressup_faces{FaceGroup::All};
double dressup_radius{1.0};
double dressup_chamfer_dist{1.0};
// Mesh
double linear_deflection{0.01};
template<class Archive>
void serialize(Archive& ar) {
ar(extrude_len, extrude_sym, extrude_taper, revolve_deg, is_pocket,
dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist,
linear_deflection);
}
};
class SketchEngine
{
public:
static TopoDS_Shape make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane,
double length, bool symmetric = false, double taper_deg = 0.0);
static TopoDS_Shape make_extrude(const TopoDS_Face& face, const SketchPlane& plane,
double length, bool symmetric = false, double taper_deg = 0.0);
// Asymmetric two-sided prism: extrude the wire's face by `up` along +normal and `down`
// along -normal, fused into one solid. up/down are non-negative magnitudes.
// Tapered (draft) extrude of a planar wire: the top profile is the base wire offset in its
// plane by length*tan(taper_deg), lofted from base to top. Falls back to a straight prism on
// any failure (self-intersecting offset / loft error). taper_deg>0 widens the top.
static TopoDS_Shape make_extrude_taper(const TopoDS_Wire& wire, const SketchPlane& plane,
double length, double taper_deg);
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Wire& wire, const SketchPlane& plane,
double up, double down);
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Face& face, const SketchPlane& plane,
double up, double down);
static TopoDS_Shape make_extrude_face(const TopoDS_Face& face, const SketchPlane& plane,
double length, bool symmetric = false, double taper_deg = 0.0);
// Extrude a set of imported rigid regions (Text/SVG). Each region is
// contour[0]=outer loop + contour[1..]=hole loops, in plane (u,v) mm. Builds
// one planar face-with-holes per region, extrudes it, and fuses all region
// solids into a single shape. Empty/degenerate contours are skipped.
static TopoDS_Shape make_extrude_regions(
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);
// 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
// perpendicular to the spine along its length. The path may be open or closed;
// for a clean solid the path's first point should sit on/near the profile plane.
static TopoDS_Shape make_sweep(const TopoDS_Wire& profile, const TopoDS_Wire& path);
// Loft a solid through 2+ closed profile wires (each on its own plane), in the
// given order. ruled=true => straight (ruled) sections; false => smooth (C2).
static TopoDS_Shape make_loft(const std::vector<TopoDS_Wire>& profiles, bool ruled);
// Skin `profiles` WITHOUT end caps -> an open shell (sheet). Same as make_loft but the
// ThruSections solid flag is false. // ponytail: a sibling instead of a bool param, so no
// existing call site changes.
static TopoDS_Shape make_loft_surface(const std::vector<TopoDS_Wire>& profiles, bool ruled);
static TopoDS_Shape make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane,
const TopoDS_Shape& target, double depth);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
double linear_deflection = 0.01,
double angular_deflection = 0.5);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
std::vector<int>& tri_face,
double linear_deflection = 0.01,
double angular_deflection = 0.5);
static TopoDS_Wire entities_to_wire(const std::vector<SketchEntity>& entities,
const SketchPlane& plane,
bool closed_only = false);
// Every loop the sketch holds, in the order each loop's FIRST entity appears in
// `entities`. A Circle or Ellipse is a loop on its own; Line/Arc/EllipseArc/BSpline
// entities are grouped into loops by shared endpoints. An OPEN chain is returned too —
// a sweep path is legitimately open, so open-ness is not an error here — unless
// `closed_only` is true, in which case an open chain is DISCARDED (skipped, not an
// error). Empty vector = nothing usable; the caller decides whether that is an error.
static std::vector<TopoDS_Wire> entities_to_wires(const std::vector<SketchEntity>& entities,
const SketchPlane& plane,
bool closed_only = false);
// A planar face from a set of coplanar loops: the largest-area loop is the outer boundary
// and every other loop is a hole in it. Throws std::runtime_error with a message naming the
// problem when the loops do not describe one such region.
static TopoDS_Face wires_to_face(const std::vector<TopoDS_Wire>& wires,
const SketchPlane& plane);
static std::vector<SketchEntity> mirror_entities(
const std::vector<SketchEntity>& src, const Vec2d& a, const Vec2d& b);
// Offset a sketch by `d`, PRESERVING CHAINS. Entities joined by shared endpoints are
// 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
// kind of curve) and are dropped from the result.
static std::vector<SketchEntity> offset_entities(
const std::vector<SketchEntity>& src, double d);
// Rigid-transform array. Returns the (count-1) copies for instance i=1..count-1
// (the originals in `src` are NOT included). Each copy i is `src` rigidly
// transformed by: rotate by i*angle_step about `pivot`, then translate by i*step.
// Rectangular/linear array: angle_step = 0, step = spacing*direction (pivot unused).
// Polar array: step = (0,0), angle_step = sweep/count, pivot = centre.
// Orientation-preserving, so arc/ellipse parametric angles shift by i*angle_step.
static std::vector<SketchEntity> array_entities(
const std::vector<SketchEntity>& src, int count,
const Vec2d& step, double angle_step, const Vec2d& pivot);
// General affine transform (move / rotate / scale), applied IN PLACE: returns
// the SAME entities (same count and order), each mapped by
// p -> pivot + scale * R(angle) * (p - pivot) + move
// (radii scale by |scale|; arc/ellipse parametric/rotation angles shift by
// `angle`). Unlike array_entities this mutates the subjects rather than adding
// copies. Move: angle=0, scale=1. Rotate-in-place: move=(0,0), scale=1,
// pivot=centroid. Scale: angle=0.
static std::vector<SketchEntity> transform_entities(
const std::vector<SketchEntity>& src,
const Vec2d& move, double angle, double scale, const Vec2d& pivot);
static bool fillet_lines(const SketchEntity& a, const SketchEntity& b, double r,
SketchEntity& a_out, SketchEntity& b_out, SketchEntity& arc_out);
// Symmetric chamfer between two lines meeting at a corner: trims each line back
// by setback distance `d` from the shared corner and returns the connecting
// straight segment (seg_out) in place of the corner. a_out/b_out are the trimmed
// lines; seg_out goes seg_out.p0 (on a) -> seg_out.p1 (on b). False if the lines
// are parallel or `d` overruns either line.
static bool chamfer_lines(const SketchEntity& a, const SketchEntity& b, double d,
SketchEntity& a_out, SketchEntity& b_out, SketchEntity& seg_out);
static bool trim_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
const Vec2d& pick);
static bool extend_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
const Vec2d& pick);
// Build a cubic-Bezier G1 bridge (as a BSpline entity, 4 poles) connecting endpoint
// `a_end` of `a` to endpoint `b_end` of `b` (0 = start/p0 side, 1 = end/p1 side).
// Tangent-continuous with both entities where the endpoint tangent is defined.
static SketchEntity make_bridge(const SketchEntity& a, int a_end,
const SketchEntity& b, int b_end);
};
// Free endpoints of a sketch: the sketch-space points where a chain fails to close.
// Same weld tolerance as the wire build, so it can never contradict it.
std::vector<Vec2d> sketch_open_ends(const std::vector<SketchEntity>&, const SketchPlane&);
} // namespace Slic3r
#endif // slic3r_SketchEngine_hpp_
+145
View File
@@ -0,0 +1,145 @@
#include "libslic3r/CAD/SketchImport.hpp"
#include "libslic3r/Emboss.hpp"
#include "libslic3r/NSVGUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/TextConfiguration.hpp" // FontProp
#include "libslic3r/libslic3r.h" // SCALING_FACTOR
#include "libslic3r/Utils.hpp" // resources_dir
#include <algorithm>
#include <limits>
namespace Slic3r {
// Convert one ExPolygon (outer contour + CW holes) into an ImportRegion,
// mapping each integer Point to plane (u,v) mm via `to_mm`.
template<class ToMm>
static ImportRegion expoly_to_region(const ExPolygon& ex, ToMm to_mm)
{
auto contour_pts = [&](const Polygon& poly) {
std::vector<Vec2d> c;
c.reserve(poly.points.size());
for (const Point& p : poly.points)
c.push_back(to_mm(p));
return c;
};
ImportRegion region;
region.push_back(contour_pts(ex.contour));
for (const Polygon& h : ex.holes)
region.push_back(contour_pts(h));
return region;
}
// Shift all regions so their common bounding-box centre sits on the origin
// (Onshape/typical CAD insert places imported art centred on the sketch).
static void center_regions(ImportRegions& regs)
{
double lo_x = std::numeric_limits<double>::max();
double lo_y = std::numeric_limits<double>::max();
double hi_x = -std::numeric_limits<double>::max();
double hi_y = -std::numeric_limits<double>::max();
bool any = false;
for (const auto& region : regs)
for (const auto& contour : region)
for (const Vec2d& p : contour) {
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());
any = true;
}
if (!any) return;
const Vec2d c(0.5 * (lo_x + hi_x), 0.5 * (lo_y + hi_y));
for (auto& region : regs)
for (auto& contour : region)
for (Vec2d& p : contour)
p -= c;
}
static std::string default_font_path()
{
return resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf";
}
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::string& font_path)
{
if (utf8.empty() || size_mm <= 0.0)
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 {};
FontProp prop(static_cast<float>(size_mm)); // per_glyph=false
HealedExPolygons healed = Emboss::text2shapes(fwc, utf8.c_str(), prop);
if (healed.expolygons.empty())
return {};
// Shape points are integers scaled by 1/SHAPE_SCALE in font units;
// get_text_shape_scale collapses (size_in_mm / unit_per_em) * SHAPE_SCALE
// into a single mm-per-shape-unit factor. FreeType y is up already.
const double s = Emboss::get_text_shape_scale(prop, *fwc.font_file);
auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); };
ImportRegions regs;
regs.reserve(healed.expolygons.size());
for (const ExPolygon& ex : healed.expolygons)
regs.push_back(expoly_to_region(ex, to_mm));
center_regions(regs);
return regs;
}
ImportRegions svg_to_regions(const std::string& svg_path, double scale)
{
if (svg_path.empty() || scale <= 0.0)
return {};
NSVGimage_ptr image = nsvgParseFromFile(svg_path, "mm", 96.0f);
if (!image)
return {};
// A filled shape that also carries a stroke would import the stroke as a
// thick outline band wrapped around the fill (the reported "too large line
// width"). For CAD import the fill silhouette is what's wanted, so drop the
// stroke on any shape that has a fill; stroke-only line art is kept.
for (NSVGshape* s = image->shapes; s != nullptr; s = s->next)
if (s->fill.type != NSVG_PAINT_NONE)
s->stroke.type = NSVG_PAINT_NONE;
// tesselation tolerance is in image (mm) scale; 0.3 mm keeps curves smooth
// without exploding the contour count. is_y_negative (default) flips SVG's
// y-down to the sketch's y-up.
NSVGLineParams param(0.3);
ExPolygonsWithIds ids = create_shape_with_ids(*image, param);
// NSVG points are integers scaled by 1/SCALING_FACTOR (param.scale default):
// mm = point * SCALING_FACTOR, then the user scale factor.
const double s = SCALING_FACTOR * scale;
auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); };
ImportRegions regs;
for (const ExPolygonsWithId& w : ids)
for (const ExPolygon& ex : w.expoly)
regs.push_back(expoly_to_region(ex, to_mm));
center_regions(regs);
return regs;
}
ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset,
double scale_x, double scale_y)
{
ImportRegions out = src;
for (auto& region : out)
for (auto& contour : region)
for (Vec2d& p : contour)
p = Vec2d(p.x() * scale_x + offset.x(), p.y() * scale_y + offset.y());
return out;
}
} // namespace Slic3r
+37
View File
@@ -0,0 +1,37 @@
#ifndef slic3r_SketchImport_hpp_
#define slic3r_SketchImport_hpp_
#include "libslic3r/Point.hpp" // Vec2d
#include <string>
#include <vector>
namespace Slic3r {
// 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.
using ImportRegion = std::vector<std::vector<Vec2d>>;
using ImportRegions = std::vector<ImportRegion>;
// Vectorize UTF-8 text into filled regions (mm), centred on the origin.
// `size_mm` is the cap/line height. `font_path` empty -> a bundled default
// 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());
// 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
// vector on any failure.
ImportRegions svg_to_regions(const std::string& svg_path, double scale = 1.0);
// Apply an axis-aligned placement transform to regions:
// p -> ( p.x * scale_x + offset.x, p.y * scale_y + offset.y )
// Used to move / enlarge / stretch imported art non-destructively (the
// feature keeps the centred source regions + this transform).
ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset,
double scale_x, double scale_y);
} // namespace Slic3r
#endif // slic3r_SketchImport_hpp_
+234
View File
@@ -0,0 +1,234 @@
#include "libslic3r/CAD/SketchInference.hpp"
#include <algorithm>
#include <cmath>
namespace Slic3r {
// Candidate target collected during the scan; we keep the closest within each
// priority tier and resolve ties by tier then distance.
namespace {
struct Cand {
InferenceSnap::Kind kind{InferenceSnap::Kind::None};
int entity{-1};
SketchPointRole role{SketchPointRole::P0};
Vec2d point{0, 0};
double dist{0.0};
};
// Lower number = higher priority.
int tier(InferenceSnap::Kind k)
{
switch (k) {
case InferenceSnap::Kind::Endpoint: return 0;
case InferenceSnap::Kind::Center: return 1;
case InferenceSnap::Kind::Origin: return 2;
case InferenceSnap::Kind::Midpoint: return 3;
case InferenceSnap::Kind::OnEdge: return 4;
default: return 9;
}
}
} // namespace
InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
const Vec2d& query, double tol,
bool include_origin)
{
Cand best;
best.kind = InferenceSnap::Kind::None;
best.point = query;
auto offer = [&](InferenceSnap::Kind k, int ent, SketchPointRole r, const Vec2d& q) {
const double d = (q - query).norm();
if (d > tol) return;
const bool better = (best.kind == InferenceSnap::Kind::None) ||
(tier(k) < tier(best.kind)) ||
(tier(k) == tier(best.kind) && d < best.dist);
if (better) { best.kind = k; best.entity = ent; best.role = r; best.point = q; best.dist = d; }
};
for (size_t i = 0; i < entities.size(); ++i) {
const SketchEntity& e = entities[i];
const int ei = int(i);
switch (e.type) {
case SketchEntity::Type::Line: {
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0, 0.5 * (e.p0 + e.p1));
// Projection onto the segment interior (PointOnObject candidate).
const Vec2d d = e.p1 - e.p0;
const double L2 = d.squaredNorm();
if (L2 > 1e-12) {
double t = (query - e.p0).dot(d) / L2;
if (t > 0.02 && t < 0.98)
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::P0, e.p0 + t * d);
}
break;
}
case SketchEntity::Type::Arc: {
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
// Mid-arc point, so an arc is as snappable in its middle as a line is.
const double am = 0.5 * (e.start_angle + e.end_angle);
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0,
Vec2d(e.center.x() + e.radius * std::cos(am),
e.center.y() + e.radius * std::sin(am)));
break;
}
case SketchEntity::Type::Circle: {
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
// Nearest point on the circle rim (PointOnObject candidate).
const Vec2d v = query - e.center;
const double n = v.norm();
if (n > 1e-9 && e.radius > 1e-9)
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::Center,
e.center + v * (e.radius / n));
break;
}
case SketchEntity::Type::Point:
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
break;
case SketchEntity::Type::EllipseArc:
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
break;
case SketchEntity::Type::Ellipse:
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
break;
case SketchEntity::Type::BSpline:
// Endpoints (first/last pole) snap for loop closure.
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
break;
}
}
if (include_origin)
offer(InferenceSnap::Kind::Origin, -1, SketchPointRole::P0, Vec2d(0, 0));
InferenceSnap r;
r.kind = best.kind; r.entity = best.entity; r.role = best.role; r.point = best.point;
return r;
}
std::optional<SketchConstraintType>
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad)
{
const Vec2d d = tip - anchor;
if (d.squaredNorm() < 1e-12) return std::nullopt;
const double ang = std::atan2(std::abs(d.y()), std::abs(d.x())); // 0=horizontal, pi/2=vertical
if (ang <= ang_tol_rad) return SketchConstraintType::Horizontal;
if (ang >= M_PI / 2.0 - ang_tol_rad) return SketchConstraintType::Vertical;
return std::nullopt;
}
// Unsigned angle between two (unnormalized) direction vectors, in [0, pi]. 0 = same
// direction, pi = opposite, pi/2 = perpendicular. Inputs must be non-degenerate.
// static: this is a file-local helper, not part of the module's interface -- at namespace
// scope with external linkage it would be a link-time collision waiting to happen.
static double unsigned_angle(const Vec2d& a, const Vec2d& b)
{
const double cross = a.x() * b.y() - a.y() * b.x();
const double dot = a.x() * b.x() + a.y() * b.y();
return std::atan2(std::abs(cross), dot);
}
std::vector<SketchEntityConstraintDef>
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
double ang_tol_rad, double len_tol_frac)
{
std::vector<SketchEntityConstraintDef> out;
if (new_ei <= 0 || new_ei >= int(entities.size())) return out;
// 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
// EqualRadius candidates, the batch is rejected as over-constrained, and the caller's
// one-at-a-time fallback then runs a solve per constraint. Measured 2026-08-31: that
// pinned the app at 95% of a core with the MCP socket unresponsive -- the same failure
// the axes batch above already carries a warning about. Keep the best candidate only.
int best_ang_j = -1, best_rad_j = -1, best_tan_j = -1;
double best_ang_err = 1e30, best_rad_err = 1e30, best_tan_err = 1e30;
SketchConstraintType best_ang_type = SketchConstraintType::Parallel;
const SketchEntity& n = entities[new_ei];
const bool n_line = n.type == SketchEntity::Type::Line;
const bool n_curve = n.type == SketchEntity::Type::Arc || n.type == SketchEntity::Type::Circle;
if (!n_line && !n_curve) return out; // not a Line / Arc / Circle
if (n_line && (n.p1 - n.p0).squaredNorm() < 1e-18) return out; // degenerate
if (n_curve && n.radius < 1e-9) return out;
for (int j = 0; j < new_ei; ++j) {
const SketchEntity& o = entities[j];
const bool o_line = o.type == SketchEntity::Type::Line;
const bool o_curve = o.type == SketchEntity::Type::Arc || o.type == SketchEntity::Type::Circle;
if (!o_line && !o_curve) continue;
if (o_line && (o.p1 - o.p0).squaredNorm() < 1e-18) continue;
if (o_curve && o.radius < 1e-9) continue;
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;
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);
const double per_err = std::abs(ang - M_PI / 2.0);
if (par_err <= ang_tol_rad && par_err < best_ang_err) {
best_ang_err = par_err; best_ang_j = j;
best_ang_type = SketchConstraintType::Parallel;
} else if (per_err <= ang_tol_rad && per_err < best_ang_err) {
best_ang_err = per_err; best_ang_j = j;
best_ang_type = SketchConstraintType::Perpendicular;
}
} else if (n_curve && o_curve) {
// R2 — equal radius between circles / arcs, relative to the larger.
const double larger = n.radius > o.radius ? n.radius : o.radius;
const double err = std::abs(n.radius - o.radius) / larger;
if (err <= len_tol_frac && err < best_rad_err) { best_rad_err = err; best_rad_j = j; }
} else {
// R3 — tangent where a line meets a circle / arc at a shared endpoint, and only
// when the line is ALREADY perpendicular to the radius at that point.
const SketchEntity& ln = n_line ? n : o;
const SketchEntity& cv = n_line ? o : n;
const Vec2d ldir = ln.p1 - ln.p0;
bool tangent = false;
const Vec2d le[2] = { ln.p0, ln.p1 };
for (int k = 0; k < 2 && !tangent; ++k) {
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;
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;
if (tangent) break;
}
} 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 (r.squaredNorm() < 1e-18) continue;
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
}
}
if (tangent && best_tan_err > 0.0) { best_tan_err = 0.0; best_tan_j = j; }
}
}
auto emit = [&](SketchConstraintType t, int j) {
if (j < 0) return;
SketchEntityConstraintDef c;
c.type = t; c.ea = j; c.eb = new_ei;
out.push_back(c);
};
emit(best_ang_type, best_ang_j); // R1
emit(SketchConstraintType::EqualRadius, best_rad_j); // R2
emit(SketchConstraintType::Tangent, best_tan_j); // R3
return out;
}
} // namespace Slic3r
+55
View File
@@ -0,0 +1,55 @@
#ifndef slic3r_SketchInference_hpp_
#define slic3r_SketchInference_hpp_
#include "libslic3r/CAD/SketchEngine.hpp"
#include <vector>
#include <optional>
#include <cmath>
namespace Slic3r {
// Result of snapping a free cursor point onto the most relevant inference target
// among the committed sketch entities and the sketch origin. This is the backbone
// that lets geometry self-constrain as it is drawn: the GUI records the returned
// target at click time and, once the entity it belongs to exists, emits the
// matching constraint (Coincident onto an endpoint/centre, Fix onto the origin,
// PointOnObject onto an edge) so the relation survives a re-solve.
struct InferenceSnap {
enum class Kind { None, Endpoint, Center, Midpoint, OnEdge, Origin };
Kind kind{Kind::None};
int entity{-1}; // hit entity index (-1 = origin/none)
SketchPointRole role{SketchPointRole::P0}; // which point of `entity` (Endpoint/Center)
Vec2d point{0, 0}; // snapped coordinate (== query when None)
bool snapped() const { return kind != Kind::None; }
};
// Snap `query` onto the best inference target within `tol` plane units. Priority,
// highest first: Endpoint, Center, Origin, Midpoint, OnEdge. Construction entities
// participate (you constrain to them too). Returns {None, query} when nothing is in
// range. Pure — no GUI / GL dependencies, so it is unit-testable in libslic3r.
InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
const Vec2d& query, double tol,
bool include_origin = true);
// Relational inference for an in-progress segment anchor->tip. If its direction is
// 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.
std::optional<SketchConstraintType>
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = 3.0 * M_PI / 180.0);
// 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.
//
// Deliberately conservative: every rule requires the relation to be ALREADY TRUE within
// tolerance, so an inferred constraint never moves geometry the user drew — it only pins a
// 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 len_tol_frac = 0.01);
} // namespace Slic3r
#endif // slic3r_SketchInference_hpp_
+555
View File
@@ -0,0 +1,555 @@
#include "libslic3r/CAD/SketchSolver.hpp"
#include <slvs.h>
#include <cmath>
#include <cstring>
#include <functional>
#include <map>
#include <unordered_map>
namespace Slic3r {
using CT = SketchConstraintType;
using Role = SketchPointRole;
namespace {
constexpr Slvs_hGroup G_FIXED = 1; // workplane / reference: held constant
constexpr Slvs_hGroup G_SK = 2; // sketch geometry: the group we solve
// Per-entity slvs handles. p0/p1/center are point2d entity handles; prim is the
// line/arc/circle entity; rparam is the circle radius param.
struct Slots {
Slvs_hEntity prim{0}, p0{0}, p1{0}, center{0};
Slvs_hParam rparam{0};
std::vector<Slvs_hEntity> pts; // BSpline control points (point2d handles)
};
struct Build {
std::vector<Slvs_Param> params;
std::vector<Slvs_Entity> ents;
std::vector<Slvs_Constraint> cons;
Slvs_hParam ph{0};
Slvs_hEntity eh{0};
Slvs_hConstraint ch{0};
Slvs_hEntity wp{0}, normal{0};
Slvs_hParam P(Slvs_hGroup g, double v) { params.push_back(Slvs_MakeParam(++ph, g, v)); return ph; }
Slvs_hEntity E(Slvs_Entity e) { ents.push_back(e); return e.h; }
Slvs_hEntity pt2d(Slvs_hGroup g, double u, double v)
{ return E(Slvs_MakePoint2d(++eh, g, wp, P(g, u), P(g, v))); }
// Generic constraint (entityC unused by Slvs_MakeConstraint — set it manually below).
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_Constraint c = Slvs_MakeConstraint(++ch, G_SK, type, wp, val, ptA, ptB, eA, eB);
c.entityC = eC;
c.other = other;
cons.push_back(c);
}
};
inline int role_idx(Role r) { return int(r); }
} // namespace
static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, Role dragged_role)
{
SketchSolveResult out;
if (constraints.empty()) { out.ok = true; out.dof = -1; return out; }
Build b;
// ---- Fixed 2D XY workplane (origin at 0,0,0; identity normal) -------------------
Slvs_hEntity origin = b.E(Slvs_MakePoint3d(++b.eh, G_FIXED,
b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0)));
double qw, qx, qy, qz;
Slvs_MakeQuaternion(1, 0, 0, 0, 1, 0, &qw, &qx, &qy, &qz);
b.normal = b.E(Slvs_MakeNormal3d(++b.eh, G_FIXED,
b.P(G_FIXED, qw), b.P(G_FIXED, qx), b.P(G_FIXED, qy), b.P(G_FIXED, qz)));
b.wp = b.E(Slvs_MakeWorkplane(++b.eh, G_FIXED, origin, b.normal));
// Unit direction references for the axis-projected distance constraints. Both live in
// G_FIXED, so they are held constant and add no DOF to the system.
// libslvs defines a LINE_SEGMENT's direction as point[0] - point[1] (entity.cpp
// VectorGetExprs), so the unit vector's head is listed first to yield +X / +Y.
const Slvs_hEntity dir_x [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 1.0, 0.0), b.pt2d(G_FIXED, 0.0, 0.0)));
const Slvs_hEntity dir_y [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 0.0, 1.0), b.pt2d(G_FIXED, 0.0, 0.0)));
// Implicit sketch references (origin, X axis, Y axis), addressable by the negative
// sentinels in SketchEngine.hpp. G_FIXED: held constant, zero added DOF. The axis lines
// are built head-first so their direction reads +X / +Y, matching dir_x / dir_y.
const Slvs_hEntity ref_origin_pt = b.pt2d(G_FIXED, 0.0, 0.0);
const Slvs_hEntity ref_axis_x = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 1.0, 0.0), ref_origin_pt));
const Slvs_hEntity ref_axis_y = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 0.0, 1.0), ref_origin_pt));
// ---- Entities -------------------------------------------------------------------
std::vector<Slots> slot(entities.size());
for (size_t i = 0; i < entities.size(); ++i) {
const SketchEntity& e = entities[i];
Slots s;
switch (e.type) {
case SketchEntity::Type::Line:
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y());
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y());
s.prim = b.E(Slvs_MakeLineSegment(++b.eh, G_SK, b.wp, s.p0, s.p1));
break;
case SketchEntity::Type::Point:
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y());
break;
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));
break;
}
case SketchEntity::Type::Arc:
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end
s.prim = b.E(Slvs_MakeArcOfCircle(++b.eh, G_SK, b.wp, b.normal, s.center, s.p0, s.p1));
break;
// libslvs has no conic entity (scope note): register the ellipse's defining
// points only (center + arc endpoints) so center/endpoint constraints solve;
// the a/b/phi shape params pass through unsolved.
case SketchEntity::Type::Ellipse:
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = s.center; // p0 mirrors centre (circle convention)
break;
case SketchEntity::Type::EllipseArc:
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end
break;
// No native slvs curve for an arbitrary-degree spline: register the control
// poles as point2d so endpoints (and any pole-targeted constraint) solve. The
// OCCT curve is rebuilt from the solved poles. p0/p1 mirror first/last pole so
// Coincident at the spline ends closes loops just like a Line.
case SketchEntity::Type::BSpline:
s.pts.reserve(e.ctrl.size());
for (const Vec2d& cp : e.ctrl)
s.pts.push_back(b.pt2d(G_SK, cp.x(), cp.y()));
if (!s.pts.empty()) { s.p0 = s.pts.front(); s.p1 = s.pts.back(); }
break;
}
slot[i] = s;
}
auto valid = [&](int ei) { return ei >= 0 && ei < int(entities.size()); };
auto ptOf = [&](int ei, Role r) -> Slvs_hEntity {
if (ei == kSketchRefOrigin) return ref_origin_pt;
if (ei == kSketchRefAxisX || ei == kSketchRefAxisY) return ref_origin_pt; // axes pass through it
if (!valid(ei)) return 0;
const Slots& s = slot[ei];
switch (r) {
case Role::P0: return s.p0;
case Role::P1: return s.p1;
case Role::Center: return s.center ? s.center : s.p0;
}
return 0;
};
auto primOf = [&](int ei) -> Slvs_hEntity {
if (ei == kSketchRefAxisX) return ref_axis_x;
if (ei == kSketchRefAxisY) return ref_axis_y;
return valid(ei) ? slot[ei].prim : 0; // origin has no prim: it is a point
};
auto coordOf = [&](int ei, Role r) -> Vec2d {
if (is_sketch_ref(ei)) return Vec2d(0, 0); // all three pass through the origin
if (!valid(ei)) return Vec2d(0, 0);
const SketchEntity& e = entities[ei];
switch (r) { case Role::P0: return e.p0; case Role::P1: return e.p1; case Role::Center: return e.center; }
return e.p0;
};
// 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); };
// ---- Constraints ----------------------------------------------------------------
for (const auto& c : constraints) {
// Robustness: never feed libslvs a null handle. A constraint that references an
// entity which produced no solver primitive (Point/Ellipse/EllipseArc/BSpline get
// no `prim`) or no point for the requested role would make Slvs FindById abort the
// whole process. Skip such a constraint instead of crashing.
bool ref_ok = true;
switch (c.type) {
case CT::Coincident: case CT::Horizontal: case CT::Vertical: case CT::Distance:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
case CT::DistanceX:
case CT::DistanceY:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
case CT::Concentric:
ref_ok = ptOf(c.ea, Role::Center) && ptOf(c.eb, Role::Center); break;
case CT::Fix: case CT::LockX: case CT::LockY:
ref_ok = ptOf(c.ea, c.ra) != 0; break;
case CT::EqualLength: case CT::Parallel: case CT::Perpendicular:
case CT::Angle: case CT::Tangent:
ref_ok = primOf(c.ea) && primOf(c.eb); break;
case CT::Radius: case CT::Diameter:
ref_ok = primOf(c.ea) != 0; break;
case CT::Midpoint:
ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break;
case CT::Symmetric:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb) && primOf(c.ec); break;
case CT::SymmetricAboutY: case CT::SymmetricAboutX:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
case CT::PointOnLine: case CT::PointOnObject:
ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break;
case CT::EqualRadius:
case CT::Collinear:
ref_ok = primOf(c.ea) && primOf(c.eb); break;
}
if (!ref_ok) 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);
break;
case CT::Concentric:
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, Role::Center), ptOf(c.eb, Role::Center), 0, 0);
break;
case CT::Horizontal:
b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
break;
case CT::Vertical:
b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
break;
case CT::Distance:
b.C(SLVS_C_PT_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
break;
case CT::DistanceX:
// Distance between the two points measured along X only: project the vector
// between them onto the fixed unit X direction.
b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_x, 0);
break;
case CT::DistanceY:
b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_y, 0);
break;
case CT::Fix: {
const Vec2d p = coordOf(c.ea, c.ra);
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), p.y()), 0, 0);
break;
}
case CT::LockX: {
const Vec2d p = coordOf(c.ea, c.ra);
b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), fixedRef(c.value, p.y()), 0, 0);
break;
}
case CT::LockY: {
const Vec2d p = coordOf(c.ea, c.ra);
b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), c.value), 0, 0);
break;
}
case CT::EqualLength:
b.C(SLVS_C_EQUAL_LENGTH_LINES, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Parallel:
b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Perpendicular:
b.C(SLVS_C_PERPENDICULAR, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Midpoint:
b.C(SLVS_C_AT_MIDPOINT, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
break;
case CT::Symmetric:
// ptA, ptB symmetric about the axis line (ec).
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(c.ec), 0);
break;
case CT::SymmetricAboutY:
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(kSketchRefAxisY), 0);
break;
case CT::SymmetricAboutX:
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(kSketchRefAxisX), 0);
break;
case CT::Angle:
// model stores radians; slvs angle is in degrees.
b.C(SLVS_C_ANGLE, c.value * 180.0 / M_PI, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Radius:
b.C(SLVS_C_DIAMETER, 2.0 * c.value, 0, 0, primOf(c.ea), 0);
break;
case CT::Diameter:
b.C(SLVS_C_DIAMETER, c.value, 0, 0, primOf(c.ea), 0);
break;
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 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) {
// A FULL circle cannot use SLVS_C_ARC_LINE_TANGENT. That constraint reads
// arc->point[1] / point[2] — the arc's endpoints (see constrainteq.cpp,
// Type::ARC_LINE_TANGENT) — and a circle entity only has point[0], its
// centre. The zero handles send FindById into "Cannot find handle", which
// ABORTS the process rather than failing the solve, taking every later test
// with it. It is also the wrong equation for a circle: it only makes the
// line perpendicular to the radius AT AN ENDPOINT that does not exist.
//
// For a circle, tangency is exactly "the centre sits one radius away from
// the line", which slvs expresses directly.
//
// ponytail: the radius is captured here rather than tied as a variable —
// the C API takes a constant distance and offers no way to reference the
// circle's radius parameter. Exact whenever the radius is fixed or simply
// 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,
ptOf(ci, Role::Center), 0, primOf(li), 0);
} else {
b.C(SLVS_C_ARC_LINE_TANGENT, 0, 0, 0, primOf(ci), primOf(li));
}
}
break;
}
case CT::PointOnLine:
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);
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)
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);
break;
case CT::EqualRadius:
b.C(SLVS_C_EQUAL_RADIUS, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Collinear:
// libslvs has no collinear code. Two lines are collinear iff they are parallel
// AND a point of one lies on the other's infinite line — emit both.
b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb));
// Point-on-infinite-line via PT_LINE_DISTANCE=0 rather than PT_ON_LINE: the
// latter creates an internal `valP` param that this port's Slvs_Solve leaves at
// 0 in the working set (ModifyToSatisfy only updates SK.param), so an already
// collinear pair drifts. PT_LINE_DISTANCE=0 is the same condition with no extra
// parameter, so an already-satisfied solve is a clean no-op.
b.C(SLVS_C_PT_LINE_DISTANCE, 0, ptOf(c.eb, Role::P0), 0, primOf(c.ea), 0);
break;
}
}
// ---- Solve ----------------------------------------------------------------------
Slvs_System sys;
std::memset(&sys, 0, sizeof(sys));
sys.param = b.params.data(); sys.params = int(b.params.size());
sys.entity = b.ents.data(); sys.entities = int(b.ents.size());
sys.constraint = b.cons.data(); sys.constraints = int(b.cons.size());
std::vector<Slvs_hConstraint> failed(b.cons.size() + 1, 0);
sys.failed = failed.data();
sys.faileds = int(failed.size());
sys.calculateFaileds = 1;
// Drag pin: feed the dragged point's two params into sys.dragged[] so the solver
// favours keeping that point at the cursor and re-solves the rest around it.
if (dragged_ei >= 0) {
const Slvs_hEntity h = ptOf(dragged_ei, dragged_role);
for (const Slvs_Entity& en : b.ents)
if (en.h == h) { sys.dragged[0] = en.param[0]; sys.dragged[1] = en.param[1]; break; }
}
Slvs_Solve(&sys, G_SK);
out.result = sys.result;
out.dof = sys.dof;
out.ok = (sys.result == SLVS_RESULT_OKAY);
// Map solved param handles -> values, then read points back.
std::unordered_map<Slvs_hParam, double> pv;
pv.reserve(sys.params * 2);
for (int i = 0; i < sys.params; ++i) pv[sys.param[i].h] = sys.param[i].val;
std::unordered_map<Slvs_hEntity, const Slvs_Entity*> byH;
byH.reserve(sys.entities * 2);
for (int i = 0; i < sys.entities; ++i) byH[sys.entity[i].h] = &sys.entity[i];
auto coord = [&](Slvs_hEntity h) -> Vec2d {
auto it = byH.find(h);
if (it == byH.end()) return Vec2d(0, 0);
return Vec2d(pv[it->second->param[0]], pv[it->second->param[1]]);
};
// Map failed constraint handles back to indices into `constraints`.
if (!out.ok && sys.faileds > 0) {
std::unordered_map<Slvs_hConstraint, int> chToIdx;
// constraint handles were assigned in order starting after the fixed group; the
// i-th sketch constraint in b.cons has handle = its position. Rebuild by scanning.
for (size_t k = 0; k < b.cons.size(); ++k) chToIdx[b.cons[k].h] = int(k);
for (int i = 0; i < sys.faileds; ++i) {
auto it = chToIdx.find(failed[i]);
if (it != chToIdx.end() && it->second < int(constraints.size()))
out.bad.push_back(it->second);
}
}
// ---- Read solved geometry back --------------------------------------------------
// ONLY on success. A failed solve leaves libslvs' params holding its last Newton
// iterate — geometry that satisfies nothing and is usually wildly deformed. Writing
// that back made every rejected attempt destructive: the caller rolls the constraints
// back, but the sketch it rolls back to is already wreckage, so the next attempt starts
// from the corpse. The fillet degrade ladder hit this on every corner — rung 1 (a
// tangent on each leg) is legitimately over-constrained against the legs' own H/V, and
// its wreckage then failed rungs 2 and 3, which solve cleanly on their own. The arc
// ended up with no constraints at all and the solver snapped the corner shut. pl5.
if (!out.ok) return out;
for (size_t i = 0; i < entities.size(); ++i) {
SketchEntity& e = entities[i];
const Slots& s = slot[i];
if (s.p0) e.p0 = coord(s.p0);
if (s.p1) e.p1 = coord(s.p1);
if (s.center) e.center = coord(s.center);
if (e.type == SketchEntity::Type::BSpline) {
for (size_t k = 0; k < s.pts.size() && k < e.ctrl.size(); ++k)
e.ctrl[k] = coord(s.pts[k]);
if (!e.ctrl.empty()) { e.p0 = e.ctrl.front(); e.p1 = e.ctrl.back(); }
} else if (e.type == SketchEntity::Type::Circle) {
if (s.rparam) { auto it = pv.find(s.rparam); if (it != pv.end()) e.radius = it->second; }
e.p0 = e.center;
} else if (e.type == SketchEntity::Type::Arc && s.center) {
// Reflow arc angles from solved centre + endpoints, preserving sweep sign.
const double old_sweep = e.end_angle - e.start_angle;
const double ns = std::atan2(e.p0.y() - e.center.y(), e.p0.x() - e.center.x());
const double ne = std::atan2(e.p1.y() - e.center.y(), e.p1.x() - e.center.x());
double sweep = ne - ns;
const double TWO_PI = 2.0 * M_PI;
while (sweep <= -TWO_PI) sweep += TWO_PI;
while (sweep >= TWO_PI) sweep -= TWO_PI;
if (old_sweep >= 0.0 && sweep < 0.0) sweep += TWO_PI;
if (old_sweep < 0.0 && sweep > 0.0) sweep -= TWO_PI;
e.start_angle = ns;
e.end_angle = ns + sweep;
e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm());
}
}
return out;
}
// libslvs carries a COMPILE-TIME ceiling: solvespace.h declares `enum { MAX_UNKNOWNS = 1024 }`
// and sizes the System's param and equation arrays with it. solve_system() hands the solver every
// entity in the sketch, constrained or not, at 2 params per point — so a sketch of about 480 lines
// is the last one that fits, and the very next one comes back TOO_MANY_UNKNOWNS.
//
// What that did, before this: DesignSketchTool::try_add_constraints rolls the whole batch back
// when the solve fails, so the auto-constraint pass over a large sketch dropped EVERY constraint
// it had just inferred. Measured on the rig — 480 lines: 960 constraints, dof 480. 520 lines:
// 0 constraints, dof unknown. Nothing was said, and from there on no dimension and no constraint
// could ever be applied to that sketch, because each attempt re-solved the same oversized system
// and was rejected in turn. A typed length simply did nothing.
//
// Constraints only couple entities that SHARE a point, so a sketch is naturally a set of
// independent systems — a plate with 300 cut-outs is 301 little problems, not one big one.
// Solving them separately keeps every one of them far under the ceiling AND is faster, since the
// solver's work is superlinear in system size.
//
// The whole system is still tried FIRST, and this runs only on TOO_MANY_UNKNOWNS, so every sketch
// 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.
static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, Role dragged_role)
{
const int n = int(entities.size());
std::vector<int> parent(n);
for (int i = 0; i < n; ++i) parent[i] = i;
std::function<int(int)> find = [&](int a) {
while (parent[a] != a) { parent[a] = parent[parent[a]]; a = parent[a]; }
return a;
};
auto unite = [&](int a, int b) {
if (a < 0 || b < 0 || a >= n || b >= n) return;
a = find(a); b = find(b);
if (a != b) parent[a] = b;
};
for (const auto& c : constraints) { unite(c.ea, c.eb); unite(c.ea, c.ec); }
// Group the constraints by the component they belong to.
std::map<int, std::vector<int>> groups;
for (size_t i = 0; i < constraints.size(); ++i) {
const int a = constraints[i].ea;
if (a < 0 || a >= n) continue;
groups[find(a)].push_back(int(i));
}
SketchSolveResult out;
out.ok = true;
out.dof = 0;
// Solve into COPIES and commit only if every component succeeded. The contract callers rely
// on is all-or-nothing — try_add_constraints rolls the batch back and expects the geometry it
// rolls back to be untouched — and partial writes would break it.
std::vector<std::pair<std::vector<int>, std::vector<SketchEntity>>> solved;
for (const auto& [root, cidx] : groups) {
std::vector<int> ents; // global indices, in order
std::map<int, int> local; // global -> local
auto take = [&](int e) {
if (e < 0 || e >= n || local.count(e)) return;
local[e] = int(ents.size());
ents.push_back(e);
};
for (int ci : cidx) { take(constraints[ci].ea); take(constraints[ci].eb); take(constraints[ci].ec); }
std::vector<SketchEntity> sub;
sub.reserve(ents.size());
for (int e : ents) sub.push_back(entities[e]);
std::vector<SketchEntityConstraintDef> subc;
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; };
map1(d.ea); map1(d.eb); map1(d.ec);
subc.push_back(d);
}
const int sub_drag = (dragged_ei >= 0 && local.count(dragged_ei)) ? local[dragged_ei] : -1;
SketchSolveResult r = solve_system(sub, subc, sub_drag, dragged_role);
if (!r.ok) {
out.ok = false;
out.result = r.result;
for (int bi : r.bad)
if (bi >= 0 && bi < int(cidx.size())) out.bad.push_back(cidx[bi]);
}
if (r.dof > 0) out.dof += r.dof;
solved.emplace_back(std::move(ents), std::move(sub));
}
if (!out.ok) return out;
for (auto& [ents, sub] : solved)
for (size_t k = 0; k < ents.size(); ++k) entities[ents[k]] = sub[k];
return out;
}
static SketchSolveResult solve_impl(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, Role dragged_role)
{
SketchSolveResult out = solve_system(entities, constraints, dragged_ei, dragged_role);
if (out.ok || out.result != SLVS_RESULT_TOO_MANY_UNKNOWNS) return out;
return solve_partitioned(entities, constraints, dragged_ei, dragged_role);
}
SketchSolveResult sketch_solve(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints)
{
return solve_impl(entities, constraints, -1, Role::P0);
}
SketchSolveResult sketch_solve_drag(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, SketchPointRole dragged_role)
{
return solve_impl(entities, constraints, dragged_ei, dragged_role);
}
} // namespace Slic3r
+37
View File
@@ -0,0 +1,37 @@
#ifndef slic3r_SketchSolver_hpp_
#define slic3r_SketchSolver_hpp_
// Bridge from the Design tab's SketchEntity / SketchEntityConstraintDef model onto the
// vendored SolveSpace constraint solver (src/libslic3r/slvs, libslvs). Replaces the
// hand-rolled SketchConstraints: full constraint set, real DoF counting, and
// over-constrained (bad-constraint) detection. Solves on a fixed 2D XY workplane.
#include "libslic3r/CAD/SketchEngine.hpp"
#include <vector>
namespace Slic3r {
struct SketchSolveResult {
bool ok{false}; // solver converged & consistent
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
};
// Solve `constraints` over `entities` in place (writes solved coordinates back into the
// entities; arc angles are reflowed preserving sweep direction). No-op success when
// `constraints` is empty.
SketchSolveResult sketch_solve(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints);
// Drag-aware solve: pins the (dragged_ei, dragged_role) point's parameters via the
// solver's `dragged[]` priority list so the solver keeps that point where the cursor
// placed it (caller must have moved it first) and moves the OTHER free geometry to
// re-satisfy the constraints. dragged_ei < 0 behaves identically to sketch_solve.
SketchSolveResult sketch_solve_drag(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, SketchPointRole dragged_role);
} // namespace Slic3r
#endif
+95
View File
@@ -0,0 +1,95 @@
#include "libslic3r/CAD/ThreadStandards.hpp"
namespace Slic3r {
// Imperial helpers: convert nominal inch diameter / threads-per-inch to mm.
static constexpr double IN = 25.4;
static inline double tpi_pitch(double tpi) { return IN / tpi; }
const std::vector<ThreadSpec>& thread_standards()
{
using S = ThreadSpec::Series;
static const std::vector<ThreadSpec> table = {
// --- ISO metric, coarse pitch (ISO 261 preferred series) ---
{"M1", 1.0, 0.25, S::MetricCoarse},
{"M1.2", 1.2, 0.25, S::MetricCoarse},
{"M1.6", 1.6, 0.35, S::MetricCoarse},
{"M2", 2.0, 0.40, S::MetricCoarse},
{"M2.5", 2.5, 0.45, S::MetricCoarse},
{"M3", 3.0, 0.50, S::MetricCoarse},
{"M4", 4.0, 0.70, S::MetricCoarse},
{"M5", 5.0, 0.80, S::MetricCoarse},
{"M6", 6.0, 1.00, S::MetricCoarse},
{"M8", 8.0, 1.25, S::MetricCoarse},
{"M10", 10.0, 1.50, S::MetricCoarse},
{"M12", 12.0, 1.75, S::MetricCoarse},
{"M14", 14.0, 2.00, S::MetricCoarse},
{"M16", 16.0, 2.00, S::MetricCoarse},
{"M20", 20.0, 2.50, S::MetricCoarse},
{"M24", 24.0, 3.00, S::MetricCoarse},
{"M30", 30.0, 3.50, S::MetricCoarse},
{"M36", 36.0, 4.00, S::MetricCoarse},
{"M42", 42.0, 4.50, S::MetricCoarse},
{"M48", 48.0, 5.00, S::MetricCoarse},
{"M56", 56.0, 5.50, S::MetricCoarse},
{"M64", 64.0, 6.00, S::MetricCoarse},
// --- ISO metric, common fine pitches (ISO 261 fine series) ---
{"M8x1", 8.0, 1.00, S::MetricFine},
{"M10x1.25", 10.0, 1.25, S::MetricFine},
{"M10x1", 10.0, 1.00, S::MetricFine},
{"M12x1.5", 12.0, 1.50, S::MetricFine},
{"M12x1.25", 12.0, 1.25, S::MetricFine},
{"M16x1.5", 16.0, 1.50, S::MetricFine},
{"M20x1.5", 20.0, 1.50, S::MetricFine},
{"M24x2", 24.0, 2.00, S::MetricFine},
// --- Unified National Coarse (UTS / ASME B1.1) ---
{"#1-64 UNC", 0.073 * IN, tpi_pitch(64), S::UNC},
{"#2-56 UNC", 0.086 * IN, tpi_pitch(56), S::UNC},
{"#3-48 UNC", 0.099 * IN, tpi_pitch(48), S::UNC},
{"#4-40 UNC", 0.112 * IN, tpi_pitch(40), S::UNC},
{"#5-40 UNC", 0.125 * IN, tpi_pitch(40), S::UNC},
{"#6-32 UNC", 0.138 * IN, tpi_pitch(32), S::UNC},
{"#8-32 UNC", 0.164 * IN, tpi_pitch(32), S::UNC},
{"#10-24 UNC", 0.190 * IN, tpi_pitch(24), S::UNC},
{"#12-24 UNC", 0.216 * IN, tpi_pitch(24), S::UNC},
{"1/4-20 UNC", 0.250 * IN, tpi_pitch(20), S::UNC},
{"5/16-18 UNC", 0.3125 * IN, tpi_pitch(18), S::UNC},
{"3/8-16 UNC", 0.375 * IN, tpi_pitch(16), S::UNC},
{"7/16-14 UNC", 0.4375 * IN, tpi_pitch(14), S::UNC},
{"1/2-13 UNC", 0.500 * IN, tpi_pitch(13), S::UNC},
{"9/16-12 UNC", 0.5625 * IN, tpi_pitch(12), S::UNC},
{"5/8-11 UNC", 0.625 * IN, tpi_pitch(11), S::UNC},
{"3/4-10 UNC", 0.750 * IN, tpi_pitch(10), S::UNC},
{"7/8-9 UNC", 0.875 * IN, tpi_pitch(9), S::UNC},
{"1-8 UNC", 1.000 * IN, tpi_pitch(8), S::UNC},
// --- Unified National Fine (UTS / ASME B1.1) ---
{"#2-64 UNF", 0.086 * IN, tpi_pitch(64), S::UNF},
{"#4-48 UNF", 0.112 * IN, tpi_pitch(48), S::UNF},
{"#6-40 UNF", 0.138 * IN, tpi_pitch(40), S::UNF},
{"#8-36 UNF", 0.164 * IN, tpi_pitch(36), S::UNF},
{"#10-32 UNF", 0.190 * IN, tpi_pitch(32), S::UNF},
{"1/4-28 UNF", 0.250 * IN, tpi_pitch(28), S::UNF},
{"5/16-24 UNF", 0.3125 * IN, tpi_pitch(24), S::UNF},
{"3/8-24 UNF", 0.375 * IN, tpi_pitch(24), S::UNF},
{"7/16-20 UNF", 0.4375 * IN, tpi_pitch(20), S::UNF},
{"1/2-20 UNF", 0.500 * IN, tpi_pitch(20), S::UNF},
{"9/16-18 UNF", 0.5625 * IN, tpi_pitch(18), S::UNF},
{"5/8-18 UNF", 0.625 * IN, tpi_pitch(18), S::UNF},
{"3/4-16 UNF", 0.750 * IN, tpi_pitch(16), S::UNF},
{"1-12 UNF", 1.000 * IN, tpi_pitch(12), S::UNF},
};
return table;
}
const ThreadSpec* find_thread_standard(const std::string& name)
{
for (const ThreadSpec& s : thread_standards())
if (s.name == name)
return &s;
return nullptr;
}
} // namespace Slic3r
+39
View File
@@ -0,0 +1,39 @@
#ifndef slic3r_ThreadStandards_hpp_
#define slic3r_ThreadStandards_hpp_
#include <string>
#include <vector>
namespace Slic3r {
// Canonical mechanical thread specifications (ISO metric + Unified imperial).
// All dimensions are stored in millimetres so the CAD kernel can consume them
// directly. The profile is the common 60deg V shared by ISO 261/965 and ASME
// B1.1 (UTS), so the cut/ridge depth used by the Design-tab Thread tool is the
// basic external thread height h = 0.6134 * pitch, and the internal (tapped)
// minor diameter is D1 = D - 1.0825 * pitch (= D - 2*5H/8).
struct ThreadSpec {
enum class Series { MetricCoarse, MetricFine, UNC, UNF };
std::string name; // designation, e.g. "M6", "1/4-20 UNC"
double major_diameter_mm; // nominal (crest) diameter
double pitch_mm; // axial advance per turn
Series series;
// 60deg basic external thread height (radial crest-to-root engagement).
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; }
bool imperial() const { return series == Series::UNC || series == Series::UNF; }
};
// Full ordered table (metric coarse, metric fine, UNC, UNF) for GUI listing.
const std::vector<ThreadSpec>& thread_standards();
// Exact case-sensitive designation lookup; nullptr if not a known standard.
const ThreadSpec* find_thread_standard(const std::string& name);
} // namespace Slic3r
#endif
+58
View File
@@ -21,6 +21,11 @@ endif()
option(BUILD_SHARED_LIBS "Build shared libs" OFF)
option(USE_SLIC3R_CONSOLE_LOG "Enable console logging in RelWithDebInfo builds" OFF)
# SolveSpace constraint solver (2D sketch solver backbone), built in deps/SLVS.
if (SLIC3R_CAD)
find_package(SLVS REQUIRED)
endif ()
set(lisbslic3r_sources
AABBMesh.cpp
AABBMesh.hpp
@@ -304,6 +309,8 @@ set(lisbslic3r_sources
Layer.cpp
Layer.hpp
LayerRegion.cpp
LayOnFace.cpp
LayOnFace.hpp
libslic3r.cpp
libslic3r.h
Line.cpp
@@ -506,6 +513,29 @@ set(lisbslic3r_sources
FlushVolPredictor.cpp
)
# Parametric Design/CAD kernel. Needs OCCT's ModelingAlgorithms module and the
# vendored SolveSpace solver; both are pulled in only when SLIC3R_CAD is ON.
if (SLIC3R_CAD)
list(APPEND lisbslic3r_sources
CAD/GeometryEngine.cpp
CAD/GeometryEngine.hpp
CAD/SketchEngine.cpp
CAD/SketchEngine.hpp
CAD/SketchConstraints.cpp
CAD/SketchConstraints.hpp
CAD/SketchSolver.cpp
CAD/SketchSolver.hpp
CAD/SketchInference.cpp
CAD/SketchInference.hpp
CAD/SketchImport.cpp
CAD/SketchImport.hpp
CAD/CadDocument.cpp
CAD/CadDocument.hpp
CAD/ThreadStandards.cpp
CAD/ThreadStandards.hpp
)
endif ()
if (APPLE)
list(APPEND lisbslic3r_sources
MacUtils.mm
@@ -617,6 +647,30 @@ set(OCCT_LIBS
TKMath
TKernel
)
# The CAD kernel is the only consumer of OCCT's ModelingAlgorithms module: TKFillet
# (BRepFilletAPI), TKOffset (BRepOffsetAPI) and TKBool, which the other two need.
#
# PREPEND, never append: this list is single-pass static link order, dependents before
# dependencies — note TKernel, which everything needs, is deliberately last. TKOffset
# references BRepAlgo_Loop, which TKBool defines, so TKOffset must come BEFORE TKBool.
# Appending put it after, and a strictly single-pass linker (the Flatpak build) failed with
# libTKOffset.a(BRepOffset_MakeLoops.cxx.o): undefined reference to
# `BRepAlgo_Loop::BRepAlgo_Loop()'
# while the ordinary Linux, macOS and Windows links resolved it anyway. Use set() rather
# than list(PREPEND), which needs CMake 3.15 and this project supports 3.13.
if (SLIC3R_CAD)
set(OCCT_LIBS TKFillet TKOffset TKBool ${OCCT_LIBS})
# deps is configured separately, so its SLIC3R_CAD can differ from ours. The module is
# all-or-nothing, so one absent toolkit proves it; fail here rather than at link time.
if (NOT TARGET TKFillet)
message(FATAL_ERROR
"SLIC3R_CAD is ON, but the OpenCASCADE in ${CMAKE_PREFIX_PATH} was built without "
"BUILD_MODULE_ModelingAlgorithms. Rebuild the dependencies with -DSLIC3R_CAD=ON, "
"or configure this project with -DSLIC3R_CAD=OFF.")
endif ()
endif ()
# Published for the Windows packaging step in the top-level CMakeLists.txt.
set(OCCT_LIBS "${OCCT_LIBS}" CACHE INTERNAL "OCCT toolkits linked by libslic3r")
target_link_libraries(libslic3r
PUBLIC
@@ -669,6 +723,10 @@ if (TARGET OpenVDB::openvdb)
target_link_libraries(libslic3r PRIVATE OpenVDB::openvdb)
endif()
if (SLIC3R_CAD)
target_link_libraries(libslic3r PUBLIC SLVS::slvs)
endif ()
if(WIN32)
target_link_libraries(libslic3r PRIVATE Psapi.lib bcrypt.lib)
endif()
+4
View File
@@ -454,6 +454,10 @@ class ExtrusionLoop : public ExtrusionEntity
{
public:
ExtrusionPaths paths;
// ORCA: Set on a loop extruded entirely in mid air and out of reach of the layer below: it has
// nothing to lean on until this layer is bridged, so the G-code writer holds it back until the
// infill is down. See defer_unsupported_loops() in PerimeterGenerator.cpp.
bool print_after_infill = false;
ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
+32
View File
@@ -175,6 +175,10 @@ const std::string BBS_MODEL_CONFIG_RELS_FILE = "Metadata/_rels/model_settings.co
const std::string SLICE_INFO_CONFIG_FILE = "Metadata/slice_info.config";
const std::string FILAMENT_SEQUENCE_FILE = "Metadata/filament_sequence.json";
const std::string BBS_LAYER_HEIGHTS_PROFILE_FILE = "Metadata/layer_heights_profile.txt";
const std::string ORCA_CAD_RECIPE_FILE = "Metadata/orca_cad.bin";
// Read-only: the recipe entry's pre-rename name. A reader that knows only the new one drops the
// feature tree of every project written before the move, without a word. Never written.
const std::string LEGACY_CAD_RECIPE_FILE = "Metadata/SnapOrca_cad.bin";
const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/layer_config_ranges.xml";
const std::string BRIM_EAR_POINTS_FILE = "Metadata/brim_ear_points.txt";
/*const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt";
@@ -1950,6 +1954,15 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// extract slic3r print config file
_extract_project_config_from_archive(archive, stat, config, config_substitutions, model);
}
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) {
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);
}
}
else if (boost::algorithm::iequals(name, CUT_INFORMATION_FILE)) {
// extract object cut info
_extract_cut_information_from_archive(archive, stat, config_substitutions);
@@ -6008,6 +6021,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _add_mesh_to_object_stream(std::function<bool(std::string &, bool)> const &flush, ObjectData const &object_data) const;
bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) const;
bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_brim_ear_points_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model);
@@ -6404,6 +6418,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
if (!_add_cad_recipe_file_to_archive(archive, model)) {
close_zip_writer(&archive);
return false;
}
// BBS progress point
/*BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format("export 3mf EXPORT_STAGE_ADD_LAYER_RANGE\n");
if (proFn) {
@@ -7658,6 +7677,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Exporter::_add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model)
{
if (model.cad_recipe.empty())
return true;
if (!mz_zip_writer_add_mem(&archive, ORCA_CAD_RECIPE_FILE.c_str(),
(const void*)model.cad_recipe.data(), model.cad_recipe.length(),
MZ_DEFAULT_COMPRESSION)) {
add_error("Unable to add CAD recipe file to archive");
return false;
}
return true;
}
bool _BBS_3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model)
{
std::string out = "";
+62 -19
View File
@@ -1028,11 +1028,21 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
double current_z = gcodegen.writer().get_position().z();
if (z == -1.) // in case no specific z was provided, print at current_z pos
z = current_z;
if (!is_approx(z, current_z)) {
// Orca: wipe_tower_no_sparse_layers crash guard. With sparse layers skipped the tower is
// compacted far below the object, so descending to it is only safe once the nozzle is parked
// over the tower - which is what the is_finish_first travel above does. Otherwise the nozzle
// is still over the model and this descent would drive it into the print, so defer it to the
// re-descents below, which run after the travel to the tower.
const bool defer_compacted_descend = m_sparse_layers_skipped
&& !tcr.priming && !tcr.is_finish_first && (current_z - z) > EPSILON;
if (!is_approx(z, current_z) && !defer_compacted_descend) {
gcode += gcodegen.writer().retract();
gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer.");
gcode += gcodegen.writer().unretract();
}
// Tower compacted below the object, so any extrusion emitted without an explicit z has to be
// pulled back down to it first.
const bool compacted_below_object = m_sparse_layers_skipped && z >= 0. && (tcr.print_z - z) > EPSILON;
// Process the end filament gcode.
bool add_change_filament_624 = false;
@@ -1085,11 +1095,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string nozzle_change_gcode_trans;
if (is_nozzle_change) {
// move to start_pos before nozzle change
// Orca: travel_to() lifts to the object layer height to clear the print. That lift is
// needed when arriving from the model, but is a wasted full-height Z bounce when the
// nozzle already sits on the compacted tower, so travel at the compacted z instead.
const bool compact_intower_nc_travel = compacted_below_object
&& (tcr.print_z - gcodegen.writer().get_position().z()) > EPSILON;
std::string start_pos_str;
start_pos_str = gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.start_pos) + plate_origin_2d), erMixed,
"Move to nozzle change start pos");
"Move to nozzle change start pos", compact_intower_nc_travel ? z : DBL_MAX);
check_add_eol(start_pos_str);
nozzle_change_gcode_trans += start_pos_str;
// The nozzle-change wipe below carries no explicit z, so it would extrude at the object
// layer height and float above the compacted tower. Descend unless the travel stayed down.
if (!compact_intower_nc_travel && compacted_below_object) {
std::string nc_z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
check_add_eol(nc_z_descend);
nozzle_change_gcode_trans += nc_z_descend;
}
nozzle_change_gcode_trans += gcodegen.unretract();
nozzle_change_gcode_trans += transform_gcode(tcr.nozzle_change_result.gcode, tcr.nozzle_change_result.start_pos, wipe_tower_offset, wipe_tower_rotation);
gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.end_pos) + plate_origin_2d));
@@ -1428,6 +1450,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
start_filament_gcode_str = start_filament_gcode_str + wipe_next_start_point_str + toolchange_unretract_str;
// Orca: the custom change_filament_gcode lifts to the object layer height and the unretract
// de-hops back to it, so every tower extrusion emitted after it (purge moves, and the wall
// when it prints after the toolchange) would float above the compacted tower. Descend first.
if (compacted_below_object) {
std::string z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
check_add_eol(z_descend);
start_filament_gcode_str += z_descend;
}
// Insert the end filament, toolchange, and start filament gcode into the generated gcode.
DynamicConfig config;
config.set_key_value("filament_end_gcode", new ConfigOptionString(end_filament_gcode_str));
@@ -1915,11 +1946,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1;
bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
if (m_sparse_layers_skipped) {
wipe_tower_z = m_last_wipe_tower_print_z;
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool &&
m_layer_idx != 0);
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0;
if (m_tool_change_idx == 0 && !ignore_sparse)
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
}
@@ -1935,12 +1964,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1;
bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
wipe_tower_z = m_last_wipe_tower_print_z;
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
if (m_tool_change_idx == 0 && !ignore_sparse)
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
if (m_sparse_layers_skipped) {
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
wipe_tower_z = m_compacted_tower_z[m_layer_idx];
}
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
@@ -1953,10 +1979,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
if (!ignore_sparse) {
if (!ignore_sparse)
gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z);
m_last_wipe_tower_print_z = wipe_tower_z;
}
}
}
@@ -1970,9 +1994,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return true;
bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
}
if (m_sparse_layers_skipped)
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
return false;
@@ -6580,6 +6603,8 @@ LayerResult GCode::process_layer(
}
// Then print infill
gcode += this->extrude_infill(print, by_region_specific, false);
// Then the walls left hanging in mid air, now that the infill can anchor them
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
// Then print perimeters of regions that has is_infill_first == true
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
}
@@ -6875,6 +6900,7 @@ LayerResult GCode::process_layer(
has_insert_timelapse_gcode = true;
}
gcode += this->extrude_infill(print, by_region_specific, false);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
// ironing
gcode += this->extrude_infill(print, by_region_specific, true);
@@ -7615,7 +7641,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
}
// Extrude perimeters: Decide where to put seams (hide or align seams).
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first)
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only)
{
std::string gcode;
for (const ObjectByExtruder::Island::Region &region : by_region)
@@ -7634,7 +7660,24 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
m_config.wipe_inward_distance.value > 0. &&
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
wipe_support.emplace();
// ORCA: loops flagged as extruded in mid air, out of reach of the layer below, are held back
// for a second pass after the infill that anchors them. Infill already precedes infill first walls.
const bool defer_unsupported = !is_infill_first;
auto waits_for_infill = [](const ExtrusionEntity *ee) {
return ee->is_loop() && static_cast<const ExtrusionLoop *>(ee)->print_after_infill;
};
// The deferred pass runs after the infill, so the loops the first pass emitted are
// already down and belong in the prefix an inward wipe may land on.
if (wipe_support && defer_unsupported && unsupported_loops_only)
for (const ExtrusionEntity* ee : region.perimeters)
if (!waits_for_infill(ee))
wipe_support->append(*ee);
for (const ExtrusionEntity* ee : region.perimeters) {
if (defer_unsupported && waits_for_infill(ee) != unsupported_loops_only)
continue;
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
wipe_support ? &*wipe_support : nullptr);
if (wipe_support)
+12 -2
View File
@@ -106,8 +106,13 @@ public:
m_enable_wrapping_detection(print_config.enable_wrapping_detection && (print_config.wrapping_exclude_area.values.size() > 2) && (slice_used_filaments.size() <= 1)),
m_is_first_print(true),
m_print_config(&print_config),
m_last_wipe_tower_print_z(print_config.z_offset.value)
m_last_wipe_tower_print_z(print_config.z_offset.value),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config))
{
// Precomputed rather than accumulated while emitting, so that the clearance validator and
// the emitter cannot disagree about where the compacted tower sits on any given layer.
if (m_sparse_layers_skipped)
m_compacted_tower_z = compute_compacted_wipe_tower_z(tool_changes, float(print_config.z_offset.value));
// initialize with the extruder offset of master extruder id
m_extruder_offsets.resize(print_config.filament_map.size(), print_config.extruder_offset.get_at(print_config.master_extruder_id.value - 1));
const auto& filament_map = print_config.filament_map.values; // 1 based idx
@@ -167,6 +172,11 @@ private:
float m_wipe_tower_depth;
BoundingBoxf m_wipe_tower_bbx;
Vec2f m_rib_offset{Vec2f(0, 0)};
// wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw
// option: smooth timelapse and wrapping detection keep a tower on every layer regardless.
const bool m_sparse_layers_skipped;
// Print z of the compacted tower per planned layer. Empty when the tower is not compacted.
std::vector<float> m_compacted_tower_z;
};
class ColorPrintColors
@@ -524,7 +534,7 @@ private:
// For sequential print, the instance of the object to be printing has to be defined.
const size_t single_object_instance_idx);
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first);
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only = false);
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
+64 -7
View File
@@ -10,6 +10,7 @@
#include "FilamentMixer.hpp"
#include "LocalesUtils.hpp"
#include "Utils.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include <boost/log/trivial.hpp>
@@ -82,8 +83,9 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
int printable_status = print_config->filament_printable.get_at(filament_id);
int extruder_idx = filament_maps[filament_id];
if (!(printable_status >> extruder_idx & 1)) {
std::string extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
std::string error_msg = _L("Grouping error: ") + filament_type + _L(" can not be placed in the ") + extruder_name + _L(" nozzle");
std::string error_msg = extruder_idx == 0 ?
Slic3r::format(_L("Grouping error: %1% cannot be placed in the left nozzle"), filament_type) :
Slic3r::format(_L("Grouping error: %1% cannot be placed in the right nozzle"), filament_type);
throw Slic3r::RuntimeError(error_msg);
}
}
@@ -2735,6 +2737,28 @@ void ToolOrdering::enforce_mixed_component_order()
}
}
// Declared in ToolOrdering.hpp (exposed for unit testing).
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
{
std::vector<unsigned int> order;
for (const std::string& token : split_string(str, ',')) {
try {
size_t pos = 0;
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
// consumed (bar trailing whitespace) to drop it like any other garbage.
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
continue;
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
order.emplace_back((unsigned int)(filament - 1));
} catch (const std::exception&) {
// Not a number, ignore it.
}
}
return order;
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{
const PrintConfig* print_config = m_print_config_ptr;
@@ -2832,11 +2856,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
const bool use_cyclic_ordering =
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
// sequence); the cyclic sequence is only forced onto it when the user opts in.
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
// still yields the default cyclic order.
const std::vector<unsigned int> cyclic_order =
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
: std::vector<unsigned int>();
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
// after the listed ones.
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
std::sort(filaments.begin(), filaments.end());
if (!cyclic_order.empty())
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
auto rank = [&cyclic_order](unsigned int filament) {
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
};
return rank(lhs) < rank(rhs);
});
};
// other_layers_seq: the layer_idx and extruder_idx are base on 1
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool {
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
if (!reorder_first_layer && layer_idx == 0) {
out_seq.resize(first_layer_filaments.size());
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; });
// The first layer tool order is already decided (adhesion-optimized, plus any custom first
// layer sequence). Only override it with the cyclic sequence when the user opted in.
std::vector<unsigned int> ordered = first_layer_filaments;
if (cyclic_first_layer)
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
return true;
}
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
@@ -2847,9 +2901,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
}
}
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) {
// Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
&& size_t(layer_idx) < layer_filaments.size()) {
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
std::sort(ordered.begin(), ordered.end());
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
return true;
+5
View File
@@ -417,6 +417,11 @@ private:
int most_used_extruder;
};
// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices.
// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string
// still orders the filaments it does name. Exposed for unit testing.
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders);
} // namespace SLic3r
#endif /* slic3r_ToolOrdering_hpp_ */
+31 -7
View File
@@ -25,6 +25,30 @@ static constexpr int arc_fit_size = 20;
enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow, LimitRammingFlowNC};//nc:nozzle change
static const std::map<float, float> nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}};
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config)
{
return config.wipe_tower_no_sparse_layers.value && config.timelapse_type.value != TimelapseType::tlSmooth &&
! config.enable_wrapping_detection.value;
}
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
{
return layer_tool_changes.size() == 1 && layer_tool_changes.front().initial_tool == layer_tool_changes.front().new_tool;
}
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
float base_z)
{
std::vector<float> tower_z(tool_changes.size(), base_z);
float last = base_z;
for (size_t i = 0; i < tool_changes.size(); ++i) {
if (! tool_changes[i].empty() && ! wipe_tower_layer_is_sparse(tool_changes[i]))
last += tool_changes[i].front().layer_height;
tower_z[i] = last;
}
return tower_z;
}
inline float align_round(float value, float base)
{
return std::round(value / base) * base;
@@ -1879,7 +1903,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
m_z_pos(0.f),
//m_bridging(float(config.wipe_tower_bridging)),
m_bridging(10.f),
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_current_tool(initial_tool),
@@ -2977,7 +3001,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_no_sparse_layers || toolchanges_on_layer)
if (! m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -3021,7 +3045,7 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool))
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool))
m_first_layer_idx = m_plan.size() - 1;
if (old_tool == new_tool) // new layer without toolchanges - we are done
@@ -3874,7 +3898,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -3984,7 +4008,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4101,7 +4125,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -5155,7 +5179,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
+19 -1
View File
@@ -521,7 +521,7 @@ private:
//float m_parking_pos_retraction = 0.f;
//float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_no_sparse_layers = false;
bool m_sparse_layers_skipped = false;
// BBS: remove useless config
//bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
@@ -680,6 +680,24 @@ private:
};
// Compaction rule for wipe_tower_no_sparse_layers. Shared by the G-code emitter and by the
// clearance validator so that both agree on where the compacted tower actually sits; a drift
// between the two would either let a real nozzle collision through or reject a safe plate.
// Whether sparse layers are really skipped, i.e. whether the tower is compacted at all. Smooth
// timelapse and wrapping detection put a tower on every layer, so no layer is ever dropped and the
// tower keeps following the object even though the option is on. Tower planning, G-code emission and
// the clearance validator all ask this single question, so none of them can compact on its own.
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config);
// A planned layer prints no tower at all when its only toolchange keeps the same filament.
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes);
// Print z the compacted tower reaches on every planned layer. Sparse layers carry over the
// previous value, so the tower falls one layer height behind the object for each of them. base_z is
// the z the tower starts from, which Orca offsets by z_offset.
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
float base_z = 0.f);
} // namespace Slic3r
+7 -7
View File
@@ -1032,7 +1032,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_y_shift(0.f),
m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)),
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
@@ -1730,7 +1730,7 @@ void WipeTower2::toolchange_Change(
} else if (m_wall_type == (int)wtwCone) {
const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
m_wipe_tower_cone_angle).second;
const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z;
const double z = m_sparse_layers_skipped ? (m_current_height + m_layer_info->height) : m_layer_info->z;
const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z);
const double w = m_layer_info->depth + m_perimeter_width;
if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall
@@ -1872,7 +1872,7 @@ void WipeTower2::toolchange_Wipe(
// All the calculations in all other places take the spacing into account for all the layers.
// If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down.
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
float wipe_speed = 0.33f * target_speed;
// if there is less than 2.5*line_width to the edge, advance straightaway (there is likely a blob anyway)
@@ -1970,7 +1970,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Slow down on the 1st layer.
// If spare layers are excluded -> if 1 or less toolchange has been done, it must be still the first layer, too. So slow down.
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers);
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped);
float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
if (m_enable_tower_interface_features && m_prev_layer_had_interface)
feedrate = std::min(feedrate, 20.f * 60.f);
@@ -2103,7 +2103,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_no_sparse_layers || toolchanges_on_layer || first_layer) {
if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) {
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
m_current_height += m_layer_info->height;
@@ -2226,7 +2226,7 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool || m_plan.size() == 1))
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool || m_plan.size() == 1))
m_first_layer_idx = m_plan.size() - 1;
if (old_tool == new_tool) // new layer without toolchanges - we are done
@@ -2652,7 +2652,7 @@ Polygon WipeTower2::generate_support_cone_wall(
const auto [R, support_scale] = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
m_wipe_tower_cone_angle);
double z = m_no_sparse_layers ?
double z = m_sparse_layers_skipped ?
(m_current_height + m_layer_info->height) :
m_layer_info->z; // the former should actually work in both cases, but let's stay on the safe side (the 2.6.0 is close)
+1 -1
View File
@@ -267,7 +267,7 @@ private:
float m_parking_pos_retraction = 0.f;
float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_no_sparse_layers = false;
bool m_sparse_layers_skipped = false;
bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
GCodeFlavor m_gcode_flavor;
+221
View File
@@ -0,0 +1,221 @@
#include "LayOnFace.hpp"
#include "Geometry.hpp"
#include "Geometry/ConvexHull.hpp"
#include "Model.hpp"
#include "TriangleMesh.hpp"
#include <algorithm>
#include <cmath>
#include <numeric>
namespace Slic3r {
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &inst_matrix)
{
// An object can only rest on its convex hull, so candidate faces are taken from the hull of all model parts.
TriangleMesh ch;
for (const ModelVolume* vol : object.volumes) {
if (vol->type() != ModelVolumeType::MODEL_PART)
continue;
TriangleMesh vol_ch = vol->get_convex_hull();
vol_ch.transform(vol->get_matrix());
ch.merge(vol_ch);
}
ch = ch.convex_hull_3d();
std::vector<LayOnFacePlane> planes;
// Following constants are used for discarding too small polygons.
const float minimal_area = 5.f; // in square mm (world coordinates)
const float minimal_side = 1.f; // mm
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
// Now we'll go through all the facets and append Points of facets sharing the same normal.
// This part is still performed in mesh coordinate system.
const int num_of_facets = ch.facets_count();
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
std::vector<int> facet_queue(num_of_facets, 0);
std::vector<bool> facet_visited(num_of_facets, false);
int facet_queue_cnt = 0;
const stl_normal* normal_ptr = nullptr;
int facet_idx = 0;
while (1) {
// Find next unvisited triangle:
for (; facet_idx < num_of_facets; ++ facet_idx)
if (!facet_visited[facet_idx]) {
facet_queue[facet_queue_cnt ++] = facet_idx;
facet_visited[facet_idx] = true;
normal_ptr = &face_normals[facet_idx];
planes.emplace_back();
break;
}
if (facet_idx == num_of_facets)
break; // Everything was visited already
while (facet_queue_cnt > 0) {
int facet_idx = facet_queue[-- facet_queue_cnt];
const stl_normal& this_normal = face_normals[facet_idx];
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
const Vec3i32 face = ch.its.indices[facet_idx];
for (int j=0; j<3; ++j)
planes.back().outline.emplace_back(ch.its.vertices[face[j]].cast<double>());
facet_visited[facet_idx] = true;
for (int j = 0; j < 3; ++ j)
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
facet_queue[facet_queue_cnt ++] = neighbor_idx;
}
}
planes.back().normal = normal_ptr->cast<double>();
Pointf3s& verts = planes.back().outline;
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
// make real sense.
verts = transform(verts, inst_matrix);
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
if (verts.size() == 3 &&
((verts[0] - verts[1]).norm() < minimal_side
|| (verts[0] - verts[2]).norm() < minimal_side
|| (verts[1] - verts[2]).norm() < minimal_side))
planes.pop_back();
}
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
// Now we'll go through all the polygons, transform the points into xy plane to process them:
for (unsigned int polygon_id=0; polygon_id < planes.size(); ++polygon_id) {
Pointf3s& polygon = planes[polygon_id].outline;
const Vec3d& normal = planes[polygon_id].normal;
// transform the normal according to the instance matrix:
const Vec3d normal_transformed = normal_matrix * normal;
// We are going to rotate about z and y to flatten the plane
Eigen::Quaterniond q;
Transform3d& m = planes[polygon_id].to_plane_frame;
m = Transform3d::Identity();
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
polygon = transform(polygon, m);
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
Vec3d bb_size = BoundingBoxf3(polygon).size();
float sf = std::min(1./bb_size(0), 1./bb_size(1));
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
polygon = transform(polygon, tr);
polygon = Slic3r::Geometry::convex_hull(polygon);
polygon = transform(polygon, tr.inverse());
// Calculate area of the polygons and discard ones that are too small
float& area = planes[polygon_id].area;
area = 0.f;
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
area = 0.5f * std::abs(area);
bool discard = false;
if (area < minimal_area)
discard = true;
else {
// We also check the inner angles and discard polygons with angles smaller than the following threshold
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
for (unsigned int i = 0; i < polygon.size(); ++i) {
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
const Vec3d& curr = polygon[i];
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
discard = true;
break;
}
}
}
if (discard) {
planes[polygon_id--] = std::move(planes.back());
planes.pop_back();
continue;
}
const Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0)) / double(polygon.size());
planes[polygon_id].center = inst_matrix.inverse() * (m.inverse() * centroid);
}
std::sort(planes.rbegin(), planes.rend(), [](const LayOnFacePlane& a, const LayOnFacePlane& b) { return a.area < b.area; });
return planes;
}
int find_largest_plane(const std::vector<LayOnFacePlane> &planes)
{
// The plane frame maps the instance normal to +Z, so the normal's z in instance coordinates is element (2, 2).
auto downward = [](const LayOnFacePlane &plane) { return -plane.to_plane_frame.linear()(2, 2); };
// Areas are floats from rounded geometry, so faces within 0.1% count as equal.
int best = -1;
for (size_t i = 0; i < planes.size() && planes[i].area >= planes.front().area * (1. - 1e-3); ++i)
if (best < 0 || downward(planes[i]) > downward(planes[best]))
best = int(i);
return best;
}
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction)
{
const Vec3d dir = direction.normalized();
int best = -1;
double best_dot = -2.;
for (size_t i = 0; i < planes.size(); ++i)
if (const double dot = planes[i].normal.dot(dir); dot > best_dot) {
best_dot = dot;
best = int(i);
}
return best;
}
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
const Vec3d &point, double tolerance)
{
const Vec3d instance_point = instance_matrix_no_offset * point;
for (size_t i = 0; i < planes.size(); ++i) {
const Pointf3s &outline = planes[i].outline;
if (outline.empty())
continue;
const Vec3d p = planes[i].to_plane_frame * instance_point;
// Facets with slightly different normals are merged into one face, so the outline is not exactly flat.
const double z = std::accumulate(outline.begin(), outline.end(), 0., [](double sum, const Vec3d &v) { return sum + v.z(); }) / double(outline.size());
if (std::abs(p.z() - z) > tolerance)
continue;
// The outline is convex: the point is inside when it is not on both sides of its edges.
bool left = false, right = false;
for (size_t j = 0; j < outline.size(); ++j) {
const Vec2d a = outline[j].head<2>();
const Vec2d edge = outline[(j + 1) % outline.size()].head<2>() - a;
const double len = edge.norm();
if (len < EPSILON)
continue;
const double side = cross2(edge, Vec2d(p.head<2>() - a)) / len;
left |= side > tolerance;
right |= side < -tolerance;
}
if (!(left && right))
return int(i);
}
return -1;
}
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal)
{
ModelInstance &instance = *object.instances[instance_idx];
const Geometry::Transformation &trafo = instance.get_transformation();
// Same rotation as Selection::flattening_rotate(): turn the transformed normal to point down.
const Vec3d tnormal = trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
const Transform3d rotation = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
instance.set_transformation(Geometry::Transformation(trafo.get_offset_matrix() * rotation * trafo.get_matrix_no_offset()));
// Drop this instance only: ensure_on_bed() skips instances without auto_drop and measures the first instance.
object.translate_instance(instance_idx, -object.instance_bounding_box(instance_idx).min.z() * Vec3d::UnitZ());
}
} // namespace Slic3r
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "Point.hpp"
#include <vector>
namespace Slic3r {
class ModelObject;
// A face of an object's convex hull that the object can rest on. These are the faces the
// "Lay on Face" gizmo offers and the ones the CLI --ground-* options choose from.
//
// Frames: "object" coordinates have the volume transformations applied but not the instance
// transformation. "Instance" coordinates additionally have the instance rotation, scale and
// mirror applied, but not its offset.
struct LayOnFacePlane
{
Vec3d normal; // outward unit normal, object coordinates
Vec3d center; // centroid of the outline, object coordinates; on the face's mean plane
float area; // mm², instance coordinates
Pointf3s outline; // convex outline in the plane frame, where the face is horizontal
Transform3d to_plane_frame; // rotation from instance coordinates to the plane frame
};
// Candidate faces of the object's model parts, largest first. The instance transformation
// (without offset) is applied before measuring, so faces too small to rest on are dropped
// by their printed size: under 5 mm², a side under 1 mm, or an inner angle under 1°.
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &instance_matrix_no_offset);
// Index of the largest plane, or -1 if `planes` is empty. Of planes with the same area, such as
// the top and bottom of a box, the one already facing down the most wins, so flat parts stay put.
int find_largest_plane(const std::vector<LayOnFacePlane> &planes);
// Index of the plane whose normal is closest to `direction` (object coordinates),
// or -1 if `planes` is empty.
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction);
// Index of the plane whose face contains `point` (object coordinates) within `tolerance` mm, or -1
// if there is none. `instance_matrix_no_offset` is the one the planes were computed with.
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
const Vec3d &point, double tolerance);
// Rotates the instance so that `normal` (object coordinates) points down, the same rotation as
// the gizmo applies, then drops the instance so its lowest point is at z = 0.
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal);
} // namespace Slic3r
+1
View File
@@ -153,6 +153,7 @@ bool Layer::is_perimeter_compatible(const Print& print, const PrintRegion& a, co
&& config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value
&& config.detect_overhang_wall == other_config.detect_overhang_wall
&& config.unsupported_wall_last == other_config.unsupported_wall_last
&& config.overhang_reverse == other_config.overhang_reverse
&& config.overhang_reverse_threshold == other_config.overhang_reverse_threshold
&& config.wall_direction == other_config.wall_direction
+3
View File
@@ -108,6 +108,8 @@ Model& Model::assign_copy(const Model &rhs)
this->md_value = rhs.md_value;
this->texture_mesh = rhs.texture_mesh;
this->cad_recipe = rhs.cad_recipe;
return *this;
}
@@ -152,6 +154,7 @@ Model& Model::assign_copy(Model &&rhs)
rhs.model_info.reset();
this->profile_info = rhs.profile_info;
rhs.profile_info.reset();
this->cad_recipe = std::move(rhs.cad_recipe);
return *this;
}
+4
View File
@@ -1569,6 +1569,10 @@ public:
std::vector<std::string> md_name;
std::vector<std::string> md_value;
// Opaque parametric CAD recipe (CadDocument::serialize_recipe()), round-tripped through
// the 3MF as Metadata/orca_cad.bin. Empty for non-CAD projects.
std::string cad_recipe;
void SetDesigner(std::string designer, std::string designer_user_id) {
if (design_info == nullptr) {
design_info = std::make_shared<ModelDesignInfo>();
+71
View File
@@ -550,6 +550,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
if (!paths.empty()) {
if (extrusion->is_closed) {
ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole);
extrusion_loop.inset_idx = extrusion->inset_idx;
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) ==
(pg_extrusion.is_contour || pg_extrusions.size() == 2))
extrusion_loop.make_counter_clockwise();
@@ -1318,6 +1319,73 @@ static void reorient_perimeters(ExtrusionEntityCollection &entities, bool steep_
}
}
// A loop made of nothing but overhang paths lies entirely off the lower layer.
static bool is_unsupported_loop(const ExtrusionEntity *entity)
{
if (!entity->is_loop())
return false;
const ExtrusionPaths &paths = static_cast<const ExtrusionLoop *>(entity)->paths;
return !paths.empty() && std::all_of(paths.begin(), paths.end(),
[](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; });
}
// ORCA: A wall loop with nothing under it has nothing to lean on, so whatever the configured wall
// sequence it is extruded after the loops that anchor it, innermost first. A loop that runs alongside
// an anchored one belongs to the same wall stack and keeps its place ahead of the infill, which needs
// it as an anchor; one that touches nothing has only that infill to rest on, so it is flagged for the
// G-code writer to hold it back until the infill is down.
static void defer_unsupported_loops(const PerimeterGenerator &perimeter_generator, ExtrusionEntityCollection &entities)
{
if (!perimeter_generator.config->unsupported_wall_last)
return;
ExtrusionEntitiesPtr &src = entities.entities;
auto first_deferred = std::stable_partition(src.begin(), src.end(),
[](const ExtrusionEntity *entity) { return !is_unsupported_loop(entity); });
if (first_deferred == src.end())
return;
std::stable_sort(first_deferred, src.end(),
[](const ExtrusionEntity *lhs, const ExtrusionEntity *rhs) { return lhs->inset_idx > rhs->inset_idx; });
auto collect_lines = [](const ExtrusionEntity *entity, Lines &out) {
Polylines polylines;
entity->collect_polylines(polylines);
append(out, to_lines(polylines));
};
Lines anchored;
for (auto it = src.begin(); it != first_deferred; ++it)
collect_lines(*it, anchored);
std::vector<ExtrusionLoop *> unattached;
for (auto it = first_deferred; it != src.end(); ++it)
unattached.emplace_back(static_cast<ExtrusionLoop *>(*it));
// A loop leaning on a loop that is itself anchored is anchored as well, so spread outwards from
// the anchored loops until no unsupported loop is left touching what was reached.
const double touch_distance = 1.5 * std::max(perimeter_generator.ext_perimeter_flow.scaled_spacing(),
perimeter_generator.perimeter_flow.scaled_spacing());
while (!anchored.empty()) {
AABBTreeLines::LinesDistancer<Line> distancer{std::move(anchored)};
anchored.clear();
for (ExtrusionLoop *&loop : unattached) {
if (loop == nullptr)
continue;
const Points points = loop->as_polyline().points;
if (std::any_of(points.begin(), points.end(),
[&distancer, touch_distance](const Point &point) { return distancer.distance_from_lines<false>(point) < touch_distance; })) {
collect_lines(loop, anchored);
loop = nullptr;
}
}
}
for (ExtrusionLoop *loop : unattached)
if (loop != nullptr)
loop->print_after_infill = true;
}
void PerimeterGenerator::process_classic()
{
group_region_by_fuzzify(*this);
@@ -1804,6 +1872,8 @@ void PerimeterGenerator::process_classic()
}
}
defer_unsupported_loops(*this, entities);
// append perimeters for this slice as a collection
if (! entities.empty())
this->loops->append(entities);
@@ -2742,6 +2812,7 @@ void PerimeterGenerator::process_arachne()
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
}
defer_unsupported_loops(*this, extrusion_coll);
this->loops->append(extrusion_coll);
}
+5 -2
View File
@@ -545,7 +545,7 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str
return "";
// Dedicated namespace for preset setting_ids, distinct from the cloud per-user
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_id_tool.py;
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_profile_tool.py;
// never change this constant.
static const boost::uuids::uuid vendor_namespace =
boost::uuids::string_generator()("c1f4d9e2-7a3b-5c8d-9e0f-1a2b3c4d5e6f");
@@ -1058,6 +1058,7 @@ static std::vector<std::string> s_Preset_print_options{
"reduce_crossing_wall",
"detect_thin_wall",
"detect_overhang_wall",
"unsupported_wall_last",
"overhang_reverse",
"overhang_reverse_threshold",
"overhang_reverse_internal_only",
@@ -1320,6 +1321,8 @@ static std::vector<std::string> s_Preset_print_options{
"wipe_tower_extra_flow",
"single_extruder_multi_material_priming",
"toolchange_ordering",
"toolchange_cyclic_order",
"toolchange_cyclic_first_layer",
"wipe_tower_rotation_angle",
"tree_support_branch_distance_organic",
"tree_support_branch_diameter_organic",
@@ -1445,7 +1448,7 @@ static std::vector<std::string> s_Preset_printer_options {
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod",
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod",
"nozzle_height", "master_extruder_id",
"default_print_profile", "inherits",
"silent_mode",
+2 -2
View File
@@ -93,8 +93,8 @@ class PresetBundle;
// Deterministic preset setting_id: uuid5(vendor/type/name) -> 16 base62 chars.
// Pure function of a system preset's identity, so the value can be assigned by
// scripts/orca_id_tool.py and recomputed here when a profile ships without it.
// MUST stay byte-identical to scripts/orca_id_tool.py.
// scripts/orca_profile_tool.py and recomputed here when a profile ships without it.
// MUST stay byte-identical to scripts/orca_profile_tool.py.
// This is NOT the per-user cloud-sync setting_id
// (OrcaCloudServiceAgent::generate_uuid_for_setting_id) - do not conflate them.
std::string generate_preset_setting_id(const std::string& vendor,
+68 -1
View File
@@ -6783,7 +6783,7 @@ std::string PresetBundle::load_vendor_preset(
loaded.description = entry.description;
loaded.setting_id = entry.setting_id;
// Derive the preset setting_id on the fly when a profile ships without one,
// matching scripts/orca_id_tool.py. Only instantiated presets carry an id;
// matching scripts/orca_profile_tool.py. Only instantiated presets carry an id;
// non-instantiated base profiles return earlier above. This never
// touches the per-user cloud-sync setting_id written into user .info files.
if (loaded.setting_id.empty() && entry.instantiation == "true")
@@ -7619,6 +7619,9 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const
if (this->check_preset_references())
has_errors = true;
if (this->check_printer_default_materials())
has_errors = true;
return has_errors;
}
@@ -7711,6 +7714,70 @@ bool PresetBundle::check_preset_references() const
return found;
}
bool PresetBundle::check_printer_default_materials() const
{
bool found = false;
// A model's default_materials list is shared by its variants, so report each unknown name once.
std::set<const VendorProfile::PrinterModel *> checked_models;
// default_filament_profile is inherited from shared base machine presets, so one bad name can
// surface on many variants; report it once, at the first printer that names it.
std::set<std::string> reported_unknown_profiles;
for (const Preset &printer : printers) {
if (!printer.is_system || printer.vendor == nullptr || printer.printer_technology() != ptFFF)
continue;
const VendorProfile::PrinterModel *model = PresetUtils::system_printer_model(printer);
const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer);
// Use the same name lookup as load_installed_filaments, not UI aliases or fuzzy matching.
// A model's defaults can cover different nozzles, but at least one must cover this variant.
const bool has_default = model != nullptr && std::any_of(model->default_materials.begin(), model->default_materials.end(),
[&](const std::string &name) {
const Preset *filament = filaments.find_preset(name, false);
return filament != nullptr && filament->is_system &&
is_compatible_with_printer(filaments.get_preset_with_vendor_profile(*filament), active_printer);
});
if (!has_default) {
found = true;
BOOST_LOG_TRIVIAL(error) << "Printer preset \"" << printer.name << "\" (vendor \"" << printer.vendor->name
<< "\", model \"" << printer.config.opt_string("printer_model") << "\", variant \""
<< printer.config.opt_string("printer_variant")
<< "\") has no compatible system filament in its model's \"default_materials\". "
"Add at least one full filament preset name compatible with this printer variant:\n"
<< preset_file_uri(printer.file);
}
if (model != nullptr && checked_models.insert(model).second) {
for (const std::string &name : model->default_materials) {
const Preset *filament = filaments.find_preset(name, false);
if (filament == nullptr || !filament->is_system) {
found = true;
BOOST_LOG_TRIVIAL(error) << "Printer model \"" << model->name << "\" (vendor \"" << printer.vendor->name
<< "\") names the unknown system filament \"" << name
<< "\" in its \"default_materials\":\n" << preset_file_uri(printer.file);
}
}
}
if (printer.config.has("default_filament_profile")) {
for (const std::string &name : printer.config.opt<ConfigOptionStrings>("default_filament_profile")->values) {
// A ";"-separated list can leave an empty trailing segment; formatting noise, not a name.
if (name.empty())
continue;
const Preset *filament = filaments.find_preset(name, false);
if ((filament == nullptr || !filament->is_system) && reported_unknown_profiles.insert(name).second) {
found = true;
BOOST_LOG_TRIVIAL(error) << "Printer preset \"" << printer.name << "\" (vendor \"" << printer.vendor->name
<< "\", model \"" << printer.config.opt_string("printer_model") << "\", variant \""
<< printer.config.opt_string("printer_variant")
<< "\") names the unknown system filament \"" << name
<< "\" in its \"default_filament_profile\":\n" << preset_file_uri(printer.file);
}
}
}
}
return found;
}
// Orca: a filament is matched from the AMS by (filament_id + printer compatibility).
// For any one printer, at most one instantiated filament preset with a given
// filament_id may be compatible - otherwise the AMS match is ambiguous and the
+5
View File
@@ -617,6 +617,11 @@ public:
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
bool check_preset_references() const;
// Validator-only: every system FFF printer variant needs a compatible system filament
// named in its model's default_materials, every name there and in the printer's
// default_filament_profile must resolve to a system filament.
bool check_printer_default_materials() const;
// Merge one vendor's presets with the other vendor's presets, report duplicates.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
+389
View File
@@ -360,6 +360,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "other_layers_print_sequence"
|| opt_key == "other_layers_print_sequence_nums"
|| opt_key == "toolchange_ordering"
|| opt_key == "toolchange_cyclic_order"
|| opt_key == "toolchange_cyclic_first_layer"
|| opt_key == "extruder_ams_count"
|| opt_key == "extruder_nozzle_stats"
|| opt_key == "filament_map_mode"
@@ -964,6 +966,377 @@ StringObjectException Print::sequential_print_clearance_valid(const Print &print
return single_object_exception;
}
// ---------------------------------------------------------------------------------------------
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers.
// Ported from BambuStudio and adapted to Orca's printer config: Orca has no
// prime_tower_lift_height (z_hop alone bounds the spiral), spells the toolhead radius
// extruder_clearance_radius, and derives the spiral slope from the per-filament travel_slope instead
// of one global constant.
// ---------------------------------------------------------------------------------------------
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width)
{
// The brim is deposited material like any other and reaches past the wall on the first layer, so
// the sweeping rod has to clear it too.
//
// On top of it, two effects make a nominal outline fall short of the printed tower on its low
// corner even though it overshoots by millimetres on the high one: WipeTower re-centres the tower
// by rib_offset once its first-layer wall is known, and the precise check hulls extrusion centre
// lines, so the deposited material reaches half a line width further still. Allowing a line width
// per side covers both, which is what keeps an estimated footprint enclosing the real one and the
// pre-slice check stricter than the precise one.
return std::max(0., brim_width) + 2. * config.nozzle_diameter.get_at(0);
}
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier)
{
Polygons rings = zone.grown_nozzle;
if (any_body_tier)
append(rings, zone.grown_body);
return rings;
}
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint)
{
CompactedTowerZone zone;
if (tower_footprint.points.empty())
return zone;
// Spiral Z-hop at wipe-tower entry (the G3 Z I J that GCodeWriter emits for a SpiralLift) starts on
// the tower outline at a low Z. The spiral centre sits one radius away from the start point, so the
// circle reaches 2 * radius beyond the outline. radius = lift / (2*pi*atan(travel_slope)) is the
// same formula GCodeWriter uses; both are per filament, so take the widest any filament can make.
double spiral_reach = 0.;
for (size_t i = 0; i < config.z_hop.size(); ++i) {
const double lift = std::min(double(config.z_hop.get_at(i)), 5.);
if (lift < EPSILON)
continue;
const double slope = i < config.travel_slope.size() ? double(config.travel_slope.get_at(i)) : 0.;
if (slope < EPSILON)
continue;
spiral_reach = std::max(spiral_reach, 2. * lift / (2. * PI * std::atan(slope)));
}
// Working footprint = outline grown by the spiral envelope. All later clearance tests use this, so
// a travel that leaves the deposited wall at low Z is still treated as part of the tower.
zone.hull = tower_footprint;
if (spiral_reach > EPSILON) {
const Polygons grown = offset(tower_footprint, float(scale_(spiral_reach)), jtRound, scale_(0.1));
if (! grown.empty())
zone.hull = Geometry::convex_hull(grown);
}
// The rod sweeps the whole X axis, so its keep-out band is the tower's Y span widened by half
// the nozzle-to-rod offset per side (the instance carries the other half). Orca's sequential
// check has no such margin, having had no option to read it from until now.
zone.bbox_rod = zone.hull.bounding_box();
zone.bbox_rod.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
// Horizontal clearance, mirroring the sequential print check down to how the distance is split:
// there each of the two object hulls grows by half of extruder_clearance_radius, so the two
// outlines touch exactly when the objects are the full radius apart. Splitting it the same way
// here (half on the tower, half on the instance in compacted_wipe_tower_clearance) states the
// same criterion, and it is what lets the plater draw both outlines: they meet at the instant the
// check trips, instead of one of them being already buried inside the other. The smaller
// MAX_OUTER_NOZZLE_DIAMETER tier is the bare nozzle cone, the only part narrow enough to sit
// beside an object rising less than nozzle_height. The 0.2 mm shaved off is the same rounding
// slack the sequential check applies, 0.1 mm per side. Both rings are built here; which one a
// given object is measured against depends on its own height and is decided in
// compacted_wipe_tower_clearance().
zone.body_radius = config.extruder_clearance_radius.value;
zone.grown_body = offset(zone.hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
zone.grown_nozzle = offset(zone.hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
return zone;
}
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
const Polygon &inst_hull, double object_rise)
{
BoundingBox inst_bbox = inst_hull.bounding_box();
inst_bbox.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
// Only the Y span matters for the rod: it spans the whole X axis, so an object sharing the tower's
// Y band passes under it however far apart the two are in X.
const bool overlaps_in_y = std::min(inst_bbox.max.y(), zone.bbox_rod.max.y()) - std::max(inst_bbox.min.y(), zone.bbox_rod.min.y()) > 0;
CompactedTowerClearance result;
result.far_clearance = overlaps_in_y ? config.extruder_clearance_height_to_rod.value : config.extruder_clearance_height_to_lid.value;
// The rod and the lid are the only obstacles once the object stands far enough away. Closer than
// the toolhead radius it is the head body itself that hits the object, and it does so as soon as
// the object rises past the nozzle cone, which is far below the rod.
// The instance carries the other half of each clearance, the tower rings already hold the first
// half; see compacted_wipe_tower_zone(). Both halves are needed for the verdict to mean
// "a full radius apart", and drawing what is tested is what keeps the plater honest.
//
// Which tier applies is a property of this object alone: the head body sits above the nozzle cone,
// so it cannot reach an object that stays below nozzle_height however close it stands, and however
// tall the rest of the plate is.
const bool object_is_short = object_rise <= double(config.nozzle_height.value) + EPSILON;
result.body_clearance = object_is_short ? double(MAX_OUTER_NOZZLE_DIAMETER) : zone.body_radius;
const Polygons inst_near_nozzle = offset(inst_hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
const bool near_nozzle = ! intersection(zone.grown_nozzle, inst_near_nozzle).empty();
result.near_body = false;
if (! object_is_short) {
const Polygons inst_near_body = offset(inst_hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
result.near_body = ! intersection(zone.grown_body, inst_near_body).empty();
}
result.allowed_rise = result.far_clearance;
if (near_nozzle)
result.allowed_rise = 0.;
else if (result.near_body)
result.allowed_rise = std::min(result.far_clearance, double(config.nozzle_height.value));
return result;
}
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance)
{
// Exactly the half-clearance the check grew this instance by, so the halo drawn around an object is
// the very outline that was tested against the tower ring of the same tier. Passing the clearance
// the object was actually judged on keeps a short object from being drawn with the wide ring it is
// not subject to.
const Polygons grown = offset(inst_hull, float(scale_(compacted_tower_half_clearance(body_clearance))), jtRound, scale_(0.1));
return grown.empty() ? inst_hull : grown.front();
}
// Shared user-facing message for every compacted-tower clearance failure. Height-limit and too-close
// are the same class of layout violation under "No sparse layers", so they share one wording.
static std::string compacted_wipe_tower_clearance_error()
{
return L("The relative position of the model and the prime tower does not meet the requirements of the \"No sparse layers\" feature. Please adjust their relative positions, lower the model height, or turn off \"No sparse layers\".");
}
// Convex hull of one print instance in bed coordinates, the same outline both compacted tower checks
// compare against the tower.
static Polygon compacted_tower_print_instance_hull(const PrintObject &object, const PrintInstance &instance)
{
Points pts;
for (const ModelVolume *v : object.model_object()->volumes) {
if (! v->is_model_part())
continue;
Polygon hull = v->get_convex_hull_2d(Geometry::assemble_transform(Vec3d::Zero(), instance.model_instance->get_rotation(),
instance.model_instance->get_scaling_factor(), instance.model_instance->get_mirror()));
hull.translate(instance.shift - object.center_offset());
append(pts, hull.points);
}
return pts.empty() ? Polygon() : Geometry::convex_hull(pts);
}
// Footprint the compacted prime tower is expected to occupy on the plate, in bed coordinates.
// Before psWipeTower has run there is no tower geometry at all, so this falls back to the same
// estimate the plater builds its preview box from. Answering while the user is still arranging the
// plate is the whole point of the pre-slice check, and an estimate is all that can be had then.
static Polygon estimated_wipe_tower_footprint(const Print &print)
{
const PrintConfig &config = print.config();
const size_t filaments_cnt = print.extruders().size();
if (filaments_cnt == 0)
return Polygon();
const WipeTowerData &wtd = print.wipe_tower_data(filaments_cnt);
double width, depth, brim;
Vec2d local_min;
if (wtd.bbx.size().x() > EPSILON && wtd.bbx.size().y() > EPSILON) {
// The tower has already been generated once, so use its real box (brim included) instead of
// re-estimating. Same frame first_layer_wipe_tower_corners() works in.
width = wtd.bbx.size().x();
depth = wtd.bbx.size().y();
local_min = wtd.bbx.min + wtd.rib_offset.cast<double>();
brim = 0.;
} else {
depth = wtd.depth;
if (depth < EPSILON)
return Polygon();
// PartPlate::estimate_wipe_tower_size() squares the rib tower off and the preview box the user
// drags around is built from that, so match it here rather than keeping the nominal width.
width = config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? depth : double(config.prime_tower_width.value);
local_min = Vec2d::Zero();
brim = double(wtd.brim_width);
}
const double padding = compacted_tower_footprint_padding(config, brim);
local_min -= Vec2d(padding, padding);
width += 2. * padding;
depth += 2. * padding;
const Eigen::Rotation2Dd rot(Geometry::deg2rad(config.wipe_tower_rotation_angle.value));
const Vec2d translate(config.wipe_tower_x.get_at(print.get_plate_index()) + print.get_plate_origin()(0),
config.wipe_tower_y.get_at(print.get_plate_index()) + print.get_plate_origin()(1));
Polygon footprint;
for (const Vec2d &corner : { local_min,
Vec2d(local_min.x() + width, local_min.y()),
Vec2d(local_min.x() + width, local_min.y() + depth),
Vec2d(local_min.x(), local_min.y() + depth) }) {
const Vec2d p = rot * corner + translate;
footprint.points.emplace_back(scale_(p.x()), scale_(p.y()));
}
return footprint;
}
// Pre-slice counterpart of validate_compacted_wipe_tower_clearance(). It applies the very same
// clearance rule, but to an estimated tower footprint instead of the real tool-change extrusions,
// which is what lets it run from Print::validate() before anything has been sliced. Reporting through
// polygons / height_polygons rather than by throwing is what puts the collision area and the height
// limit plane on the plater, exactly the way sequential printing does it.
StringObjectException Print::compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons, std::vector<std::pair<Polygon, float>> *height_polygons)
{
const PrintConfig &config = print.config();
if (! wipe_tower_sparse_layers_skipped(config) || config.print_sequence != PrintSequence::ByLayer || ! print.has_wipe_tower())
return {};
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, estimated_wipe_tower_footprint(print));
if (zone.empty())
return {};
StringObjectException exception;
Polygons offenders;
bool body_tier_used = false;
for (const PrintObject *object : print.objects()) {
const double object_top = unscaled<double>(object->max_z());
for (const PrintInstance &instance : object->instances()) {
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
if (inst_hull.points.empty())
continue;
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
// Every tier the precise check applies is applied here too, otherwise an object standing
// within the toolhead radius would pass here and then be rejected mid-slice, which is the
// one outcome this check exists to prevent. The compacted tower base is unknown before
// slicing, so the rise is measured from the plate rather than from the tower top; that
// overstates it by the tower's own height and makes this check err strict, never lax.
if (object_top <= clearance.allowed_rise + EPSILON)
continue;
// Height-limit and too-close cases share one user-facing message: both mean the layout
// violates the "No sparse layers" clearance rule, and the remedies are the same.
const std::string msg = compacted_wipe_tower_clearance_error();
if (exception.string.empty()) {
exception.string = msg;
exception.object = instance.model_instance;
} else {
// Same wording for every offender; keep a single copy and drop the object pointer.
exception.object = nullptr;
}
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
offenders.emplace_back(outline);
if (height_polygons)
height_polygons->emplace_back(outline, float(clearance.allowed_rise));
}
}
// Draw the tower's keep-out ring alongside the offending objects, so the collision area reads as
// "this object reaches into the space the toolhead needs around the tower" rather than as a lone
// highlighted object. Emitted only on a real collision; the plater discards polygons otherwise.
// Only the rings some object on this plate is actually measured against are drawn, so that a ring
// and an object outline touching always means that object is over its limit.
if (polygons && ! offenders.empty()) {
append(*polygons, compacted_wipe_tower_rings(zone, body_tier_used));
append(*polygons, offenders);
}
return exception;
}
// With wipe_tower_no_sparse_layers the tower only grows on layers that carry a real toolchange,
// so it ends up far below the object and the nozzle has to descend to it. While the nozzle sits
// down on the compacted tower the rod is at tower_z + extruder_clearance_height_to_rod, and it
// sweeps the tower's Y band across the whole X axis. Anything already printed above that line and
// sharing the band gets hit. Nearer than the toolhead radius the head body hits the object well before
// the rod does, which is the horizontal half of the same problem. The spiral Z-hop that opens a wipe-
// tower travel also leaves the extrusion outline at a low Z, so the footprint used here is the
// deposited hull grown by the spiral circle's maximum reach. This mirrors both clearance checks of
// sequential printing, except that the tower is revisited over and over, so every object is compared
// against it.
void Print::validate_compacted_wipe_tower_clearance() const
{
// Nothing to check when the tower is not compacted: it then follows the object as usual and the
// regular by-layer clearance check already covers it. Asking wipe_tower_sparse_layers_skipped()
// rather than the raw option keeps this from rejecting plates whose tower is in fact full height.
if (! wipe_tower_sparse_layers_skipped(m_config) || m_config.print_sequence != PrintSequence::ByLayer)
return;
const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes = m_wipe_tower_data.tool_changes;
if (tool_changes.empty() || m_objects.empty())
return;
// Same accumulation the G-code emitter runs, so validation and output cannot disagree.
const std::vector<float> tower_z = compute_compacted_wipe_tower_z(tool_changes, float(m_config.z_offset.value));
// Wipe tower footprint: build it from the ACTUAL tool-change extrusions rather than the nominal
// width x depth rectangle returned by first_layer_wipe_tower_corners(). With a rib wall the printed
// wall bulges past the nominal box and the first-layer brim reaches even further; the nominal box
// (m_wipe_tower_data.bbx) undercounts that outermost extent by several millimetres, which is
// exactly the extent that decides how close the sweeping rod comes to a neighbouring object. The
// extrusion end-points are stored in the wipe-tower local frame, so we map them to the bed frame
// with the same transform the G-code emitter applies. The two emitters differ in where rib_offset
// enters: WipeTowerIntegration::append_tcr() (type 1) rotates the point and then adds the offset,
// append_tcr2() (type 2) adds it before rotating. On a rotated rib-wall tower the two land several
// millimetres apart, which is exactly the margin this check measures, so follow the emitter in use.
const Eigen::Rotation2Dd wt_rot(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
const Vec2d wt_translate(m_config.wipe_tower_x.get_at(m_plate_index) + m_origin(0),
m_config.wipe_tower_y.get_at(m_plate_index) + m_origin(1));
const Vec2d rib_off = m_wipe_tower_data.rib_offset.cast<double>();
const bool rib_off_rotates = this->wipe_tower_type() == WipeTowerType::Type2;
auto to_bed = [&wt_rot, &wt_translate, &rib_off, rib_off_rotates](const Vec2d &pt) {
return rib_off_rotates ? Vec2d(wt_rot * (pt + rib_off) + wt_translate) : Vec2d(wt_rot * pt + wt_translate + rib_off);
};
Points tower_pts;
for (const std::vector<WipeTower::ToolChangeResult> &layer : tool_changes) {
if (layer.empty() || wipe_tower_layer_is_sparse(layer))
continue;
for (const WipeTower::ToolChangeResult &tcr : layer)
for (size_t i = 0; i < tcr.extrusions.size(); ++i) {
// A zero width marks a travel end-point. Keep it only when it opens a real extrusion, so
// the hull covers the deposited material and nothing else; travels reach a bit further out
// than the walls do.
const WipeTower::Extrusion &e = tcr.extrusions[i];
if (e.width == 0.f && (i + 1 == tcr.extrusions.size() || tcr.extrusions[i + 1].width == 0.f))
continue;
const Vec2d p = to_bed(Vec2d(e.pos.x(), e.pos.y()));
tower_pts.emplace_back(scale_(p.x()), scale_(p.y()));
}
}
if (tower_pts.empty())
return;
const CompactedTowerZone zone = compacted_wipe_tower_zone(m_config, Geometry::convex_hull(tower_pts));
if (zone.empty())
return;
for (const PrintObject *object : m_objects) {
const double object_top = unscaled<double>(object->max_z());
for (const PrintInstance &instance : object->instances()) {
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
if (inst_hull.points.empty())
continue;
// Report the worst layer rather than the first offending one, it is the one that explains the
// collision best. The rise has to be known before the clearance: it is what selects the
// horizontal tier, the nozzle cone being out of the head body's reach.
double max_rise = 0.;
for (size_t i = 0; i < tool_changes.size(); ++i) {
if (tool_changes[i].empty() || wipe_tower_layer_is_sparse(tool_changes[i]))
continue;
// Nothing above the current layer exists yet, so a tall object only counts up to it.
const double rise = std::min(object_top, double(tool_changes[i].front().print_z)) - tower_z[i];
if (rise > max_rise)
max_rise = rise;
}
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(m_config, zone, inst_hull, max_rise);
if (max_rise <= clearance.allowed_rise + EPSILON)
continue;
// Same wording as compacted_wipe_tower_clearance_valid(): height-limit and too-close
// share one message, since both are layout violations of "No sparse layers".
throw Slic3r::SlicingError(compacted_wipe_tower_clearance_error());
}
}
}
//BBS
static StringObjectException layered_print_cleareance_valid(const Print &print, StringObjectException *warning)
{
@@ -1408,6 +1781,16 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
if (!layer_warning.string.empty())
add_warning(layer_warning);
// Orca: a compacted prime tower drags the nozzle back down to the plate on every toolchange, so
// tall objects collide with it much like they do in sequential printing. Checking it here rather
// than only during slicing is what lets the plater show the collision area and the height limit
// while the plate is still being arranged.
ret = compacted_wipe_tower_clearance_valid(*this, collison_polygons, height_polygons);
if (!ret.string.empty()) {
ret.type = STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT;
return ret;
}
}
if (m_config.enable_prime_tower) {
@@ -2620,6 +3003,12 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (this->has_wipe_tower()) {
m_fake_wipe_tower.set_pos({ m_config.wipe_tower_x.get_at(m_plate_index), m_config.wipe_tower_y.get_at(m_plate_index) });
// Validated on every process() run rather than only when the wipe tower step is (re)generated.
// Moving the tower changes only wipe_tower_x/y, which invalidates psSkirtBrim but not psWipeTower,
// so a validate call living inside _make_wipe_tower would be skipped and keep using the stale
// position, missing a fresh collision. The tower geometry (tool_changes) is stored in the local
// frame and is position independent, so re-checking here with the current position is correct.
this->validate_compacted_wipe_tower_clearance();
}
if (this->set_started(psSkirtBrim)) {
+87
View File
@@ -1160,6 +1160,8 @@ public:
//BBS
static StringObjectException sequential_print_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
// Orca: pre-slice clearance check for a prime tower compacted by "No sparse layers".
static StringObjectException compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
ConflictResultOpt get_conflict_result() const { return m_conflict_result; }
// Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim.
@@ -1174,6 +1176,8 @@ public:
void set_calib_params(const Calib_Params& params);
const Calib_Params& calib_params() const { return m_calib_params; }
Vec2d translate_to_print_space(const Vec2d &point) const;
// Orca: precise counterpart of compacted_wipe_tower_clearance_valid(), run once the tower exists.
void validate_compacted_wipe_tower_clearance() const;
float get_wipe_tower_depth() const { return m_wipe_tower_data.depth; }
BoundingBoxf get_wipe_tower_bbx() const { return m_wipe_tower_data.bbx; }
Vec2f get_rib_offset() const { return m_wipe_tower_data.rib_offset; }
@@ -1394,6 +1398,89 @@ public:
};
// ---------------------------------------------------------------------------------------------
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers. Shared by the precise
// check that runs on the real extrusions, the pre-slice estimate that feeds the plater with collision
// polygons, and the plater's own live preview while the user drags the tower or an object around.
// Keeping the rule in one place is what stops those three from drifting apart and reporting different
// things for the same plate.
// ---------------------------------------------------------------------------------------------
// Half of a clearance distance, the share each of the two outlines carries. Sequential printing splits
// extruder_clearance_radius between the two object hulls this way; the tower checks split their
// clearances between the tower ring and the instance hull for the same reason, so that the two
// outlines the plater draws touch precisely when the check trips. The 0.2 mm comes off first: it is
// the rounding slack the sequential check applies, 0.1 mm per side.
inline double compacted_tower_half_clearance(double clearance) { return 0.5 * (clearance - 0.2); }
// Keep-out geometry a compacted tower projects onto the plate, derived from its bare footprint.
struct CompactedTowerZone
{
// Footprint the checks work on: the raw outline grown by the spiral Z-hop envelope.
Polygon hull;
// hull grown by half the toolhead radius; an object whose own half-grown hull reaches into it is
// hit by the head body. This is also the ring the plater draws.
Polygons grown_body;
// hull grown by half the bare nozzle cone radius, the innermost tier.
Polygons grown_nozzle;
// hull bounding box, the Y band the rod sweeps.
BoundingBox bbox_rod;
// Full body clearance, of which grown_body carries half. Which of the two tiers applies is decided
// per object rather than here; see compacted_wipe_tower_clearance().
double body_radius { 0. };
bool empty() const { return hull.points.empty(); }
};
// Per-side padding a bare wipe tower outline needs before the clearance checks may treat it as the
// tower's footprint. Callers whose outline already carries the first-layer brim pass zero for it.
// Shared by the pre-slice estimate and the plater's live preview: both start from an outline that
// falls short of the printed tower in the same two ways, and padding them by different amounts is
// exactly how the preview and the validation behind it would end up disagreeing.
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width);
// Grow a bare tower footprint (bed frame, scaled) into its keep-out zone.
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint);
// How far an object may rise above the compacted tower base before the toolhead hits it.
struct CompactedTowerClearance
{
// Height the object may reach above the tower base. Zero means it may not rise at all.
double allowed_rise;
// Clearance that applies once the object stands clear of the toolhead in XY, i.e. rod or lid.
double far_clearance;
// The object sits within the toolhead radius, so the head body limits it rather than the rod.
bool near_body;
// Horizontal clearance this particular object has to keep from the tower: the full toolhead
// radius once it rises past the nozzle cone, the bare cone while it stays below. It is what the
// error message quotes and what the plater grows the object outline by.
double body_clearance;
};
// object_rise is the height above the tower base that the caller is going to compare against
// allowed_rise. It also selects the horizontal tier, so the two cannot disagree.
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
const Polygon &inst_hull, double object_rise);
// This object was judged on a tier reaching past the bare nozzle cone, so the wide ring is the one its
// outline has to be drawn against.
inline bool compacted_tower_body_tier(const CompactedTowerClearance &clearance)
{
return clearance.body_clearance > double(MAX_OUTER_NOZZLE_DIAMETER);
}
// Keep-out rings to draw around the tower. The nozzle one always applies; the wide body one is drawn
// only when some object on the plate is actually measured against it, otherwise it would show a
// keep-out zone no object can violate.
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier);
// Outline to hand the plater for an offending object: the instance hull grown by the same half
// clearance the check grew it by, which is CompactedTowerClearance::body_clearance for that object.
// Sequential printing reports its hulls the same way, and it doubles as the fix for the bare hull
// being unusable on screen, where drawn flat it hides under the object and drawn at the height limit
// it ends up buried inside the mesh.
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance);
} /* slic3r_Print_hpp_ */
#endif
+115 -3
View File
@@ -2549,6 +2549,16 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back("5");
def->mode = comAdvanced;
// Orca: already carried by the BBL/Qidi/Geeetech/Eryone machine profiles, which inherited it from
// the BambuStudio import; without a definition here it was parsed as an unknown key and dropped.
def = this->add("extruder_clearance_dist_to_rod", coFloat);
def->label = L("Distance to rod");
def->tooltip = L("Horizontal distance of the nozzle tip to the rod's farther edge. Used for collision avoidance in by-object printing.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(40));
def = this->add("extruder_clearance_height_to_rod", coFloat);
def->label = L("Height to rod");
def->tooltip = L("Distance from the nozzle tip to the lower rod. Used for collision avoidance in by-object printing.");
@@ -5537,6 +5547,16 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("unsupported_wall_last", coBool);
def->label = L("Print unsupported walls last");
def->category = L("Quality");
def->tooltip = L("Wall loops that lie entirely in mid air are printed once something can hold them:\n"
"they are extruded after the other walls of their island, innermost first, whatever the wall order is.\n"
"A loop that only the bridges of this layer can anchor waits until those bridges are printed, while a loop running "
"alongside a supported wall keeps its place before the infill, which needs it as an anchor.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("outer_wall_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Outer walls");
@@ -6282,6 +6302,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("wipe_inward_distance", coFloatOrPercent);
def->label = L("Wipe inward distance");
def->category = L("Quality");
// xgettext:no-c-format, no-boost-format
def->tooltip = L("The distance the wipe path is shifted away from the external perimeter, specified in millimeters "
"or as a percentage of the actual outer-wall extrusion width.\n\n"
"For example, 50% shifts the path by half of the outer-wall width. The effective offset is limited "
@@ -6673,8 +6694,10 @@ void PrintConfigDef::init_fff_params()
def = this->add("wipe_tower_no_sparse_layers", coBool);
def->label = L("No sparse layers (beta)");
def->tooltip = L("If enabled, the wipe tower will not be printed on layers with no tool changes. "
"On layers with a tool change, extruder will travel downward to print the wipe tower. "
"User is responsible for ensuring there is no collision with the print.");
"On layers with a tool change, extruder will travel downward to print the wipe tower, "
"so the tower ends up below the model and the toolhead has to reach down to it. "
"Layouts where that would collide with an already printed object are rejected. "
"Has no effect with smooth timelapse or clumping detection, which need a tower on every layer.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
@@ -6700,6 +6723,34 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.emplace_back(L("Cyclic"));
def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default));
def = this->add("toolchange_cyclic_order", coString);
def->label = L("Cyclic order");
def->category = L("Advanced");
def->tooltip = L(
"Custom filament sequence used by the cyclic toolchange ordering, as filament numbers separated by commas (e.g. \"3,2,1,4\").\n"
"Each layer prints its filaments following this sequence; filaments not listed are printed last, in ascending order.\n"
"Leave empty to cycle through the filaments in ascending order."
);
def->mode = comExpert;
def->set_default_value(new ConfigOptionString(""));
def = this->add("toolchange_cyclic_first_layer", coBool);
def->label = L("Apply cyclic order to first layer");
def->category = L("Advanced");
def->tooltip = L(
"Applies the cyclic toolchange order to the first layer as well.\n"
"By default this is disabled, because the first layer is instead ordered for the best bed "
"adhesion: filaments that print small, fragile first-layer features are printed last, so the "
"following tool changes and travel moves are less likely to knock those weakly anchored parts "
"loose. This first-layer order also honors a custom first layer filament sequence when one is set. "
"The cyclic order's benefit (extra tool changes give each layer more time to cool) does not apply "
"to the first layer, which is printed slowly and hot for adhesion.\n"
"Enable this only if you need the exact same tool sequence on every layer, including the first, at "
"the cost of that adhesion optimization."
);
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("slice_closing_radius", coFloat);
def->label = L("Slice gap closing radius");
def->category = L("Quality");
@@ -6712,7 +6763,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("slicing_mode", coEnum);
def->label = L("Slicing Mode");
def->category = L("Other");
def->category = L("Others");
def->tooltip = L("Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model.");
def->enum_keys_map = &ConfigOptionEnum<SlicingMode>::get_enum_values();
def->enum_values.push_back("regular");
@@ -11923,6 +11974,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def->tooltip = L("Do not run any validity checks, such as G-code path conflicts check.");
def->set_default_value(new ConfigOptionBool(false));
// --strict turns the non-critical slicing warnings the CLI otherwise only logs into a
// failed run, and records strict_mode in result.json so consumers can tell the modes apart.
def = this->add("strict", coBool);
def->label = L("Strict mode");
def->tooltip = L("Exit non-zero when slicing raises a non-critical warning that is "
"otherwise only logged, such as a model that needs support while "
"support is disabled. Use this in CI or scripted pipelines that should "
"never ship a subtly broken slice. Each such warning is also listed "
"with a stable class in the `warnings` array of result.json, which is "
"written on Linux only. Cannot be combined with --no-check, which skips "
"the support check.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("normative_check", coBool);
def->label = L("Normative check");
def->tooltip = L("Check the normative items.");
@@ -11943,6 +12007,26 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def->tooltip = L("This outputs the model\u2019s information.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("inspect_mesh", coBool);
def->label = L("Inspect mesh (JSON to stdout)");
def->tooltip = L("Print a JSON summary of each loaded object to stdout, then exit: its bounding boxes and the "
"convex hull faces it can be laid on, with their normals, areas and centers. These are the faces "
"the --ground-* options choose from. Machine-readable alternative to --info.");
def->set_default_value(new ConfigOptionBool(false));
// --inspect-paint \u2014 dump the per-facet enforcer/blocker/extruder/fuzzy
// paint state stored on the loaded model (supports, seam, MMU color,
// fuzzy-skin) as JSON. Read-only; lets CI / scripted / AI tooling
// reason about existing paint on a .3mf without loading the GUI.
def = this->add("inspect_paint", coBool);
def->label = L("Inspect paint (JSON to stdout)");
def->tooltip = L("Print a structured JSON summary of every painted layer "
"(supports, seam, MMU color, fuzzy-skin) already stored on "
"the loaded model \u2014 per-state facet count, surface area, "
"and mesh-local bounding box \u2014 then exit. Machine-readable "
"alternative to opening the paint gizmos in the GUI.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("export_settings", coString);
def->label = L("Export Settings");
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
@@ -12062,6 +12146,34 @@ CLITransformConfigDef::CLITransformConfigDef()
def->sidetext = u8"°"; // degrees, don't need translation
def->set_default_value(new ConfigOptionFloat(0));
// The --ground-* options choose from the faces the "Lay on Face" gizmo offers. Like the other
// transforms they run in command-line order, so they see the rotations given before them.
def = this->add("ground_largest_face", coBool);
def->label = L("Ground largest face");
def->tooltip = L("Lay each object on the largest face of its convex hull and drop it onto the bed. Of equally large "
"faces, the one already facing down is kept. Objects without a face large enough to rest on are left "
"as they are. Transforms run in command-line order, so rotations given before this option are respected. "
"--orient 1 runs after all transforms and replaces the orientation.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("ground_face_normal", coString);
def->label = L("Ground face by normal");
def->tooltip = L("Lay each object on the convex hull face whose outward normal is closest to the direction NX,NY,NZ "
"and drop it onto the bed. The direction is in object coordinates, which include the rotations given "
"before this option and match the plate axes unless the input file rotates the object. For example, "
"1,0,0 stands the object on its +X side. --orient 1 runs after all transforms and replaces the orientation.");
def->cli_params = "NX,NY,NZ";
def->set_default_value(new ConfigOptionString(""));
def = this->add("ground_face_point", coString);
def->label = L("Ground face at point");
def->tooltip = L("Lay each object on the convex hull face that contains the point X,Y,Z and drop it onto the bed. "
"The point is in object coordinates, which include the rotations given before this option; "
"--inspect-mesh reports face centers in them. Objects without such a face are left as they are, and "
"the run fails if no object has one. --orient 1 runs after all transforms and replaces the orientation.");
def->cli_params = "X,Y,Z";
def->set_default_value(new ConfigOptionString(""));
def = this->add("scale", coFloat);
def->label = L("Scale");
def->tooltip = L("Scale the model by a float factor.");
+4
View File
@@ -1353,6 +1353,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloatsNullable, filament_ironing_speed))
// Detect bridging perimeters
((ConfigOptionBool, detect_overhang_wall))
((ConfigOptionBool, unsupported_wall_last))
((ConfigOptionInt, outer_wall_filament_id))
((ConfigOptionInt, inner_wall_filament_id))
((ConfigOptionFloatOrPercent, inner_wall_line_width))
@@ -1627,6 +1628,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, manual_filament_change))
((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
((ConfigOptionString, toolchange_cyclic_order))
((ConfigOptionBool, toolchange_cyclic_first_layer))
((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode))
@@ -1788,6 +1791,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBools, slow_down_for_layer_cooling))
((ConfigOptionInts, close_fan_the_first_x_layers))
((ConfigOptionEnum<DraftShield>, draft_shield))
((ConfigOptionFloat, extruder_clearance_dist_to_rod))//BBS
((ConfigOptionFloat, extruder_clearance_height_to_rod))//BBs
((ConfigOptionFloat, extruder_clearance_height_to_lid))//BBS
((ConfigOptionFloat, extruder_clearance_radius))
+1
View File
@@ -1501,6 +1501,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "fuzzy_skin_octaves"
|| opt_key == "fuzzy_skin_persistence"
|| opt_key == "detect_overhang_wall"
|| opt_key == "unsupported_wall_last"
|| opt_key == "overhang_reverse"
|| opt_key == "overhang_reverse_internal_only"
|| opt_key == "overhang_reverse_threshold"