Files
OrcaSlicer/tests/libslic3r/test_sketchedit.cpp
T
Tommaso Bianchi 2b02a8e3dd Design tab: move the CAD sources into their own folder
Review request on PR #15238: "Place CAD-related files (e.g. CadDocument/
GeometryEngine) into a separate folder."

  src/libslic3r/CAD/     the kernel — CadDocument, GeometryEngine, the four
                         Sketch* units, SketchSolver, ThreadStandards
  src/slic3r/GUI/CAD/    the tab — DesignPanel, DesignCanvas, DesignSketchTool,
                         SketchInlineEditor, McpControl, generated DesignOffer

Pure relocation: no line of logic changes. Two include rewrites follow from it —
files that moved re-spell their own neighbours against src/ (already on the
include path), and files that did not move pick up the new folder. docs and
docs/ux/mockups/gen_offer_table.py follow the same paths.

Verified: libslic3r, libslic3r_gui and libslic3r_tests all build, CAD suite green
at 2518 assertions in 194 test cases, and the sibling fork builds identically —
17 shared sources still byte-identical, 8 diverging by their expected counts.
2026-08-20 17:44:34 +02:00

489 lines
14 KiB
C++

#include <catch2/catch_all.hpp> // mainline OrcaSlicer ships Catch2 v3 (v2 was catch2/catch.hpp)
#include "libslic3r/CAD/SketchEngine.hpp"
#include <cmath>
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
TEST_CASE("Mirror Line across Y axis", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(3, 2);
e.p1 = Vec2d(5, 4);
Vec2d a(0, -1);
Vec2d b(0, 1);
auto result = SketchEngine::mirror_entities({e}, a, b);
REQUIRE(result.size() == 1);
const auto& m = result[0];
REQUIRE(m.type == SketchEntity::Type::Line);
REQUIRE_THAT(m.p0.x(), WithinAbs(-3.0, 1e-9));
REQUIRE_THAT(m.p0.y(), WithinAbs(2.0, 1e-9));
REQUIRE_THAT(m.p1.x(), WithinAbs(-5.0, 1e-9));
REQUIRE_THAT(m.p1.y(), WithinAbs(4.0, 1e-9));
}
TEST_CASE("Mirror Circle across Y axis", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Circle;
e.center = Vec2d(5, 0);
e.p0 = Vec2d(5, 0);
e.radius = 3;
Vec2d a(0, -1);
Vec2d b(0, 1);
auto result = SketchEngine::mirror_entities({e}, a, b);
REQUIRE(result.size() == 1);
const auto& m = result[0];
REQUIRE(m.type == SketchEntity::Type::Circle);
REQUIRE_THAT(m.center.x(), WithinAbs(-5.0, 1e-9));
REQUIRE_THAT(m.center.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(m.radius, WithinAbs(3.0, 1e-9));
REQUIRE_THAT(m.p0.x(), WithinAbs(-5.0, 1e-9));
REQUIRE_THAT(m.p0.y(), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Mirror Arc across X axis", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Arc;
e.center = Vec2d(0, 0);
e.radius = 1.0;
e.start_angle = 0.0;
e.end_angle = M_PI / 2.0;
e.p0 = Vec2d(1, 0);
e.p1 = Vec2d(0, 1);
Vec2d a(-1, 0);
Vec2d b(1, 0);
auto result = SketchEngine::mirror_entities({e}, a, b);
REQUIRE(result.size() == 1);
const auto& m = result[0];
REQUIRE(m.type == SketchEntity::Type::Arc);
REQUIRE_THAT(m.p0.x(), WithinAbs(1.0, 1e-9));
REQUIRE_THAT(m.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(m.p1.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(m.p1.y(), WithinAbs(-1.0, 1e-9));
double sweep = m.end_angle - m.start_angle;
double orig_sweep = e.end_angle - e.start_angle;
REQUIRE(orig_sweep > 0.0);
REQUIRE(sweep < 0.0);
}
TEST_CASE("Offset Line by positive d", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(0, 0);
e.p1 = Vec2d(10, 0);
auto result = SketchEngine::offset_entities({e}, 2.0);
REQUIRE(result.size() == 1);
const auto& o = result[0];
REQUIRE(o.type == SketchEntity::Type::Line);
REQUIRE_THAT(o.p0.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(o.p0.y(), WithinAbs(2.0, 1e-9));
REQUIRE_THAT(o.p1.x(), WithinAbs(10.0, 1e-9));
REQUIRE_THAT(o.p1.y(), WithinAbs(2.0, 1e-9));
}
TEST_CASE("Offset Circle: expand and collapse", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Circle;
e.center = Vec2d(0, 0);
e.p0 = Vec2d(0, 0);
e.radius = 5;
auto expanded = SketchEngine::offset_entities({e}, 2.0);
REQUIRE(expanded.size() == 1);
REQUIRE_THAT(expanded[0].radius, WithinAbs(7.0, 1e-9));
auto collapsed = SketchEngine::offset_entities({e}, -5.0);
REQUIRE(collapsed.empty());
}
TEST_CASE("Offset Arc by positive d", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Arc;
e.center = Vec2d(0, 0);
e.radius = 4.0;
e.start_angle = 0.0;
e.end_angle = M_PI / 2.0;
e.p0 = Vec2d(4, 0);
e.p1 = Vec2d(0, 4);
auto result = SketchEngine::offset_entities({e}, 1.0);
REQUIRE(result.size() == 1);
const auto& o = result[0];
REQUIRE(o.type == SketchEntity::Type::Arc);
REQUIRE_THAT(o.radius, WithinAbs(5.0, 1e-9));
REQUIRE_THAT(o.p0.x(), WithinAbs(5.0, 1e-9));
REQUIRE_THAT(o.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(o.p1.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(o.p1.y(), WithinAbs(5.0, 1e-9));
}
TEST_CASE("Fillet right-angle corner", "[SketchEdit]")
{
SketchEntity a;
a.type = SketchEntity::Type::Line;
a.p0 = Vec2d(0, 0);
a.p1 = Vec2d(10, 0);
SketchEntity b;
b.type = SketchEntity::Type::Line;
b.p0 = Vec2d(10, 0);
b.p1 = Vec2d(10, 10);
SketchEntity a_out, b_out, arc_out;
bool ok = SketchEngine::fillet_lines(a, b, 2.0, a_out, b_out, arc_out);
REQUIRE(ok);
REQUIRE_THAT(a_out.p0.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(a_out.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(a_out.p1.x(), WithinAbs(8.0, 1e-9));
REQUIRE_THAT(a_out.p1.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(b_out.p0.x(), WithinAbs(10.0, 1e-9));
REQUIRE_THAT(b_out.p0.y(), WithinAbs(2.0, 1e-9));
REQUIRE_THAT(b_out.p1.x(), WithinAbs(10.0, 1e-9));
REQUIRE_THAT(b_out.p1.y(), WithinAbs(10.0, 1e-9));
REQUIRE(arc_out.type == SketchEntity::Type::Arc);
REQUIRE_THAT(arc_out.radius, WithinAbs(2.0, 1e-9));
REQUIRE_THAT(arc_out.center.x(), WithinAbs(8.0, 1e-9));
REQUIRE_THAT(arc_out.center.y(), WithinAbs(2.0, 1e-9));
REQUIRE_THAT((arc_out.p0 - arc_out.center).norm(), WithinAbs(2.0, 1e-9));
REQUIRE_THAT((arc_out.p1 - arc_out.center).norm(), WithinAbs(2.0, 1e-9));
}
TEST_CASE("Fillet parallel lines returns false", "[SketchEdit]")
{
SketchEntity a;
a.type = SketchEntity::Type::Line;
a.p0 = Vec2d(0, 0);
a.p1 = Vec2d(10, 0);
SketchEntity b;
b.type = SketchEntity::Type::Line;
b.p0 = Vec2d(0, 5);
b.p1 = Vec2d(10, 5);
SketchEntity a_out, b_out, arc_out;
REQUIRE_FALSE(SketchEngine::fillet_lines(a, b, 1.0, a_out, b_out, arc_out));
}
TEST_CASE("Fillet arc too big returns false", "[SketchEdit]")
{
SketchEntity a;
a.type = SketchEntity::Type::Line;
a.p0 = Vec2d(0, 0);
a.p1 = Vec2d(1, 0);
SketchEntity b;
b.type = SketchEntity::Type::Line;
b.p0 = Vec2d(1, 0);
b.p1 = Vec2d(1, 1);
SketchEntity a_out, b_out, arc_out;
REQUIRE_FALSE(SketchEngine::fillet_lines(a, b, 5.0, a_out, b_out, arc_out));
}
TEST_CASE("Trim right arm", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(-5, 0);
e.p1 = Vec2d(5, 0);
SketchEntity vc;
vc.type = SketchEntity::Type::Line;
vc.p0 = Vec2d(0, -5);
vc.p1 = Vec2d(0, 5);
bool ok = SketchEngine::trim_entity(e, {vc}, Vec2d(3, 0));
REQUIRE(ok);
REQUIRE_THAT(e.p0.x(), WithinAbs(-5.0, 1e-9));
REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p1.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Trim left arm", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(-5, 0);
e.p1 = Vec2d(5, 0);
SketchEntity vc;
vc.type = SketchEntity::Type::Line;
vc.p0 = Vec2d(0, -5);
vc.p1 = Vec2d(0, 5);
bool ok = SketchEngine::trim_entity(e, {vc}, Vec2d(-3, 0));
REQUIRE(ok);
REQUIRE_THAT(e.p0.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p1.x(), WithinAbs(5.0, 1e-9));
REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Trim no cut (u out of range)", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(-5, 0);
e.p1 = Vec2d(5, 0);
SketchEntity other;
other.type = SketchEntity::Type::Line;
other.p0 = Vec2d(0, 3);
other.p1 = Vec2d(0, 8);
REQUIRE_FALSE(SketchEngine::trim_entity(e, {other}, Vec2d(3, 0)));
}
TEST_CASE("Extend forward to line", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(0, 0);
e.p1 = Vec2d(2, 0);
SketchEntity other;
other.type = SketchEntity::Type::Line;
other.p0 = Vec2d(5, -5);
other.p1 = Vec2d(5, 5);
bool ok = SketchEngine::extend_entity(e, {other}, Vec2d(2, 0));
REQUIRE(ok);
REQUIRE_THAT(e.p0.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p1.x(), WithinAbs(5.0, 1e-9));
REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Extend forward to circle", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(0, 0);
e.p1 = Vec2d(2, 0);
SketchEntity other;
other.type = SketchEntity::Type::Circle;
other.center = Vec2d(10, 0);
other.p0 = Vec2d(10, 0);
other.radius = 3;
bool ok = SketchEngine::extend_entity(e, {other}, Vec2d(2, 0));
REQUIRE(ok);
REQUIRE_THAT(e.p0.x(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p1.x(), WithinAbs(7.0, 1e-9));
REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Extend backward", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(0, 0);
e.p1 = Vec2d(2, 0);
SketchEntity other;
other.type = SketchEntity::Type::Line;
other.p0 = Vec2d(-3, -5);
other.p1 = Vec2d(-3, 5);
bool ok = SketchEngine::extend_entity(e, {other}, Vec2d(0, 0));
REQUIRE(ok);
REQUIRE_THAT(e.p0.x(), WithinAbs(-3.0, 1e-9));
REQUIRE_THAT(e.p0.y(), WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.p1.x(), WithinAbs(2.0, 1e-9));
REQUIRE_THAT(e.p1.y(), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Extend no target", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Line;
e.p0 = Vec2d(0, 0);
e.p1 = Vec2d(2, 0);
SketchEntity other;
other.type = SketchEntity::Type::Line;
other.p0 = Vec2d(5, -5);
other.p1 = Vec2d(5, -1);
REQUIRE_FALSE(SketchEngine::extend_entity(e, {other}, Vec2d(2, 0)));
}
// --- Arc/Circle subject trim & extend (Fase 4.5 kernel) -------------------
TEST_CASE("Trim arc drops the picked (start) side", "[SketchEdit]")
{
// Upper semicircle r=5, ccw from (5,0) to (-5,0); cutter = vertical axis.
SketchEntity e;
e.type = SketchEntity::Type::Arc;
e.center = Vec2d(0, 0);
e.radius = 5;
e.start_angle = 0.0;
e.end_angle = M_PI;
SketchEntity cut;
cut.type = SketchEntity::Type::Line;
cut.p0 = Vec2d(0, -10);
cut.p1 = Vec2d(0, 10);
// Pick the right quarter (phi=pi/4) -> it is removed, left quarter kept.
bool ok = SketchEngine::trim_entity(e, {cut}, Vec2d(5 * std::cos(M_PI/4), 5 * std::sin(M_PI/4)));
REQUIRE(ok);
REQUIRE(e.type == SketchEntity::Type::Arc);
REQUIRE_THAT(e.radius, WithinAbs(5.0, 1e-9));
REQUIRE_THAT(e.start_angle, WithinAbs(M_PI / 2.0, 1e-9));
REQUIRE_THAT(e.end_angle, WithinAbs(M_PI, 1e-9));
}
TEST_CASE("Trim arc drops the picked (end) side", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Arc;
e.center = Vec2d(0, 0);
e.radius = 5;
e.start_angle = 0.0;
e.end_angle = M_PI;
SketchEntity cut;
cut.type = SketchEntity::Type::Line;
cut.p0 = Vec2d(0, -10);
cut.p1 = Vec2d(0, 10);
// Pick the left quarter (phi=3pi/4) -> removed, right quarter kept.
bool ok = SketchEngine::trim_entity(e, {cut}, Vec2d(5 * std::cos(3*M_PI/4), 5 * std::sin(3*M_PI/4)));
REQUIRE(ok);
REQUIRE(e.type == SketchEntity::Type::Arc);
REQUIRE_THAT(e.start_angle, WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.end_angle, WithinAbs(M_PI / 2.0, 1e-9));
}
TEST_CASE("Trim circle opens into an arc excluding the pick", "[SketchEdit]")
{
// Full circle r=5; vertical axis cuts it at (0,+-5). Pick the right side
// (5,0): the kept arc is the left half, sweeping pi and centred on (-5,0).
SketchEntity e;
e.type = SketchEntity::Type::Circle;
e.center = Vec2d(0, 0);
e.p0 = Vec2d(5, 0);
e.radius = 5;
SketchEntity cut;
cut.type = SketchEntity::Type::Line;
cut.p0 = Vec2d(0, -10);
cut.p1 = Vec2d(0, 10);
bool ok = SketchEngine::trim_entity(e, {cut}, Vec2d(5, 0));
REQUIRE(ok);
REQUIRE(e.type == SketchEntity::Type::Arc);
REQUIRE_THAT(e.radius, WithinAbs(5.0, 1e-9));
REQUIRE_THAT(e.end_angle - e.start_angle, WithinAbs(M_PI, 1e-9));
// Midpoint of the kept arc must point left (away from the pick).
double mid = 0.5 * (e.start_angle + e.end_angle);
REQUIRE_THAT(5 * std::cos(mid), WithinAbs(-5.0, 1e-9));
REQUIRE_THAT(5 * std::sin(mid), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Extend arc forward (end) to a crossing", "[SketchEdit]")
{
// Quarter arc (5,0)->(0,5); cutter crosses the circle at (-5,0). Picking
// near the end grows the sweep ccw to pi.
SketchEntity e;
e.type = SketchEntity::Type::Arc;
e.center = Vec2d(0, 0);
e.radius = 5;
e.start_angle = 0.0;
e.end_angle = M_PI / 2.0;
SketchEntity cut;
cut.type = SketchEntity::Type::Line;
cut.p0 = Vec2d(-10, 0);
cut.p1 = Vec2d(0, 0);
bool ok = SketchEngine::extend_entity(e, {cut}, Vec2d(0, 5));
REQUIRE(ok);
REQUIRE(e.type == SketchEntity::Type::Arc);
REQUIRE_THAT(e.start_angle, WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.end_angle, WithinAbs(M_PI, 1e-9));
}
TEST_CASE("Extend arc backward (start) to a crossing", "[SketchEdit]")
{
// Quarter arc (0,5)->(-5,0); cutter crosses at (5,0). Picking near the
// start grows the sweep cw to start_angle 0.
SketchEntity e;
e.type = SketchEntity::Type::Arc;
e.center = Vec2d(0, 0);
e.radius = 5;
e.start_angle = M_PI / 2.0;
e.end_angle = M_PI;
SketchEntity cut;
cut.type = SketchEntity::Type::Line;
cut.p0 = Vec2d(10, 0);
cut.p1 = Vec2d(0, 0);
bool ok = SketchEngine::extend_entity(e, {cut}, Vec2d(0, 5));
REQUIRE(ok);
REQUIRE_THAT(e.start_angle, WithinAbs(0.0, 1e-9));
REQUIRE_THAT(e.end_angle, WithinAbs(M_PI, 1e-9));
}
TEST_CASE("Extend circle returns false (closed)", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Circle;
e.center = Vec2d(0, 0);
e.p0 = Vec2d(5, 0);
e.radius = 5;
SketchEntity cut;
cut.type = SketchEntity::Type::Line;
cut.p0 = Vec2d(0, -10);
cut.p1 = Vec2d(0, 10);
REQUIRE_FALSE(SketchEngine::extend_entity(e, {cut}, Vec2d(5, 0)));
}
TEST_CASE("Trim arc with no crossing returns false", "[SketchEdit]")
{
SketchEntity e;
e.type = SketchEntity::Type::Arc;
e.center = Vec2d(0, 0);
e.radius = 5;
e.start_angle = 0.0;
e.end_angle = M_PI / 2.0;
SketchEntity cut; // far away, never reaches the r=5 circle
cut.type = SketchEntity::Type::Line;
cut.p0 = Vec2d(20, -5);
cut.p1 = Vec2d(20, 5);
REQUIRE_FALSE(SketchEngine::trim_entity(e, {cut}, Vec2d(5 * std::cos(M_PI/4), 5 * std::sin(M_PI/4))));
}