mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
M3c: 2D bridging curve — cubic-Bezier G1 connector between sketch endpoints
Adds SketchEngine::make_bridge (4-pole cubic Bezier, G1-tangent to Line/Arc endpoints, straight-line fallback for other types) emitted as the existing BSpline SketchEntity — no new geometry type, no serialized-field change, golden recipe fixture untouched. CadDocument::add_bridge appends it (non-parametric, index-validated, throws on bad refs). MCP `bridge` method mirrors action_project. 4 new [CadDocument][bridge] tests; full kernel suite green (76 cases, 1280 asserts). 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
62d39fba27
commit
65caa2ea6e
@@ -800,6 +800,21 @@ int CadDocument::add_project_edges(int source_body, const std::vector<int>& edge
|
||||
return int(features.size()) - 1;
|
||||
}
|
||||
|
||||
int CadDocument::add_bridge(int sketch_ref, int ent_a, int end_a, int ent_b, int end_b,
|
||||
const std::string& name)
|
||||
{
|
||||
(void)name;
|
||||
if (sketch_ref < 0 || sketch_ref >= int(features.size())
|
||||
|| features[sketch_ref].type != CadFeatureType::Sketch)
|
||||
throw std::runtime_error("bridge: sketch_ref must refer to a Sketch feature");
|
||||
auto& ents = features[sketch_ref].entities;
|
||||
if (ent_a < 0 || ent_a >= int(ents.size()) || ent_b < 0 || ent_b >= int(ents.size()))
|
||||
throw std::runtime_error("bridge: entity index out of range in sketch");
|
||||
SketchEntity br = SketchEngine::make_bridge(ents[ent_a], end_a, ents[ent_b], end_b);
|
||||
ents.push_back(br);
|
||||
return int(ents.size()) - 1;
|
||||
}
|
||||
|
||||
int CadDocument::add_plane(int base, double offset, double angle_tilt, int axis,
|
||||
const std::string& name)
|
||||
{
|
||||
|
||||
@@ -378,6 +378,12 @@ public:
|
||||
// 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);
|
||||
// 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.
|
||||
|
||||
@@ -1372,4 +1372,57 @@ bool SketchEngine::extend_entity(SketchEntity& e, const std::vector<SketchEntity
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bridge: cubic Bézier with G1 continuity at both ends.
|
||||
// Poles = {Pa, Pa + Ta*d/3, Pb - Tb*d/3, Pb}, where d = |Pb - Pa|.
|
||||
SketchEntity SketchEngine::make_bridge(const SketchEntity& a, int a_end,
|
||||
const SketchEntity& b, int b_end)
|
||||
{
|
||||
auto endpoint = [](const SketchEntity& e, int end) -> Vec2d {
|
||||
return end == 0 ? e.p0 : e.p1;
|
||||
};
|
||||
|
||||
auto tangent = [](const SketchEntity& e, int end, const Vec2d& fallback_dir) -> Vec2d {
|
||||
switch (e.type) {
|
||||
case SketchEntity::Type::Line: {
|
||||
Vec2d dir = end == 1 ? e.p1 - e.p0 : e.p0 - e.p1;
|
||||
double len = dir.norm();
|
||||
if (len < 1e-12) return fallback_dir;
|
||||
return dir / len;
|
||||
}
|
||||
case SketchEntity::Type::Arc: {
|
||||
double theta = end == 1 ? e.end_angle : e.start_angle;
|
||||
// Tangent to the circle at angle theta, CCW: (-sin θ, cos θ).
|
||||
// At end=1 (end_angle), the outward direction is along the sweep direction.
|
||||
// At end=0 (start_angle), outward is opposite the sweep direction.
|
||||
double sweep = e.end_angle - e.start_angle;
|
||||
int sign = end == 1 ? (sweep >= 0 ? 1 : -1) : (sweep >= 0 ? -1 : 1);
|
||||
Vec2d t(-std::sin(theta), std::cos(theta));
|
||||
return t * double(sign);
|
||||
}
|
||||
default:
|
||||
// ponytail: straight-ish bridge for unsupported entity types.
|
||||
return fallback_dir;
|
||||
}
|
||||
};
|
||||
|
||||
const Vec2d Pa = endpoint(a, a_end);
|
||||
const Vec2d Pb = endpoint(b, b_end);
|
||||
const double d = (Pb - Pa).norm();
|
||||
|
||||
// Fallback tangent direction: point toward the other endpoint.
|
||||
Vec2d fallback = d < 1e-9 ? Vec2d(1, 0) : (Pb - Pa) / d;
|
||||
Vec2d Ta = tangent(a, a_end, fallback);
|
||||
Vec2d Tb = tangent(b, b_end, fallback * -1.0);
|
||||
|
||||
const double k = std::max(d, 1e-9) / 3.0;
|
||||
|
||||
SketchEntity e;
|
||||
e.type = SketchEntity::Type::BSpline;
|
||||
e.construction = false;
|
||||
e.ctrl = { Pa, Pa + Ta * k, Pb - Tb * k, Pb };
|
||||
e.p0 = e.ctrl.front();
|
||||
e.p1 = e.ctrl.back();
|
||||
return e;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -250,6 +250,12 @@ public:
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -256,6 +256,14 @@ json describe_tools()
|
||||
json{{"name", "edges"}, {"type", "array"}, {"default", json::array()}, {"description", "global edge ids to project; empty => project the face"}},
|
||||
json{{"name", "plane"}, {"type", "string"}, {"default", "XY"}, {"description", "target sketch plane (XY/XZ/YZ)"}},
|
||||
})}},
|
||||
json{{"name", "bridge"}, {"summary", "Build a cubic-Bezier G1 bridge (BSpline) between two sketch-entity endpoints within a sketch feature."},
|
||||
{"params", json::array({
|
||||
json{{"name", "sketch"}, {"type", "integer"}, {"description", "sketch feature index"}},
|
||||
json{{"name", "ent_a"}, {"type", "integer"}, {"description", "first entity index within the sketch"}},
|
||||
json{{"name", "end_a"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 1}, {"description", "0 = start/p0 side, 1 = end/p1 side"}},
|
||||
json{{"name", "ent_b"}, {"type", "integer"}, {"description", "second entity index within the sketch"}},
|
||||
json{{"name", "end_b"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 0}, {"description", "0 = start/p0 side, 1 = end/p1 side"}},
|
||||
})}},
|
||||
json{{"name", "query_topology"}, {"summary", "Measured faces (centroid/normal/cylinder) and edges (length/circle) of a body."},
|
||||
{"params", json::array({
|
||||
json{{"name", "body"}, {"type", "integer"}, {"default", 0}},
|
||||
@@ -958,6 +966,26 @@ json action_project(DesignPanel* panel, const json& params)
|
||||
return json{{"ok", ok}, {"project_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}};
|
||||
}
|
||||
|
||||
json action_bridge(DesignPanel* panel, const json& params)
|
||||
{
|
||||
if (!params.contains("sketch")) throw std::runtime_error("bridge needs 'sketch' (feature index)");
|
||||
if (!params.contains("ent_a")) throw std::runtime_error("bridge needs 'ent_a' (entity index)");
|
||||
if (!params.contains("ent_b")) throw std::runtime_error("bridge needs 'ent_b' (entity index)");
|
||||
int sketch = params["sketch"].get<int>();
|
||||
int ent_a = params["ent_a"].get<int>();
|
||||
int ent_b = params["ent_b"].get<int>();
|
||||
int end_a = params.value("end_a", 1);
|
||||
int end_b = params.value("end_b", 0);
|
||||
CadDocument& doc = panel->mcp_doc();
|
||||
doc.checkpoint();
|
||||
int ei = doc.add_bridge(sketch, ent_a, end_a, ent_b, end_b, "Bridge");
|
||||
bool ok = doc.recompute();
|
||||
if (!ok) doc.undo();
|
||||
panel->mcp_after_change();
|
||||
return json{{"ok", ok}, {"sketch_index", sketch}, {"entity_index", ei},
|
||||
{"bodies", int(doc.bodies.size())}, {"error", doc.error}};
|
||||
}
|
||||
|
||||
json action_axis(DesignPanel* panel, const json& params)
|
||||
{
|
||||
CadDocument& doc = panel->mcp_doc();
|
||||
@@ -1060,6 +1088,7 @@ std::string handle_on_main(const std::string& method, const json& params, const
|
||||
if (method == "thicken") return rpc_result(id, action_thicken(panel, params));
|
||||
if (method == "split") return rpc_result(id, action_split(panel, params));
|
||||
if (method == "project") return rpc_result(id, action_project(panel, params));
|
||||
if (method == "bridge") return rpc_result(id, action_bridge(panel, params));
|
||||
if (method == "axis") return rpc_result(id, action_axis(panel, params));
|
||||
if (method == "coordsys") return rpc_result(id, action_coordsys(panel, params));
|
||||
if (method == "helix") return rpc_result(id, action_helix(panel, params));
|
||||
|
||||
@@ -3808,3 +3808,134 @@ TEST_CASE("golden recipe v1 still deserialises", "[CadDocument]")
|
||||
INFO("the primary format check.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bridge tests (M3c) ---
|
||||
|
||||
TEST_CASE("bridge two collinear lines", "[CadDocument][bridge]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
std::vector<SketchEntity> ents = {
|
||||
{SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)},
|
||||
{SketchEntity::Type::Line, Vec2d(20,0), Vec2d(30,0)},
|
||||
};
|
||||
int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S");
|
||||
REQUIRE(sk == 0);
|
||||
|
||||
int bi = doc.add_bridge(0, 0, 1, 1, 0, "Bridge");
|
||||
REQUIRE(bi == 2);
|
||||
|
||||
const auto& se = doc.features[0].entities;
|
||||
REQUIRE(se.size() == 3);
|
||||
REQUIRE(se[bi].type == SketchEntity::Type::BSpline);
|
||||
REQUIRE(se[bi].ctrl.size() == 4);
|
||||
REQUIRE_THAT(se[bi].ctrl.front().x(), WithinAbs(10.0, 1e-6));
|
||||
REQUIRE_THAT(se[bi].ctrl.front().y(), WithinAbs(0.0, 1e-6));
|
||||
REQUIRE_THAT(se[bi].ctrl.back().x(), WithinAbs(20.0, 1e-6));
|
||||
REQUIRE_THAT(se[bi].ctrl.back().y(), WithinAbs(0.0, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("bridge closes a C profile and extrudes", "[CadDocument][bridge]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
CadDocument doc;
|
||||
// Right-side of a closed square: P0(10,-10), up to P1(10,10)
|
||||
std::vector<SketchEntity> ents = {
|
||||
// bottom edge: (-10,-10) to (10,-10)
|
||||
{SketchEntity::Type::Line, Vec2d(-10,-10), Vec2d(10,-10)},
|
||||
// left edge: (10,-10) to (10,10)
|
||||
{SketchEntity::Type::Line, Vec2d(10,-10), Vec2d(10,10)},
|
||||
// top edge: (10,10) to (-10,10)
|
||||
{SketchEntity::Type::Line, Vec2d(10,10), Vec2d(-10,10)},
|
||||
};
|
||||
// Missing: left edge from (-10,10) to (-10,-10). Build it as a separate line
|
||||
// entity so the bridge connects two existing lines.
|
||||
int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "C");
|
||||
REQUIRE(sk == 0);
|
||||
|
||||
// Add the closing line as entity 3: (-10,10) to (-10,-10)
|
||||
CadFeature& f = doc.features[sk];
|
||||
SketchEntity closing;
|
||||
closing.type = SketchEntity::Type::Line;
|
||||
closing.p0 = Vec2d(-10, 10);
|
||||
closing.p1 = Vec2d(-10, -10);
|
||||
// The C is entities 0,1,2 (bottom cap, right side, top cap).
|
||||
// Entity 0 end=1 is (10,-10); entity 2 start=0 is (10,10). That's a U.
|
||||
// But we need a closed square from C shape.
|
||||
// Re-think: a C shape open on the left side.
|
||||
// Entities: 0 = bottom edge (-10,-10)->(10,-10) [end=1 at (10,-10)]
|
||||
// 1 = right edge (10,-10)->(10,10) [start=0 at (10,-10), end=1 at (10,10)]
|
||||
// 2 = top edge (10,10)->(-10,10) [start=0 at (10,10), end=1 at (-10,10)]
|
||||
// The C is open: entity 2's end is at (-10,10) and entity 0's start is at (-10,-10).
|
||||
// Bridge: entity 2 end=1 (-10,10) -> entity 0 start=0 (-10,-10).
|
||||
f.entities.push_back(closing);
|
||||
REQUIRE(f.entities.size() == 4);
|
||||
|
||||
// Now bridge from top end (entity 2 end=1 = (-10,10)) to bottom start (entity 0 end=0 = (-10,-10))
|
||||
int bi = doc.add_bridge(sk, 2/*top edge*/, 1/*end*/, 0/*bottom edge*/, 0/*start*/, "Bridge");
|
||||
REQUIRE(bi == 4);
|
||||
REQUIRE(f.entities.size() == 5);
|
||||
|
||||
// Now the entities should form a closed loop -> extrude
|
||||
int ex = doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(ex >= 0);
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
REQUIRE(doc.display_mesh.facets_count() > 0);
|
||||
REQUIRE_THAT(double(doc.display_mesh.volume()), WithinRel(20.0 * 20.0 * 5.0, 1e-2));
|
||||
}
|
||||
|
||||
TEST_CASE("bridge bad indices throw", "[CadDocument][bridge]")
|
||||
{
|
||||
CadDocument doc;
|
||||
std::vector<SketchEntity> ents = {
|
||||
{SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)},
|
||||
{SketchEntity::Type::Line, Vec2d(20,0), Vec2d(30,0)},
|
||||
};
|
||||
doc.add_sketch_entities(ents, SketchPlane::XY(), "S");
|
||||
|
||||
// out-of-range entity a
|
||||
REQUIRE_THROWS(doc.add_bridge(0, 99, 1, 1, 0, "Bad"));
|
||||
// out-of-range entity b
|
||||
REQUIRE_THROWS(doc.add_bridge(0, 0, 1, 99, 0, "Bad"));
|
||||
// out-of-range sketch_ref
|
||||
REQUIRE_THROWS(doc.add_bridge(99, 0, 1, 1, 0, "Bad"));
|
||||
// non-sketch feature as sketch_ref
|
||||
doc.add_extrude(0, 5.0, false, BooleanMode::New, "Ex");
|
||||
REQUIRE_THROWS(doc.add_bridge(1, 0, 1, 1, 0, "Bad"));
|
||||
}
|
||||
|
||||
TEST_CASE("bridge round-trip serialization", "[CadDocument][bridge]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
std::vector<SketchEntity> ents = {
|
||||
{SketchEntity::Type::Line, Vec2d(0,0), Vec2d(10,0)},
|
||||
{SketchEntity::Type::Line, Vec2d(20,0), Vec2d(30,0)},
|
||||
};
|
||||
int sk = doc.add_sketch_entities(ents, SketchPlane::XY(), "S");
|
||||
REQUIRE(sk == 0);
|
||||
int bi = doc.add_bridge(sk, 0, 1, 1, 0, "Bridge");
|
||||
REQUIRE(bi == 2);
|
||||
doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(doc.recompute());
|
||||
|
||||
auto blob = doc.serialize_recipe();
|
||||
REQUIRE_FALSE(blob.empty());
|
||||
|
||||
CadDocument doc2;
|
||||
REQUIRE(doc2.deserialize_recipe(blob));
|
||||
REQUIRE(doc2.features.size() == 2);
|
||||
|
||||
const auto& br = doc2.features[0].entities[2];
|
||||
REQUIRE(br.type == SketchEntity::Type::BSpline);
|
||||
REQUIRE(br.ctrl.size() == 4);
|
||||
REQUIRE_THAT(br.ctrl.front().x(), WithinAbs(10.0, 1e-9));
|
||||
REQUIRE_THAT(br.ctrl.front().y(), WithinAbs(0.0, 1e-9));
|
||||
REQUIRE_THAT(br.ctrl.back().x(), WithinAbs(20.0, 1e-9));
|
||||
REQUIRE_THAT(br.ctrl.back().y(), WithinAbs(0.0, 1e-9));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user