Sketcher: add reverse mapping correction to Carbon Copy (#25745)
* Sketcher: add reverse mapping correction to Carbon Copy * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Replace M_PI with std::numbers::pi * Replace vector initialization with Base::Vector3d::UnitX/UnitY, apply suggestions from code review Co-authored-by: Kacper Donat <kadet1090@gmail.com> * Fix guard clause logic, apply suggestions from code review * Replace std::numbers::pi with pi via `using std::numbers` * Fix issues reported by the linter --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kacper Donat <kadet1090@gmail.com>
This commit is contained in:
@@ -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<int, int> 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<Part::Geometry*>::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<Part::GeomPoint>()) {
|
||||
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<Sketcher::Constraint*>::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<Sketcher::Constraint*>::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<App::Expression> 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>(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>(
|
||||
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>(App::Expression::parse(this, sref));
|
||||
setExpression(Constraints.createPath(nextcid), std::move(expr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user