mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Orca-Cad: port SnapOrca Design (parametric CAD tab) onto mainline OrcaSlicer
Grafts the sketch-first CAD environment from snaporca-cad onto the mainline OrcaSlicer/OrcaSlicer base (vs snaporca's Snapmaker/OrcaSlicer base): - 133 new files: CadDocument/SketchEngine/GeometryEngine/SketchConstraints/ SketchSolver/SketchInference/ThreadStandards + vendored libslvs solver; DesignPanel/DesignCanvas/DesignSketchTool/SketchInlineEditor GUI; GLGizmo Primitive/Sketch; 75 design icons; Catch2 tests. - Integration hooks ported to mainline's diverged versions: Design tab in MainFrame, embedded design viewport + sketch overlay + per-canvas chrome suppression in GLCanvas3D/PartPlate, gizmo registration, Plater accessors, CMake wiring (libslvs subdir, CAD sources, OCCT ModelingAlgorithms=ON). Structural integration complete; build verification pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
449a4cf9fc
commit
0f4060c0a9
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
||||
#include <catch2/catch.hpp>
|
||||
#include "libslic3r/SketchConstraints.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Coincident with anchor", "[SketchConstraints]")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(0, 0);
|
||||
int b = sc.add_point(5, 5);
|
||||
sc.fix_point(a);
|
||||
sc.coincident(a, b);
|
||||
REQUIRE(sc.solve());
|
||||
Vec2d pb = sc.get_point(b);
|
||||
REQUIRE_THAT(pb.x(), Catch::Matchers::WithinAbs(0.0, 1e-4));
|
||||
REQUIRE_THAT(pb.y(), Catch::Matchers::WithinAbs(0.0, 1e-4));
|
||||
}
|
||||
|
||||
TEST_CASE("Horizontal + distance", "[SketchConstraints]")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(0, 0);
|
||||
int b = sc.add_point(5, 3);
|
||||
sc.fix_point(a);
|
||||
sc.horizontal(a, b);
|
||||
sc.distance(a, b, 10);
|
||||
REQUIRE(sc.solve());
|
||||
Vec2d pb = sc.get_point(b);
|
||||
REQUIRE_THAT(pb.y(), Catch::Matchers::WithinAbs(0.0, 1e-3));
|
||||
REQUIRE_THAT(std::abs(pb.x()), Catch::Matchers::WithinAbs(10.0, 1e-3));
|
||||
}
|
||||
|
||||
TEST_CASE("Rectangle", "[SketchConstraints]")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int p0 = sc.add_point(0, 0);
|
||||
int p1 = sc.add_point(8, 1);
|
||||
int p2 = sc.add_point(9, 5);
|
||||
int p3 = sc.add_point(-1, 4);
|
||||
sc.fix_point(p0);
|
||||
sc.lock_x(p0, 0);
|
||||
sc.lock_y(p0, 0);
|
||||
sc.horizontal(p0, p1);
|
||||
sc.vertical(p1, p2);
|
||||
sc.horizontal(p2, p3);
|
||||
sc.vertical(p3, p0);
|
||||
sc.distance(p0, p1, 10);
|
||||
sc.distance(p1, p2, 6);
|
||||
REQUIRE(sc.solve());
|
||||
Vec2d pp1 = sc.get_point(p1);
|
||||
Vec2d pp2 = sc.get_point(p2);
|
||||
Vec2d pp3 = sc.get_point(p3);
|
||||
REQUIRE_THAT(pp1.x(), Catch::Matchers::WithinAbs(10.0, 1e-3));
|
||||
REQUIRE_THAT(pp1.y(), Catch::Matchers::WithinAbs(0.0, 1e-3));
|
||||
REQUIRE_THAT(pp2.x(), Catch::Matchers::WithinAbs(10.0, 1e-3));
|
||||
REQUIRE_THAT(pp2.y(), Catch::Matchers::WithinAbs(6.0, 1e-3));
|
||||
REQUIRE_THAT(pp3.x(), Catch::Matchers::WithinAbs(0.0, 1e-3));
|
||||
REQUIRE_THAT(pp3.y(), Catch::Matchers::WithinAbs(6.0, 1e-3));
|
||||
}
|
||||
|
||||
TEST_CASE("residual_norm after each solve", "[SketchConstraints]")
|
||||
{
|
||||
SECTION("coincident case")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(0, 0);
|
||||
int b = sc.add_point(5, 5);
|
||||
sc.fix_point(a);
|
||||
sc.coincident(a, b);
|
||||
REQUIRE(sc.solve());
|
||||
REQUIRE(sc.residual_norm() < 1e-5);
|
||||
}
|
||||
SECTION("horizontal+distance case")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(0, 0);
|
||||
int b = sc.add_point(5, 3);
|
||||
sc.fix_point(a);
|
||||
sc.horizontal(a, b);
|
||||
sc.distance(a, b, 10);
|
||||
REQUIRE(sc.solve());
|
||||
REQUIRE(sc.residual_norm() < 1e-5);
|
||||
}
|
||||
SECTION("rectangle case")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int p0 = sc.add_point(0, 0);
|
||||
int p1 = sc.add_point(8, 1);
|
||||
int p2 = sc.add_point(9, 5);
|
||||
int p3 = sc.add_point(-1, 4);
|
||||
sc.fix_point(p0);
|
||||
sc.lock_x(p0, 0);
|
||||
sc.lock_y(p0, 0);
|
||||
sc.horizontal(p0, p1);
|
||||
sc.vertical(p1, p2);
|
||||
sc.horizontal(p2, p3);
|
||||
sc.vertical(p3, p0);
|
||||
sc.distance(p0, p1, 10);
|
||||
sc.distance(p1, p2, 6);
|
||||
REQUIRE(sc.solve());
|
||||
REQUIRE(sc.residual_norm() < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("midpoint", "[SketchConstraints]")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(0, 0);
|
||||
int b = sc.add_point(10, 0);
|
||||
int m = sc.add_point(3, 7);
|
||||
sc.fix_point(a);
|
||||
sc.fix_point(b);
|
||||
sc.midpoint(m, a, b);
|
||||
REQUIRE(sc.solve());
|
||||
Vec2d pm = sc.get_point(m);
|
||||
REQUIRE_THAT(pm.x(), Catch::Matchers::WithinAbs(5.0, 1e-3));
|
||||
REQUIRE_THAT(pm.y(), Catch::Matchers::WithinAbs(0.0, 1e-3));
|
||||
}
|
||||
|
||||
TEST_CASE("symmetric across Y axis", "[SketchConstraints]")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(2, 3);
|
||||
int b = sc.add_point(-1, 1);
|
||||
int c = sc.add_point(0, 0);
|
||||
int d = sc.add_point(0, 1);
|
||||
sc.fix_point(a);
|
||||
sc.fix_point(c);
|
||||
sc.fix_point(d);
|
||||
sc.symmetric(a, b, c, d);
|
||||
REQUIRE(sc.solve());
|
||||
Vec2d pb = sc.get_point(b);
|
||||
REQUIRE_THAT(pb.x(), Catch::Matchers::WithinAbs(-2.0, 1e-3));
|
||||
REQUIRE_THAT(pb.y(), Catch::Matchers::WithinAbs(3.0, 1e-3));
|
||||
}
|
||||
|
||||
TEST_CASE("angle 90 degrees", "[SketchConstraints]")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(0, 0);
|
||||
int b = sc.add_point(1, 0);
|
||||
int c = sc.add_point(0, 0);
|
||||
int d = sc.add_point(1, 1);
|
||||
sc.fix_point(a);
|
||||
sc.fix_point(b);
|
||||
sc.fix_point(c);
|
||||
sc.angle(a, b, c, d, M_PI / 2);
|
||||
REQUIRE(sc.solve());
|
||||
Vec2d pd = sc.get_point(d);
|
||||
Vec2d pc = sc.get_point(c);
|
||||
REQUIRE_THAT(pd.x() - pc.x(), Catch::Matchers::WithinAbs(0.0, 1e-3));
|
||||
REQUIRE(pd.y() > pc.y());
|
||||
}
|
||||
|
||||
TEST_CASE("point-line distance", "[SketchConstraints]")
|
||||
{
|
||||
SketchConstraints sc;
|
||||
int a = sc.add_point(0, 0);
|
||||
int b = sc.add_point(10, 0);
|
||||
int p = sc.add_point(3, 1);
|
||||
sc.fix_point(a);
|
||||
sc.fix_point(b);
|
||||
sc.lock_x(p, 3.0);
|
||||
sc.point_line_distance(p, a, b, 5.0);
|
||||
REQUIRE(sc.solve());
|
||||
Vec2d pp = sc.get_point(p);
|
||||
REQUIRE_THAT(std::abs(pp.y()), Catch::Matchers::WithinAbs(5.0, 1e-3));
|
||||
REQUIRE_THAT(pp.x(), Catch::Matchers::WithinAbs(3.0, 1e-3));
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
#include <catch2/catch.hpp>
|
||||
#include "libslic3r/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))));
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include "libslic3r/SketchImport.hpp"
|
||||
#include "libslic3r/Utils.hpp" // resources_dir
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("svg_to_regions parses a filled path into a region", "[SketchImport]")
|
||||
{
|
||||
// A 10x10 mm filled square. Written to a temp file because nanosvg reads
|
||||
// from disk.
|
||||
const std::string path = "/tmp/snaporca_test_square.svg";
|
||||
{
|
||||
std::ofstream f(path);
|
||||
f << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10mm\" height=\"10mm\" "
|
||||
"viewBox=\"0 0 10 10\">"
|
||||
"<path d=\"M0,0 L10,0 L10,10 L0,10 Z\" fill=\"#000000\"/></svg>";
|
||||
}
|
||||
|
||||
ImportRegions regs = svg_to_regions(path, 1.0);
|
||||
REQUIRE(regs.size() >= 1);
|
||||
// Outer contour present with at least a few vertices.
|
||||
REQUIRE(regs[0].size() >= 1);
|
||||
REQUIRE(regs[0][0].size() >= 4);
|
||||
|
||||
// Centred on the origin: bbox half-extent ~5 mm on each side.
|
||||
double hi = 0.0;
|
||||
for (const auto& region : regs)
|
||||
for (const auto& contour : region)
|
||||
for (const Vec2d& p : contour)
|
||||
hi = std::max(hi, std::max(std::abs(p.x()), std::abs(p.y())));
|
||||
REQUIRE(hi > 3.0); // not collapsed
|
||||
REQUIRE(hi < 8.0); // ~5 mm half-size after centring
|
||||
}
|
||||
|
||||
TEST_CASE("svg_to_regions rejects bad input gracefully", "[SketchImport]")
|
||||
{
|
||||
REQUIRE(svg_to_regions("", 1.0).empty());
|
||||
REQUIRE(svg_to_regions("/tmp/snaporca_does_not_exist.svg", 1.0).empty());
|
||||
REQUIRE(svg_to_regions("/tmp/snaporca_test_square.svg", 0.0).empty()); // scale<=0
|
||||
}
|
||||
|
||||
TEST_CASE("transform_regions moves and scales independently", "[SketchImport]")
|
||||
{
|
||||
ImportRegions r = {{ {Vec2d(-1,-1), Vec2d(1,-1), Vec2d(1,1), Vec2d(-1,1)} }};
|
||||
ImportRegions t = transform_regions(r, Vec2d(10, 20), 2.0, 3.0);
|
||||
REQUIRE(t.size() == 1);
|
||||
REQUIRE(t[0][0].size() == 4);
|
||||
// (-1,-1) -> (-1*2+10, -1*3+20) = (8, 17)
|
||||
REQUIRE_THAT(t[0][0][0].x(), Catch::Matchers::WithinAbs(8.0, 1e-9));
|
||||
REQUIRE_THAT(t[0][0][0].y(), Catch::Matchers::WithinAbs(17.0, 1e-9));
|
||||
// (1,1) -> (1*2+10, 1*3+20) = (12, 23)
|
||||
REQUIRE_THAT(t[0][0][2].x(), Catch::Matchers::WithinAbs(12.0, 1e-9));
|
||||
REQUIRE_THAT(t[0][0][2].y(), Catch::Matchers::WithinAbs(23.0, 1e-9));
|
||||
// identity is a no-op
|
||||
ImportRegions id = transform_regions(r, Vec2d(0,0), 1.0, 1.0);
|
||||
REQUIRE_THAT(id[0][0][1].x(), Catch::Matchers::WithinAbs(1.0, 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("text_to_regions vectorizes glyphs with counters", "[SketchImport]")
|
||||
{
|
||||
// Locate the bundled font; resources_dir() may be unset under ctest, so
|
||||
// fall back to a cwd-relative path (tests run from the repo root).
|
||||
std::string font = resources_dir().empty()
|
||||
? std::string("resources/fonts/HarmonyOS_Sans_SC_Regular.ttf")
|
||||
: resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf";
|
||||
{
|
||||
std::ifstream probe(font);
|
||||
if (!probe.good()) {
|
||||
SUCCEED("bundled font not reachable in this environment; covered live on :10");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bad input is rejected without throwing.
|
||||
REQUIRE(text_to_regions("", 10.0, font).empty());
|
||||
REQUIRE(text_to_regions("A", 0.0, font).empty());
|
||||
|
||||
// 'A' has one triangular counter -> a region with an outer + 1 hole.
|
||||
ImportRegions a = text_to_regions("A", 12.0, font);
|
||||
REQUIRE(a.size() >= 1);
|
||||
bool has_hole = false;
|
||||
for (const auto& region : a)
|
||||
if (region.size() >= 2) has_hole = true;
|
||||
REQUIRE(has_hole);
|
||||
|
||||
// Two letters produce more regions than one.
|
||||
ImportRegions ab = text_to_regions("AB", 12.0, font);
|
||||
REQUIRE(ab.size() >= a.size());
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include "libslic3r/SketchInference.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using K = InferenceSnap::Kind;
|
||||
|
||||
static SketchEntity line(Vec2d a, Vec2d b)
|
||||
{
|
||||
SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = a; e.p1 = b; return e;
|
||||
}
|
||||
static SketchEntity circle(Vec2d c, double r)
|
||||
{
|
||||
SketchEntity e; e.type = SketchEntity::Type::Circle; e.center = c; e.p0 = c; e.radius = r; return e;
|
||||
}
|
||||
|
||||
TEST_CASE("inference: cursor near a line endpoint snaps Coincident-able to it", "[inference]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {10, 0}) };
|
||||
auto s = infer_point_snap(ents, {10.3, 0.2}, 1.0);
|
||||
REQUIRE(s.kind == K::Endpoint);
|
||||
CHECK(s.entity == 0);
|
||||
CHECK(s.role == SketchPointRole::P1);
|
||||
CHECK((s.point - Vec2d(10, 0)).norm() == Approx(0.0).margin(1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("inference: endpoint beats midpoint when both are in range", "[inference]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {2, 0}) };
|
||||
// Query equidistant-ish but closer to the endpoint: endpoint tier wins regardless.
|
||||
auto s = infer_point_snap(ents, {1.9, 0.0}, 5.0);
|
||||
CHECK(s.kind == K::Endpoint);
|
||||
CHECK(s.role == SketchPointRole::P1);
|
||||
}
|
||||
|
||||
TEST_CASE("inference: midpoint of a line is detected", "[inference]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {10, 0}) };
|
||||
auto s = infer_point_snap(ents, {5.1, 0.1}, 0.5, /*include_origin=*/false);
|
||||
REQUIRE(s.kind == K::Midpoint);
|
||||
CHECK((s.point - Vec2d(5, 0)).norm() == Approx(0.0).margin(1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("inference: circle centre and rim", "[inference]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { circle({0, 0}, 5.0) };
|
||||
auto c = infer_point_snap(ents, {0.2, 0.1}, 1.0, false);
|
||||
CHECK(c.kind == K::Center);
|
||||
auto r = infer_point_snap(ents, {5.1, 0.0}, 1.0, false);
|
||||
REQUIRE(r.kind == K::OnEdge);
|
||||
CHECK((r.point - Vec2d(5, 0)).norm() == Approx(0.0).margin(1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("inference: origin snap when nothing else is near", "[inference]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({20, 20}, {30, 20}) };
|
||||
auto s = infer_point_snap(ents, {0.1, 0.1}, 1.0);
|
||||
REQUIRE(s.kind == K::Origin);
|
||||
CHECK(s.entity == -1);
|
||||
CHECK((s.point - Vec2d(0, 0)).norm() == Approx(0.0).margin(1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("inference: nothing in range returns None and the raw query", "[inference]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {10, 0}) };
|
||||
auto s = infer_point_snap(ents, {50, 50}, 1.0, /*include_origin=*/false);
|
||||
CHECK(s.kind == K::None);
|
||||
CHECK((s.point - Vec2d(50, 50)).norm() == Approx(0.0).margin(1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("inference: axis inference flags horizontal / vertical segments", "[inference]")
|
||||
{
|
||||
CHECK(infer_axis_constraint({0, 0}, {10, 0.05}).value() == SketchConstraintType::Horizontal);
|
||||
CHECK(infer_axis_constraint({0, 0}, {0.05, 10}).value() == SketchConstraintType::Vertical);
|
||||
CHECK_FALSE(infer_axis_constraint({0, 0}, {10, 10}).has_value()); // 45 deg
|
||||
CHECK_FALSE(infer_axis_constraint({0, 0}, {0, 0}).has_value()); // degenerate
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include "libslic3r/SketchSolver.hpp"
|
||||
#include "libslic3r/SketchEngine.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using CT = SketchConstraintType;
|
||||
using R = SketchPointRole;
|
||||
|
||||
static SketchEntity line(Vec2d a, Vec2d b)
|
||||
{
|
||||
SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = a; e.p1 = b; return e;
|
||||
}
|
||||
static SketchEntity circle(Vec2d c, double r)
|
||||
{
|
||||
SketchEntity e; e.type = SketchEntity::Type::Circle; e.center = c; e.p0 = c; e.radius = r; return e;
|
||||
}
|
||||
static SketchEntityConstraintDef con(CT t, int ea, R ra, int eb, R rb, double v = 0.0)
|
||||
{
|
||||
SketchEntityConstraintDef c; c.type = t; c.ea = ea; c.ra = ra; c.eb = eb; c.rb = rb; c.value = v; return c;
|
||||
}
|
||||
|
||||
TEST_CASE("slvs: distance + horizontal + fix solves a line length", "[slvs]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {5, 1}) };
|
||||
std::vector<SketchEntityConstraintDef> cons = {
|
||||
con(CT::Fix, 0, R::P0, 0, R::P0),
|
||||
con(CT::Horizontal, 0, R::P0, 0, R::P1),
|
||||
con(CT::Distance, 0, R::P0, 0, R::P1, 10.0),
|
||||
};
|
||||
auto res = sketch_solve(ents, cons);
|
||||
REQUIRE(res.ok);
|
||||
CHECK((ents[0].p1 - ents[0].p0).norm() == Approx(10.0).margin(1e-6));
|
||||
CHECK(ents[0].p0.x() == Approx(0.0).margin(1e-6));
|
||||
CHECK(ents[0].p0.y() == Approx(0.0).margin(1e-6));
|
||||
CHECK(ents[0].p1.y() == Approx(0.0).margin(1e-6)); // horizontal
|
||||
}
|
||||
|
||||
TEST_CASE("slvs: coincident joins two line endpoints (loop closes)", "[slvs]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {10, 0}), line({10.3, 0.2}, {10, 10}) };
|
||||
std::vector<SketchEntityConstraintDef> cons = {
|
||||
con(CT::Coincident, 0, R::P1, 1, R::P0),
|
||||
};
|
||||
auto res = sketch_solve(ents, cons);
|
||||
REQUIRE(res.ok);
|
||||
CHECK((ents[0].p1 - ents[1].p0).norm() == Approx(0.0).margin(1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("slvs: parallel + perpendicular on lines", "[slvs]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {10, 1}), line({0, 5}, {10, 5.5}), line({0, 0}, {0.5, 10}) };
|
||||
std::vector<SketchEntityConstraintDef> cons = {
|
||||
con(CT::Fix, 0, R::P0, 0, R::P0),
|
||||
con(CT::Horizontal, 0, R::P0, 0, R::P1),
|
||||
con(CT::Parallel, 0, R::P0, 1, R::P0), // line1 parallel to line0
|
||||
con(CT::Perpendicular, 0, R::P0, 2, R::P0), // line2 perpendicular to line0
|
||||
};
|
||||
auto res = sketch_solve(ents, cons);
|
||||
REQUIRE(res.ok);
|
||||
CHECK(ents[1].p1.y() - ents[1].p0.y() == Approx(0.0).margin(1e-6)); // line1 horizontal
|
||||
CHECK(ents[2].p1.x() - ents[2].p0.x() == Approx(0.0).margin(1e-6)); // line2 vertical
|
||||
}
|
||||
|
||||
TEST_CASE("slvs: circle radius constraint", "[slvs]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { circle({2, 2}, 3.0) };
|
||||
std::vector<SketchEntityConstraintDef> cons = { con(CT::Radius, 0, R::P0, -1, R::P0, 7.0) };
|
||||
auto res = sketch_solve(ents, cons);
|
||||
REQUIRE(res.ok);
|
||||
CHECK(ents[0].radius == Approx(7.0).margin(1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("slvs: degrees of freedom reported", "[slvs]")
|
||||
{
|
||||
// One free line with only a Fix on the start: 4 DoF total minus 2 (fix) = 2 remaining.
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {3, 4}) };
|
||||
std::vector<SketchEntityConstraintDef> cons = { con(CT::Fix, 0, R::P0, 0, R::P0) };
|
||||
auto res = sketch_solve(ents, cons);
|
||||
REQUIRE(res.ok);
|
||||
CHECK(res.dof == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("slvs: drag pulls a point while constraints hold", "[slvs]")
|
||||
{
|
||||
// A vertical line of fixed length 10, P0 pinned at the origin. Dragging P1 toward
|
||||
// (10,0) must keep the length (Distance constraint) but rotate the line so the end
|
||||
// follows the cursor into positive x — the dragged param wins the under-constrained DoF.
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {0, 10}) };
|
||||
std::vector<SketchEntityConstraintDef> cons = {
|
||||
con(CT::Fix, 0, R::P0, 0, R::P0),
|
||||
con(CT::Distance, 0, R::P0, 0, R::P1, 10.0),
|
||||
};
|
||||
ents[0].p1 = Vec2d(10, 0); // user dropped the endpoint here
|
||||
auto res = sketch_solve_drag(ents, cons, 0, R::P1);
|
||||
REQUIRE(res.ok);
|
||||
CHECK((ents[0].p1 - ents[0].p0).norm() == Approx(10.0).margin(1e-6)); // length held
|
||||
CHECK(ents[0].p0.x() == Approx(0.0).margin(1e-6)); // P0 still pinned
|
||||
CHECK(ents[0].p0.y() == Approx(0.0).margin(1e-6));
|
||||
CHECK(ents[0].p1.x() > 1.0); // end followed the drag toward +x (not stuck vertical)
|
||||
}
|
||||
|
||||
TEST_CASE("slvs: over-constrained / inconsistent is detected", "[slvs]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { line({0, 0}, {5, 0}) };
|
||||
std::vector<SketchEntityConstraintDef> cons = {
|
||||
con(CT::Fix, 0, R::P0, 0, R::P0),
|
||||
con(CT::Fix, 0, R::P1, 0, R::P1),
|
||||
con(CT::Distance, 0, R::P0, 0, R::P1, 99.0), // contradicts the pinned endpoints
|
||||
};
|
||||
auto res = sketch_solve(ents, cons);
|
||||
CHECK_FALSE(res.ok); // SLVS_RESULT_INCONSISTENT
|
||||
}
|
||||
Reference in New Issue
Block a user