diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index f4a4d57215..e80b1a220c 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -7064,6 +7064,8 @@ bool SketchObject::insertBSplineKnot(int GeoId, double param, int multiplicity) int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction) { + using std::numbers::pi; + // no need to check input data validity as this is an sketchobject managed operation. Base::StateLocker lock(managedoperation, true); @@ -7096,6 +7098,11 @@ int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction) newVals.reserve(vals.size() + svals.size()); newcVals.reserve(cvals.size() + scvals.size()); + const Base::Vector3d& origin = this->Placement.getValue().getPosition(); + const Base::Rotation& rotation = this->Placement.getValue().getRotation(); + const Base::Vector3d axisH = rotation.multVec(Base::Vector3d::UnitX); + const Base::Vector3d axisV = rotation.multVec(Base::Vector3d::UnitY); + std::map extMap; if (psObj->ExternalGeo.getSize() > 1) { int i = -1; @@ -7185,8 +7192,26 @@ int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction) solverNeedsUpdate = true; } + auto applyGeometryFlipCorrection = [xinv, yinv, origin, axisV, axisH] + (Part::Geometry* geoNew) { + if (!xinv && !yinv) { + return; + } + + if (xinv) { + geoNew->mirror(origin, axisV); + } + if (yinv) { + geoNew->mirror(origin, axisH); + } + }; + for (std::vector::const_iterator it = svals.begin(); it != svals.end(); ++it) { Part::Geometry* geoNew = (*it)->copy(); + if (xinv || yinv) { + // corrections for flipped geometry + applyGeometryFlipCorrection(geoNew); + } generateId(geoNew); if (construction && !geoNew->is()) { GeometryFacade::setConstruction(geoNew, true); @@ -7194,6 +7219,65 @@ int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction) newVals.push_back(geoNew); } + auto applyConstraintFlipCorrection = [xinv, yinv] + (Sketcher::Constraint* newConstr) { + if (!xinv && !yinv) { + return; + } + + // DistanceX, DistanceY + if ((xinv && newConstr->Type == Sketcher::DistanceX) || + (yinv && newConstr->Type == Sketcher::DistanceY)) { + if (newConstr->First == newConstr->Second) { + std::swap(newConstr->FirstPos, newConstr->SecondPos); + } else{ + newConstr->setValue(-newConstr->getValue()); + } + } + + // Angle + if (newConstr->Type == Sketcher::Angle) { + auto normalizeAngle = [](double angleDeg) { + while (angleDeg > pi) angleDeg -= pi * 2.0; + while (angleDeg <= -pi) angleDeg += pi * 2.0; + return angleDeg; + }; + + if (xinv && yinv) { // rotation 180 degrees around normal axis + if (newConstr->First ==-1 || newConstr->Second == -1 + || newConstr->First == -2 || newConstr->Second == -2 + || newConstr->Second == GeoEnum::GeoUndef) { + // angle to horizontal or vertical axis + newConstr->setValue(normalizeAngle(newConstr->getValue() + pi)); + } + else { + // angle between two sketch entities + // do nothing + } + } + else if (xinv) { // rotation 180 degrees around vertical axis + if (newConstr->First == -1 || newConstr->Second == -1 || newConstr->Second == GeoEnum::GeoUndef) { + // angle to horizontal axis + newConstr->setValue(normalizeAngle(pi - newConstr->getValue())); + } + else { + // angle between two sketch entities or angle to vertical axis + newConstr->setValue(normalizeAngle(-newConstr->getValue())); + } + } + else if (yinv) { // rotation 180 degrees around horizontal axis + if (newConstr->First == -2 || newConstr->Second == -2) { + // angle to vertical axis + newConstr->setValue(normalizeAngle(pi - newConstr->getValue())); + } + else { + // angle between two sketch entities or angle to horizontal axis + newConstr->setValue(normalizeAngle(-newConstr->getValue())); + } + } + } + }; + for (std::vector::const_iterator it = scvals.begin(); it != scvals.end(); ++it) { Sketcher::Constraint* newConstr = (*it)->copy(); @@ -7211,6 +7295,11 @@ int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction) if ((*it)->Third < -2 && (*it)->Third != GeoEnum::GeoUndef) newConstr->Third -= (nextextgeoid - 2); + if (xinv || yinv) { + // corrections for flipped constraints + applyConstraintFlipCorrection(newConstr); + } + newcVals.push_back(newConstr); } @@ -7225,6 +7314,62 @@ int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction) // ViewProvider::UpdateData is triggered. Geometry.touch(); + auto makeCorrectedExpressionString = [xinv, yinv] + (const Sketcher::Constraint* constr, const std::string expr) + -> std::string { + if (!xinv && !yinv) { + return expr; + } + + // DistanceX, DistanceY + if ((xinv && constr->Type == Sketcher::DistanceX) || + (yinv && constr->Type == Sketcher::DistanceY)) { + if (constr->First == constr->Second) { + return expr; + } else{ + return "-(" + expr + ")"; + } + } + + // Angle + if (constr->Type == Sketcher::Angle) { + if (xinv && yinv) { // rotation 180 degrees around normal axis + if (constr->First ==-1 || constr->Second == -1 + || constr->First == -2 || constr->Second == -2 + || constr->Second == GeoEnum::GeoUndef) { + // angle to horizontal or vertical axis + return "(" + expr + ") + 180 deg"; + } + else { + // angle between two sketch entities + // do nothing + return expr; + } + } + else if (xinv) { // rotation 180 degrees around vertical axis + if (constr->First == -1 || constr->Second == -1 || constr->Second == GeoEnum::GeoUndef) { + // angle to horizontal axis + return "180 deg - (" + expr + ")"; + } + else { + // angle between two sketch entities or angle to vertical axis + return "-(" + expr + ")"; + } + } + else if (yinv) { // rotation 180 degrees around horizontal axis + if (constr->First == -2 || constr->Second == -2) { + // angle to vertical axis + return "180 deg - (" + expr + ")"; + } + else { + // angle between two sketch entities or angle to horizontal axis + return "-(" + expr + ")"; + } + } + } + return expr; + }; + int sourceid = 0; for (std::vector::const_iterator it = scvals.begin(); it != scvals.end(); ++it, nextcid++, sourceid++) { @@ -7235,19 +7380,22 @@ int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction) App::ObjectIdentifier spath; std::shared_ptr expr; std::string scname = (*it)->Name; + std::string sref; if (App::ExpressionParser::isTokenAnIndentifier(scname)) { spath = App::ObjectIdentifier(psObj->Constraints) << App::ObjectIdentifier::SimpleComponent(scname); - expr = std::shared_ptr(App::Expression::parse( - this, spath.getDocumentObjectName().getString() + spath.toString())); + sref = spath.getDocumentObjectName().getString() + spath.toString(); } else { spath = psObj->Constraints.createPath(sourceid); - expr = std::shared_ptr( - App::Expression::parse(this, - spath.getDocumentObjectName().getString() - + std::string(1, '.') + spath.toString())); + sref = spath.getDocumentObjectName().getString() + + std::string(1, '.') + spath.toString(); } + if (xinv || yinv) { + // corrections for flipped expressions + sref = makeCorrectedExpressionString((*it), sref); + } + expr = std::shared_ptr(App::Expression::parse(this, sref)); setExpression(Constraints.createPath(nextcid), std::move(expr)); } } diff --git a/src/Mod/Sketcher/App/SketchObject.pyi b/src/Mod/Sketcher/App/SketchObject.pyi index 8816f4cfed..84471fc4c8 100644 --- a/src/Mod/Sketcher/App/SketchObject.pyi +++ b/src/Mod/Sketcher/App/SketchObject.pyi @@ -301,6 +301,18 @@ class SketchObject(Part2DObject): """ ... + def setAllowUnaligned(self, state: bool, /) -> None: + """ + Set whether unaligned geometry is allowed in the sketch. + + setAllowUnaligned(state:bool) + + Args: + state: `True` allows unaligned geometry, + `False` enforces aligned geometry. + """ + ... + def carbonCopy(self, objName: str, asConstruction: bool = True, /) -> None: """ Copy another sketch's geometry and constraints into this sketch. diff --git a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp index 4dc119591f..dc13efb624 100644 --- a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp +++ b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp @@ -566,6 +566,18 @@ PyObject* SketchObjectPy::getIndexByName(PyObject* args) const return nullptr; } +PyObject* SketchObjectPy::setAllowUnaligned(PyObject* args) +{ + PyObject* allowObj; + if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &allowObj)) { + return nullptr; + } + bool allow = Base::asBoolean(allowObj); + this->getSketchObjectPtr()->setAllowUnaligned(allow); + + Py_Return; +} + PyObject* SketchObjectPy::carbonCopy(PyObject* args) { char* ObjectName; diff --git a/src/Mod/Sketcher/CMakeLists.txt b/src/Mod/Sketcher/CMakeLists.txt index b546c86c39..eed8b11f1d 100644 --- a/src/Mod/Sketcher/CMakeLists.txt +++ b/src/Mod/Sketcher/CMakeLists.txt @@ -17,6 +17,8 @@ set(Sketcher_TestScripts SketcherTests/TestSketcherSolver.py SketcherTests/TestSketchExpression.py SketcherTests/TestSketchValidateCoincidents.py + SketcherTests/TestSketchCarbonCopyReverseMapping.py + SketcherTests/TestSketchCarbonCopyReverseMapping.FCStd ) if(BUILD_GUI) diff --git a/src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.FCStd b/src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.FCStd new file mode 100644 index 0000000000..258c53b1bb Binary files /dev/null and b/src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.FCStd differ diff --git a/src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.py b/src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.py new file mode 100644 index 0000000000..a9305a389a --- /dev/null +++ b/src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +import FreeCAD +import math, os, unittest + + +class TestSketchCarbonCopyReverseMapping(unittest.TestCase): + def setUp(self): + location = os.path.dirname(os.path.realpath(__file__)) + self.Doc = FreeCAD.openDocument( + os.path.join(location, "TestSketchCarbonCopyReverseMapping.FCStd"), True + ) + + def test_CarbonCopyReverseMapping(self): + r = 10.0 + rad30 = math.radians(30) + cos30 = math.cos(rad30) + sin30 = math.sin(rad30) + + expected_geometries_pattern_001 = { + "Sketch001": { + 0: {"start": FreeCAD.Vector(10, 10, 0), "end": FreeCAD.Vector(20, 10, 0)}, + 1: { + "start": FreeCAD.Vector(10, 10, 0), + "end": FreeCAD.Vector(10 + cos30 * r, 10 + sin30 * r, 0), + }, + 2: { + "start": FreeCAD.Vector(10, 10, 0), + "end": FreeCAD.Vector(10 + sin30 * r, 10 + cos30 * r, 0), + }, + 3: { + "start": FreeCAD.Vector(10, 10, 0), + "end": FreeCAD.Vector(10 - sin30 * r, 10 + cos30 * r, 0), + }, + 4: {"location": FreeCAD.Vector(-10, 10, 0)}, + 5: {"location": FreeCAD.Vector(10, -10, 0)}, + 6: { + "start": FreeCAD.Vector(5, 15, 0), + "end": FreeCAD.Vector(5 + sin30 * r, 15 + cos30 * r, 0), + }, + 7: { + "start": FreeCAD.Vector(15, 15, 0), + "end": FreeCAD.Vector(15 - sin30 * r, 15 + cos30 * r, 0), + }, + }, + "Sketch002": { + 0: {"start": FreeCAD.Vector(-10, 10, 0), "end": FreeCAD.Vector(-20, 10, 0)}, + 1: { + "start": FreeCAD.Vector(-10, 10, 0), + "end": FreeCAD.Vector(-10 - cos30 * r, 10 + sin30 * r, 0), + }, + 2: { + "start": FreeCAD.Vector(-10, 10, 0), + "end": FreeCAD.Vector(-10 - sin30 * r, 10 + cos30 * r, 0), + }, + 3: { + "start": FreeCAD.Vector(-10, 10, 0), + "end": FreeCAD.Vector(-10 + sin30 * r, 10 + cos30 * r, 0), + }, + 4: {"location": FreeCAD.Vector(10, 10, 0)}, + 5: {"location": FreeCAD.Vector(-10, -10, 0)}, + 6: { + "start": FreeCAD.Vector(-5, 15, 0), + "end": FreeCAD.Vector(-5 - sin30 * r, 15 + cos30 * r, 0), + }, + 7: { + "start": FreeCAD.Vector(-15, 15, 0), + "end": FreeCAD.Vector(-15 + sin30 * r, 15 + cos30 * r, 0), + }, + }, + "Sketch003": { + 0: {"start": FreeCAD.Vector(10, -10, 0), "end": FreeCAD.Vector(20, -10, 0)}, + 1: { + "start": FreeCAD.Vector(10, -10, 0), + "end": FreeCAD.Vector(10 + cos30 * r, -10 - sin30 * r, 0), + }, + 2: { + "start": FreeCAD.Vector(10, -10, 0), + "end": FreeCAD.Vector(10 + sin30 * r, -10 - cos30 * r, 0), + }, + 3: { + "start": FreeCAD.Vector(10, -10, 0), + "end": FreeCAD.Vector(10 - sin30 * r, -10 - cos30 * r, 0), + }, + 4: {"location": FreeCAD.Vector(-10, -10, 0)}, + 5: {"location": FreeCAD.Vector(10, 10, 0)}, + 6: { + "start": FreeCAD.Vector(5, -15, 0), + "end": FreeCAD.Vector(5 + sin30 * r, -15 - cos30 * r, 0), + }, + 7: { + "start": FreeCAD.Vector(15, -15, 0), + "end": FreeCAD.Vector(15 - sin30 * r, -15 - cos30 * r, 0), + }, + }, + "Sketch004": { + 0: {"start": FreeCAD.Vector(-10, -10, 0), "end": FreeCAD.Vector(-20, -10, 0)}, + 1: { + "start": FreeCAD.Vector(-10, -10, 0), + "end": FreeCAD.Vector(-10 - cos30 * r, -10 - sin30 * r, 0), + }, + 2: { + "start": FreeCAD.Vector(-10, -10, 0), + "end": FreeCAD.Vector(-10 - sin30 * r, -10 - cos30 * r, 0), + }, + 3: { + "start": FreeCAD.Vector(-10, -10, 0), + "end": FreeCAD.Vector(-10 + sin30 * r, -10 - cos30 * r, 0), + }, + 4: {"location": FreeCAD.Vector(10, -10, 0)}, + 5: {"location": FreeCAD.Vector(-10, 10, 0)}, + 6: { + "start": FreeCAD.Vector(-5, -15, 0), + "end": FreeCAD.Vector(-5 - sin30 * r, -15 - cos30 * r, 0), + }, + 7: { + "start": FreeCAD.Vector(-15, -15, 0), + "end": FreeCAD.Vector(-15 + sin30 * r, -15 - cos30 * r, 0), + }, + }, + "Sketch005": { + 0: {"start": FreeCAD.Vector(10, 10, 0), "end": FreeCAD.Vector(20, 10, 0)}, + 1: { + "start": FreeCAD.Vector(10, 10, 0), + "end": FreeCAD.Vector(10 + cos30 * r, 10 + sin30 * r, 0), + }, + 2: { + "start": FreeCAD.Vector(10, 10, 0), + "end": FreeCAD.Vector(10 + sin30 * r, 10 + cos30 * r, 0), + }, + 3: { + "start": FreeCAD.Vector(10, 10, 0), + "end": FreeCAD.Vector(10 - sin30 * r, 10 + cos30 * r, 0), + }, + 4: {"location": FreeCAD.Vector(-10, 10, 0)}, + 5: {"location": FreeCAD.Vector(10, -10, 0)}, + 6: { + "start": FreeCAD.Vector(5, 15, 0), + "end": FreeCAD.Vector(5 + sin30 * r, 15 + cos30 * r, 0), + }, + 7: { + "start": FreeCAD.Vector(15, 15, 0), + "end": FreeCAD.Vector(15 - sin30 * r, 15 + cos30 * r, 0), + }, + }, + } + + expected_geometries_pattern_006 = { + "Sketch007": { + 0: {"start": FreeCAD.Vector(-35, -15, 0), "end": FreeCAD.Vector(-35, -5, 0)}, + 1: {"start": FreeCAD.Vector(-35, -5, 0), "end": FreeCAD.Vector(-20, -5, 0)}, + 2: {"start": FreeCAD.Vector(-20, -5, 0), "end": FreeCAD.Vector(-20, -15, 0)}, + 3: {"start": FreeCAD.Vector(-20, -15, 0), "end": FreeCAD.Vector(-35, -15, 0)}, + } + } + + def execute_carbon_copy(src, dist): + obj = FreeCAD.ActiveDocument.getObject(dist) + obj.setAllowUnaligned(False) + obj.carbonCopy(src, False) + FreeCAD.ActiveDocument.recompute() + + def check_placement(expected_dict): + for sketch, correct_points in expected_dict.items(): + obj = FreeCAD.ActiveDocument.getObject(sketch) + for i, pts in correct_points.items(): + geom = obj.Geometry[i] + if "start" in pts: + start = geom.StartPoint + end = geom.EndPoint + self.assertAlmostEqual((start - pts["start"]).Length, 0) + self.assertAlmostEqual((end - pts["end"]).Length, 0) + elif "location" in pts: + loc = geom.Location + self.assertAlmostEqual((loc - pts["location"]).Length, 0) + + execute_carbon_copy("Sketch001", "Sketch002") + execute_carbon_copy("Sketch001", "Sketch003") + execute_carbon_copy("Sketch001", "Sketch004") + execute_carbon_copy("Sketch001", "Sketch005") + execute_carbon_copy("Sketch006", "Sketch007") + + check_placement(expected_geometries_pattern_001) + check_placement(expected_geometries_pattern_006) + + def tearDown(self): + FreeCAD.closeDocument(self.Doc.Name) diff --git a/src/Mod/Sketcher/TestSketcherApp.py b/src/Mod/Sketcher/TestSketcherApp.py index 5f40d89329..3be004777b 100644 --- a/src/Mod/Sketcher/TestSketcherApp.py +++ b/src/Mod/Sketcher/TestSketcherApp.py @@ -26,6 +26,7 @@ from SketcherTests.TestSketcherSolver import TestSketcherSolver from SketcherTests.TestSketchFillet import TestSketchFillet from SketcherTests.TestSketchExpression import TestSketchExpression from SketcherTests.TestSketchValidateCoincidents import TestSketchValidateCoincidents +from SketcherTests.TestSketchCarbonCopyReverseMapping import TestSketchCarbonCopyReverseMapping # GUI-dependent tests - only import if GUI is available try: