mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
Mate connectors: derive a face-only frame's X from the face, not from world
datum_frame took a FaceAndDirection frame's Z from the face normal, which follows the body, but its X from coordsys_x_hint, a world constant, whenever no explicit edge reference was set. Spinning a body about its own face normal therefore left the frame bit-identical: the connector could not encode that rotation at all, so Fastened and Slider mates claimed to fix an orientation the frame could not see. X now comes from the face's own first usable edge, which rotates with the body. The hint survives only as a last resort, for faces that offer no in-plane direction — a full circular edge has coincident endpoints, and a seam projects to nothing in-plane. The new test spins a box 90 degrees about its top-face normal and asserts the frame's X turned with it. Reverting just the X_tent derivation and rerunning makes it fail with "1.0 is within 0.000001 of 0.0" — cos(angle) between the before and after X is exactly 1, i.e. the frame did not move — and that is the only failure in 2019 assertions, so the test discriminates this defect and nothing else. Note for anyone replaying an older document: a face-only connector's frame can now differ from what that recipe produced before, so a mate built on one may place its body differently. Nothing in the suite or the golden v3 fixture changed, but the semantics did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f13f2876f6
commit
1726e93760
@@ -1681,8 +1681,29 @@ CadDocument::DatumCoordSys CadDocument::datum_frame(const std::vector<CadBody>&
|
||||
if (fc.IsNull()) { ds.error = "face not found"; break; }
|
||||
ds.origin = GeometryEngine::face_centroid_world(fc);
|
||||
Vec3d Z = GeometryEngine::face_normal_world(fc);
|
||||
// Tentative X: edge direction if available, else the hint or a fallback.
|
||||
Vec3d X_tent = have_edge ? edge_dir : f.coordsys_x_hint;
|
||||
// Tentative X: an explicit edge reference wins. Failing that, derive X from the
|
||||
// face's OWN first usable edge, so the frame rotates with the body. Reading it from
|
||||
// coordsys_x_hint — a world constant — meant a face-only connector could not encode
|
||||
// spin about its own normal: Z followed the body, X did not, so Fastened and Slider
|
||||
// claimed to fix an orientation the frame could not see. The hint survives only as a
|
||||
// last resort, for faces that offer no usable direction (a full circular edge has
|
||||
// coincident endpoints, and a cylinder's seam projects to nothing in-plane).
|
||||
Vec3d X_tent = Vec3d::Zero();
|
||||
if (have_edge) {
|
||||
X_tent = edge_dir;
|
||||
} else {
|
||||
for (const TopoDS_Edge& fe : GeometryEngine::edges_of_face(fc)) {
|
||||
auto pts = GeometryEngine::sample_edge_world(fe);
|
||||
if (pts.size() < 2) continue;
|
||||
Vec3d d = pts.back() - pts.front();
|
||||
if (d.squaredNorm() < 1e-18) continue; // closed edge: endpoints coincide
|
||||
Vec3d in_plane = d - Z * Z.dot(d); // drop any out-of-plane component
|
||||
if (in_plane.squaredNorm() < 1e-18) continue;
|
||||
X_tent = in_plane;
|
||||
break;
|
||||
}
|
||||
if (X_tent.squaredNorm() < 1e-18) X_tent = f.coordsys_x_hint;
|
||||
}
|
||||
if (X_tent.squaredNorm() < 1e-18) { ds.error = "zero-length direction"; break; }
|
||||
X_tent.normalize();
|
||||
// Gram-Schmidt: ensure orthonormal, right-handed frame.
|
||||
|
||||
@@ -2544,6 +2544,61 @@ TEST_CASE("datum coordinate system: non-perpendicular inputs produce orthonormal
|
||||
CHECK_THAT(Z.z(), WithinAbs((X.cross(Y)).z(), 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("datum coordinate system: a face-only frame rotates with its body", "[CadDocument]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
// The bug this pins down: Z came from the face normal (body-following) but X came from
|
||||
// coordsys_x_hint, a WORLD constant. Spinning the body about its own face normal left the
|
||||
// frame identical, so a face-only connector could not encode that rotation at all — and a
|
||||
// Fastened or Slider mate built on it claimed to fix an orientation it could not see.
|
||||
// A rectangular top face is used deliberately: its edges give an unambiguous in-plane
|
||||
// direction, so "did the frame follow the body" is answerable to the degree.
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 40, 20, 10, "Box");
|
||||
doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude");
|
||||
REQUIRE(doc.recompute());
|
||||
|
||||
int n_faces = GeometryEngine::face_count(doc.bodies[0].shape);
|
||||
int top_face = -1;
|
||||
for (int i = 0; i < n_faces; ++i) {
|
||||
Vec3d fn = GeometryEngine::face_normal_world(GeometryEngine::face_by_index(doc.bodies[0].shape, i));
|
||||
if (fn.z() > 0.9) { top_face = i; break; }
|
||||
}
|
||||
REQUIRE(top_face >= 0);
|
||||
|
||||
int cs = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 0), "CS");
|
||||
doc.features[cs].coordsys_type = CoordSysType::FaceAndDirection;
|
||||
doc.features[cs].coordsys_body = 0;
|
||||
doc.features[cs].coordsys_face = top_face;
|
||||
doc.features[cs].coordsys_edge = -1; // face only: no explicit direction edge
|
||||
REQUIRE(doc.recompute());
|
||||
|
||||
auto before = doc.resolve_datum_coordsys();
|
||||
REQUIRE(before.size() == 1);
|
||||
REQUIRE(before[0].error.empty());
|
||||
const Vec3d X0 = before[0].x;
|
||||
const Vec3d Z0 = before[0].x.cross(before[0].y);
|
||||
|
||||
// Spin the body 90 degrees about its own face normal (world Z here).
|
||||
doc.add_transform(0, Vec3d(0, 0, 0), Vec3d(0, 0, 1), Vec3d(0, 0, 0), 90.0, false, "Spin");
|
||||
REQUIRE(doc.recompute());
|
||||
|
||||
auto after = doc.resolve_datum_coordsys();
|
||||
REQUIRE(after.size() == 1);
|
||||
REQUIRE(after[0].error.empty());
|
||||
const Vec3d X1 = after[0].x;
|
||||
|
||||
// The normal is unchanged by a spin about itself — that is exactly why the old code could
|
||||
// not detect the rotation.
|
||||
const Vec3d Z1 = after[0].x.cross(after[0].y);
|
||||
CHECK_THAT(std::abs(Z0.dot(Z1)), WithinAbs(1.0, 1e-6));
|
||||
|
||||
// X must have turned with the body. Before the fix X0 == X1 and this failed.
|
||||
const double cos_turn = std::clamp(X0.dot(X1), -1.0, 1.0);
|
||||
CHECK_THAT(std::abs(cos_turn), WithinAbs(0.0, 1e-6)); // 90 degrees apart
|
||||
}
|
||||
|
||||
TEST_CASE("datum coordinate system: point_world gives world axes", "[CadDocument]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
Reference in New Issue
Block a user