From 176ef6da4e3d6062bd9c0632538c5b3f712d3cf4 Mon Sep 17 00:00:00 2001 From: marbocub Date: Mon, 22 Dec 2025 23:00:49 +0900 Subject: [PATCH] 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 * 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 --- src/Mod/Sketcher/App/SketchObject.cpp | 160 ++++++++++++++- src/Mod/Sketcher/App/SketchObject.pyi | 12 ++ src/Mod/Sketcher/App/SketchObjectPyImp.cpp | 12 ++ src/Mod/Sketcher/CMakeLists.txt | 2 + .../TestSketchCarbonCopyReverseMapping.FCStd | Bin 0 -> 21085 bytes .../TestSketchCarbonCopyReverseMapping.py | 187 ++++++++++++++++++ src/Mod/Sketcher/TestSketcherApp.py | 1 + 7 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.FCStd create mode 100644 src/Mod/Sketcher/SketcherTests/TestSketchCarbonCopyReverseMapping.py 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 0000000000000000000000000000000000000000..258c53b1bba5d0bafcb2dbcf72fb68afae0c2ead GIT binary patch literal 21085 zcmcg!1z1(v)}=!P3F(%U29fSmP`bOjrBk{QkZz>AyFt3ULFq<1h5z7vzDwx6_r34` z-d^7U&N+LGHRf7#%{BMlQ^kZq!JYyE0YL#-=sGA8SY0ePq5=V}_5%Yw27JnAs%d4c zZDL7fYizWwtoUK3!^(NQV6CT{xY3$1$FH^9mQa(Ppc8ZxrT10T4yx;LZ*=l#MX-^6 zo9JE_MC}s>L_9bR2h`YH1Wk85A0+UOe4CS27ygLSi&B>+{^H4A9n^xi7O$+=xubMO zpC_oSW2BhVF`Vgc`@D{y8i$MCMLYd>Im?JG$`xNj=X$h5GHOD{UItxY%BFRpi# zUR4(jABXlR;Lp5~njMi!DOw!?Taa0NB`e@1aGJ=@TM{`%D5^^z85lw*RbVQAIY;6W z!l)|Ju{+zTD}gbO@m4;AeYxHv`~W+g8~I5-`gr9iXL<**vDPi7w`Hc*w!%_=bdZh&N+ z$g;)pwtI)2E}=SoKsq5)A1Wht4!j#Nwsl*nL0=(x|?h8rJVvW##zI~L*^Pk+czn0GpW?&l4O6phUY z#bdNfBcT_S`ux%}VPv<{0GITUNYCu67f+~RC9x%&{6({{c5<-^~&VsqikVHY9?f5C z?Ox5rUSFSZx_lp;zCK;r&TuL8_F=<;o8~os>RU9fa!*yrP^@XgUR7Cjh27 zPoM%!BVjJH3mh$5Ht2VVf{Zm!RSeSZie{x2z(Oj3%^`q|yFo?xl(;;aNvR3C$&J;y z+94So-_i0DnPmzYmQ4Z&7Ae8YTp`9{A#Vwyd?uZE%ho2!vLcC^kD1nt;@ede3uC*) zIrFdOTy#(l;sf&dQqW|tV5a?&Lue_F49K4xB??V%v^F@%KXQeN*xg z7ku-fg%)|9JIgGHm3G5YbHp4H|C0=(NFH3B44v6=7byw{3Emnp(KjIyd@+)BL5omN zNwHvlnAa*{62K@Z5yLe)eY?E;B+Fjb3{!K~mt7(4?IY()b#%n=V{itI#|g^g2Bk}S zy_x1ukCc03Vg-d&Is;^0@l4C7BYzyQj?_;hor>nA-DjqGVZn@UXA`aT1u?|tatY64 zX0~s3&icyY(QLX>zwQ{kQVIUFkFIfY*|gh1`DxiS&q4XODdW9^^5)W)Jp;8Cej^Go zd3KmSQnS+U4nhU9cd}dUI$B?iD!*i*W`3n@Gl!x^HhfV2UP3;x!t>||sef8QmDAHs zv3bha_n;ibenaF%cMax+UzRNclZ@(Sysn9I)JBJ&6sAn3RrYyCHNV|}QKgdcsLIaT z;<}bfy%BpoolwiRU6u=pK~r>r2O@%Z4uZJ><7Lxo27|lc;&b#w=eV~{bSoeW_~|>f zXBhL?kK3qoJ80BNQ};%_w%T}Fz+s-}?R0?`y_2LJnI>EJ&WC8<0gWo^(&Kigcbut? zTFX|4-O3w`hLtbvJr5VE59H=DEyKK+V>wdav)jyWW1*{mGtjgwEnlkVe=!-DCqd~DL zU+REcV1@k zt9Ii7?r`EYCF(aUFg9=C{t%}io4siE8|${E$_7H5^*S~=U)gc zMM`4P6Q=Skjy2*0I&^*3h&>I;m{QZRri`t&`XmZ6sL37A&v_~Md}iM3grX%^$Rnoe z2xB;Fna(d`Utg7v_0`cMnc{ju zz4#f$?sK&o7ce- zvknh-t&x%=DU7+&Wz0o!6*Gsa^I2!jllsi6%@;423DP;*&D|v^pRCcIG;YLejn;H# zL-Mb>LsvsXvmk)U=RMkw3Pq$o9PWMnBGAXfTphVBD`N_cOvlZyectiR&X|^VHURscnz!pUa|~S*Eh#&MneV|+pE$rz`?|4THQwx=W{EtkFXsaXaYgkJ2e6- zBXhbBT929Z=9fQra4W&TuT0wTnVvNJyiNC6S##zc?+g|1OfiAUfgfIlyj{<>3DZg+ zqYc_dI>yVGRs^Y!{|mt+H3`W`Lt}$Fz2bBH0o$ z^D`XoFrZ+t9z{r-VZ2=-)(HWXf##(u3OrIDI_gQmHteNvr|agB$ehRf){NM8tdH%w zeQUA3YL|Ib${S+WuXfr93LMg?GCo@MxehB2OWCiIlWbgmIAD1NDPH|rQ+9WDtcT!} zy+@_sc!PKK)G$b8#dIw@8uceqi36;K2 zSY?;r=D&!g9&}zCBz(kwXh-}g#HogQ-MALZq7vlx>4mKXjXO8)ScTO=-CEfVebkTh$uj zOx+S3O;tndaDPyDD68%6{I}oW}ms`ic}fmdMi_<(e*& z{*eUMc*9j9BzQce$|gEjU^;G03Z$oaNcjnL+`fAriXB>GHXoEK14ChE*s2;o%XKgD zv`6#?`!hwti0l-M$WbAF6!xg6=k_7mQ|U|!4DR94j8X%Ug7_T&k!v6ZiWtFf0CCGD z3W>j?vo>5Ii;&JYgF@-J-o#fML_$u?`lBU96Bn>gae!9OYqn6JjyE){+ski#j*bOP zl;vu;zSz`dC`B;f?k6r0}vewAZ#~ z6__xa+>^Y#L*Anv=pY4j`WQY3@2O}LC&}lIssqLoNRoa{kHYEjSmV0ONS2n0Up?+X z&Q)%0LecQjErC2~b2>WaVlENGJH-74KPyd@*B@c7-sEYE{q1pW1I&jJNf=n=l}~eJ z!z!WXeA@lC3~+DhdfF{hE4o?mwUzBB^Te;C>TN`XSX}83$3EHbyk9QW{ks1w6PaPX zRM}2f6Q=O894MMnDUZ_3wH!#u7{-5}LT7IoAt>NYE5{nQue4_kVKiU7i72EyztnS? zd@Lx!MttAyWihBg!G_$$Q)EOaj$rRp^d=)-jduhoT!mfyXcXybc4CsuqpnC5?z0$j zy_ExpG-ONBVF&j~Zy~dFDWp9XR@aP@a5&2)S#l#dB+g^XxSEN?b|_F+AYb%$kvo;9Oy;GHKmGWMrJ_cnNfkb` z^=+0z*NahXDgs+of;M6SV$}saKQ&iH0Y)4%h%Gj$o_C-2mom`59jRPU`RGa#b;gH4 zX}#{)Bsi+d#x#Jg&kWrM!O^mT%05B|UQHQ;OsTh5cOad!1J3P%dA;J^_R2}!^fToQ zRYmFE8zPCRL^$tA#r!drhABqtDSzT;^T7IBA*+-J%{_djqgTb;qr03_sA`kQudKmC zm0h)D@x~Y8lwB#(E}HYXR?NW>KhewLX)2dZG?EMN^>M!gUQMbxTuq8~gn@|?j9S&| z(R>|Y>nBD?(eeHx9<|>R^(N8?KNNpucFWhdGazj5#XyRC%+a{ar6t8+UO}lf3PTZ4 zJQ_odfMJI2ZKq96hqwIB_m~N{o)A&&`J!(I2@RL1nms!=rR$I$-wp5SiYFUjWY7IYNlA`zL zQOaHBQ5F$t@Ws*ll1RP~fPdDtMFFD~%EKl~W{$O>o|RwnwRYv%kfuCP=;QYt1bDe3 zgKzVR{IEi>pE7|6fGoi5pN-eRD`+bEWzb@hMlGRx6q~CWkbivJhxgPL+(n*LFU8{- znCl~v-8C5%QjK6S5Bo>jMT8)fL8z{8Q22m4KqC0@6-b%b(;HDf;CmJK_`*Mudtd26 z=SsPH%qdUOMfXKJL6rjO3@90Dv%{mLcnK5%DI>h45yLICgOK@?txTVWwy`&nzS?Vy zeW4+|VnWz04Hn^{#!z}1MbZ(J?3h&i8I)Soc&M3;F{a+2a_8O0F1z2n1=AP)1Nhb_K%I)2-i$`WytMp_?29pGFpeBQp518e|bc; zvOM*Wifuz(lF)>3x-Uxpgo;?!R+=00E5B0ItqN2*XopVvvlirO7>bXKGIe1RvC%yU z?KM`5&hta{@80FOq$9S__Jv=awjSk8HrFh(nPeC~v7H7zIOdu(-^zC25>1yY8Y&ZGl2Ke}|LZ z)oDGfc`UEhGU{-IguQ$X1(JCt3ojwSw;WM>N&=0hKHl`cV^a+XZePO+D?l2S^4bQT zYnIcaP@hbpPHILAT&cxqMn*$4A~?1UM!2IF(Lyp>9lC0EHLXui-iXJ<(3;dnWCz@v zg3`um$bvT_B9@_$f|60VyLZT9oRN~zEDjBR+!r_qiPxdtFe7UjI3l^sIYLWZ0I%`p zQ&5mNG$E&m2v)m{ccR_caaZq`Gt|zW?{|s?Sz*kQE5 zWpr-dXq1bkQWkQdz-Se5$(%bsMeg7!d#r_z((!4(`G$;NJ6*vwH*hwx%Y{MBHMjSX z)*j$POgRAZ2A=&Ek9@t%A~>+c>}b489?u`V__E~(PE#i_u3~##{ALS0%tOTE+r+n=6$r`kM|S>J*A_Pb|0J zV9pM<4|fWY`YS}pvUPVk?S9Y|uh|=6Y!o$y_|PU9)ZSA$4e9BBi z#Ih`VGK}KKAQcuzy1SW+(e8XF`k>th`-XejiZF|nWweg){VMcUm&JQw$rPXW!VGs1 zS60C4&X!4bIZi)anGE!71VoX=A69z9j}LPExH<;jPnWPnl(a;WtQ7P`9JKs6Z23vQ zSb<@QR+>nKFC#Vwg#{6Mm#12?@-gLb^70;|sOy|`WdCBpMef>?9`Ot? z$0_5*zLk6gmd7^2el^%$HEGStSnq4XR%yO4e`2$7%PDN_WDBMJf)|PqO#7&T2r4_M zCLXqZma1hobipL@s|mPSg&lw6bS@hpHO8{5yWn{2APMX2>;N0js9Jk|yeNu(6#?n; zynoV}_xwA=3CQNkf@$PeQ~kY)?r#-Fr{orVY;L5$c+gR(o~4t%j@xROF5EC5SBu{# z;dRv#!i4%a6z{4IPWtXQ&+nu!ntK0$wtTCwJncSqdNF%4YkO(+Bt$cUA`r0jLr=L)Smti!8 z@pi6a+U$GX&Y@=$>YYPFG?Keo`elH5uws4hXwZL1`5L7$IJoC7Xz#+rx zI76As89Uyvx^7Zma=Kyb*?x;k!w!;j-emVe#6C+69R5J=x@+H{Yf*t=KXlNS%u#7gYiZitJ}6~Udk zyEs*5m_(?Rb{4L_MG>8zL6ouhsOD6t?={IDLs`hFAtuvv3#0JMF9iefiL}vC=R>7+ zR=}^=iEzmU12w|$wZ7t4ctzxiSotwlEdj9uf0HC9X6R!jFg$=+xd`?xDO zGKla)aQu-ZV)c%seUWHfJ0BS1V*;B267e)S6u@?Dv0KEH{i2lEB$Zg=r^7NFDb6ox zK)S@h%PYSA^64lSFTQN1mzzQssj5Bl@@D=g=AHwh@j)^b40r=OeJ-^~h|JnTVbHB1 zpm20+{YEF1jTi6F`XOHHxc*4SX31~^;NP}@>dkLtCFrM88My3ucO*QID%2Eaf-rcgjY@PFd| zeSV#kxTzZrymN!9`G;w=biurZ6z=(QBVQc{lM!Y_Uf4|EYiCL$9ZaG*Av(Ne3(EPE z+4i#Fx2Thr4DD%?vGJ1$d!9RcI#&K4&c!xt)1RD&$yPo8(v*AH1c8h=;eh9?Lh430 zAmYgls*3=LQLJe+`aM)K@PNtW@ z9v71wI}|ul@!)b+c4=CN`mfQVCiL#qzVwXyO5{uyp`Gu)ZGK`$3?+xv%~&^8S@;sN zyR~L+nmP1aag~vDwOHn{jP&>k8I-rNdIyJDyC)o1LIJSifFA1z6@ex7y#AuAF_$u0 z2;X?BFE(rHu#29kTPaDI^&ZJKe#4=kEjr;`XV9?11@fgjl(T{Zf#K!&We$em);VEJ zrTUhac!g7uM&WU}s9iTEY^an(b}HJi?WZ9GA5(rN;RW@Y%JjJ&Ah8y(5cdx;SLLmW zLr?C`C;0@IoG)NLz5`x2Pk;V3pMT8DTXi!kOIu6Npr%L7H1K@82gqk2>%?QirV$Bz z2v2uCb7=?CKhYi@1QI`=(|TeW5g$KGiMd(rDroIIKhKXB%p@Y}QudCuE)MlkJVO8s zCtJUsNX-lQc>Pz6{dBFNubfL6S=W}b-}7=>7%*fGpea1xOYV`1-t0N=wm4FiBQ<|2 zl%y1UP|kNrSDn5WyoBY{;ryCEVf=`N%DVN!qK0C8NtBt~dTs&)@fyAO6lAQE zHzwaC>vi8-=B!S=2D`+ARm#Gt88;%RiOLsC!py@QIWHH5=+tw=nlTPJ)<1PuFO|## zn9u?H6I81dLrG90s(?b%9dP+O4!gKOG%QOVmI9EGy<}wM@)cLahrX zv-)jHST*gFj(CH3Z|_4qkonE5w0UV)+~>%L)PC4cI15HrI9Jb5Sowzb-d+ zXf@b9*EvvQ(9mMBqnvXLfZ1odo|&|zFwxN0HL`Bzu}Fmc9(ifdY3#i>X71ap+~;)h zCMg8#vXg5q@Ea)lGbUz2SSZjP2OzzP?g4?TxU9sEckI=Sb_s$BX&oij`>Qhuh`NK3 zHESxirQ0)SCzea#`^64l#buWgBif;slVKp&vMaNUMyd7^Nu^$U+IQq}<$%4=iz|+C zTsZ_OLLq(TYPCe366z>=gpyhVr4f!d_H=1K!`n(hv2(=4_PqTK`8w*sy4t4-d(!rXPdcrZ-09)Qao=; zH7_tqgDM;y+R_eKjMxa4l#sm;@Ca@tZTo9}l&SFoipviaJjts*iZHVf9rY{xTnh-Q zxP%%tszkzueB12OyL9OSaisCXonzL@`}cwdz~*1BJi5PMdAFgrS03Fz+j4HN^B4bp zZ!iAy-su1P-st~XZwP>~x!HXGbfmx8e{T=Q%l}^9%YP>Cc5e)S-&;n0(6<+W4c{H& zZir?0v$rxyWl?vUr%nwG3mn;U!_8uLN@}JUZ}ASQ)O`p&s2nLK8%`qLBBfR4`-g?a z4o|-m*-e9|9=46nrIA#W&awDlXznk2_L8H!KAa+?Zbn);4Z~KVYU}x$7*HVV)dBa8 zN8?kA6o^TRhvudT9XRtzD3j$cuV(o}tV^}00@ z3=PAOX=Vi5?5zl&n@VL7iOm-kV<3_xQav++;H#*e7}sO2Y+mY5Z?{6ynKy^auidTY zjo2@TtF3!?`b&0(hLGk!Ba02EjcDyTxf{%$2hM%|6mpdZ0>3B^<$~7%bCjy=a1{ld zfN}wLHxRX}mz@Oxyaom^&bI^6&oTbFe~Wa(0dlL_e%UQlG@(u3*i@1f+Cf{Ab%obS z@OiL_rpcbR2lO>12NR&vW9vZ<)>k6|Pr;rldsVoUO0qk~kw6<$EcQ|NBV}*yLGWp_ zJo?g3@k-n`Rq(qJ>qjYYGZTA}ZX9qja_dkXiWgl022Ew#s;{CnKRwT-)ri(Gns`J< zz$l<2pbaqc*M!^g;I@Y`{=J9YhTaY(jDLFA{p_`vSF0Ht1_+2m6By_T0552z zf2Xh|epYfp2%~NHJGuR2c6=pu)2s@Q4u!mh0s4?WL zA>bxfyCp`YkSUYHT2u3>UL1KTcTfY{J_I^*4S65(Z4hn^Jr?$9CGY3{rY~|Ig9S!l z?D90U+DZC7OIr-WMa4^0C%aj5^v^RJo94b59gp)j;)pD)ap)DF`dA3Tm5k+ft`G}r zk6zX0b3(lKEaW!MmCw;bZss?Q8~PS$i(0?^W`rh_e`G+L_K6&w0YB<^T>CmJD*H$5 zVI8X2+EPwu(K?zs^~6ttgL)0*aHBP|-@l6&iHeWp2YX1)2^%20hy=PT3~MYUlshb( zD^7__Xi*0VZwDGE=Wdd|sy3dwMtM1@q}-l-`uMZ^leUj+--+y8yy9kk;rgE%6mQgf zuZo`t`g`i;_F}}nL>^%Z6EK}J^2N-dey3?S^Rd2wSNfA+pKU|lLLR%}AQh558gfb& zt?F0CE_tkh<8i#N!mr{eL`>X|Yh?t~OEtP0T-Uz#e^dJWByGpxTjH5LM+E`Pq;Jq> zwbxF4s-c)8VUI<2^}0g6c6GY`T6vayF$*MZsj#NH*7quVQMnm-d~49e9)CP&6F2L| zi`eR-#l>b@Vm4#bp^^Q%9gF1gs&6c1AkS(=M38Gus*$mEAWIxjb!^H8w%DTL6%Y_~ zNHQMjFwXh`myqjNtBjVFW#LA}59yp7?4PH;<&Ds_;n)(%i-M~8C<$H@Ytrg5i(Jeq ziDbPR?InkR=4BFHzTC7kQeKneX}zjf$XcfHYzxrS7N92>YuTQifQ_C7xE$89?Hvo~ znqhStgI2zY_&BnOSxa(;Eja5Fsx6b#S32jpxei@Q2PJCR+$}44;L7#vII;C{b({QrD@uxJZoY zPN}VKg`8;Ytz%goTn-;p>nmL&8Yd1!aT+%CF&me*@vrJfQ5K_Mt1cu(=V(+nKEfat zA%w}GO_wynkbJUhZ;XLkG~J`J5(F6@PJ8cr|Ni+8q*qo1Iuvo!C^*u5p?uXNWGMHzI05{x$xS>$XAW&u*Vv{>K2Mr zLjKULBfJp3Aq>x|V6&S4Rd#KBkjTT@#OpvZxm;$?sQ=`x@BvvW`pn!hVnlq@ntiCH zJVAW2PR3_HZ!0%K=$*;okW^$39_vs8I}Vms#C3h`nCfo_h}_6YJim&25aQZC8$Z=kAut-n!P54{Ss0+DiU1J*|OeQDrl=Vo%JH*1Yp`ozMxIT{gUzEi&C4Z z6h{6jje4S$*Uvut^?Pbgr?ay$$SwL9M8-ts&IId6C5YSewumXtDGIsHN$Hxp!7JE_ zerCe+_}N-5UE$pT4MhTyM^1_eD%pb`zIlB^cwOQ>2p$h!GbOLb5CLugJfMjQ;Gn4^ zxW=k^;Gp6EZ1R?{XkQ~}d02N9Js5s|h^9`ftmbjkJV#s+P^6&+Nbz9<%&P4{1H@>> zfCQifPB8++?3dgT+yJes1C)T`nHf^R03-p}zZ#)`J}|)yoxs`>Esx`eo8E6)VGT9GSP^E@WJpMVR=Z|_qm~Nn)Z?_ zJPJZ30#Byy-@z~_wXjr_?I}4Hr!FXP{s}skQ{=q!Cwz4yc^urkgfrL@sddQ zDRo`}6`!fd3h)-c;pkUzUSm!}2{CM0$AHOe5v&o1(&qI^R!8azWTc&i=M&2noz<4=w=@ z$VMiSH8RTt&1^(6lLp--qqKkEBpmuB2t(;7Kd`IM`0@x;SQ zS7_oi%zf2_Nmvl~PrL?YTF=vrzx| zLtA}=p9SaHk=qE21hrzMeF)CUt2#Wu+wKhz#!!3O5WvZ49)kNiq0}W8*0#1}qLZ%& zQ1}FpTe_wax=OZInZx%a?W(VbUbBTWvLzh!3tI4o`Q~{RmJLkzif9@pBd4hYeptD_ zP&33gqLg}tU5-MQji?Co`c=yq&+urCDTrNN+IRlO+H!w89xFm#Fu1t>)rWLFOX972jx7f3vOKuZoI|R8yHtqY#Cn%ruJ}+b|mg%lh=-PiHm!M#d5CPblBN#R9I4GV(oKR8h8bp_- z!?j3~lQn-^l4|;vu1HIdhBFki+x(3xQgKnfkgaUt8HK0_GKFc|zNMTBez#)Fb&BV5 z2pxs#`2@l;Ju8RfMigN{#{5g6A(!1=#X;paHF-%P@t3=tSNVq5gi9(?AslHZfz~za zj5`~G*XJjbXGiMar}y;P$%g|&@dA-Q4`tVHPgQeUEAI^rVfB-gE50-yH!o=xk2_z) zD~He9V2&_dOCVRlNZ*-?(DsRp(B7cF(c{k(zyTB0nL% zHl8UX)_%0`<$QnN;32e)b4ZS*Z{epm!t z-YfIf>$`dTp5l#bt-e%7^6<-i=S*flZ32iKd-C!rW-@xo%lB1ZDr>P~3*yEG{|^3IuJb(mRgI{Y4; zLoGPX3AFO6P4Cs!)hD(In@9C3<4+R%smC|1mTd`?rNfTGfPuu!S-)<<0O5VfXtLnc z;KDXJA_&WmXTpR5%05hlm}SkZ#sq47XVMpxvDkgKh}9DXGf=lgn|=XhEnhFhEvB2U zsV4Qw?)mV_s0T#q@r*3Q#GZAWM62YXBu<{Y*~GS}tm+CVG)~{Un61G#x);Lvm?>mr zHxqvq%$>VS0IYXlCFL|c zKx!!H#h{~#u0g{d+OKhfB1cQ}6mo1QcwcT7(Wg~T`lI!#P-l@R1 z3A{g?1Pi8%ASsWVTjy#u<^!EAzcbDktuGpNoEz_Uk|0eU8ObrDTx7ZI`sMV8eR$(& zJowUzN4ye4huw&S9qnLOy#uL~k8x)9xXf$yt;KZu3W{~#$`;ksV40^CHMHqAZSFSc zLDI)-#GIrzgccKN-8T5oFHaA`>%BS?N}`&~&ka|};QfM%# znSKOc_M)-@d?W8v=@jhLXQtx;29&XV*6Hi@#l5JAtlK-*g`zQO$%8^O z?+UvU`BXpTNbIi7h?dQ#l3=3)(>+cyw;$S77k@ijYnPm|CG}PD?NP*uGLKzhK}`~a z(R+o6RATLjQ7$;~z=f_VV3(Jc+B-6`430Y1=BJk7Jboo)GY7y#-XFoF3+$168v^QR zf}a-*o1e=}KhmW{ z(*>9E_$UW?#q2SUDr3#odal8R=4IoM&fdq6!AHrqaVlH^3#on;9C=@uBVZ+yhmMxN z5DkxPTO?7teTPia$2~A%$V1B-$(5qKZew?iGhdU_IJ5p5&NXI&Mea=Rh_W6-*D#SO zos5PKuTv#7izHu zS!u0022pyjE8QTy1s=R`z+DU&BFh*Sht3?JmgtRj&vEF*X$hCd{WPASmcR|=jKA1yn?Y;$aUNA z>?vC63$S?*KAO9Byxs-R3ETvaO+lUqEmNo~5s~R8Odh=Sb}FfOa>Pio7faw3>4(kG ztgLyaRraBJDh5T>bEQ>4|5$e0lM;u180vZa+y@~I4KXz?4|2V)h;z9ibzpq;Cdfh- zj4F9LqlsC^MP?VoNodOPmZ?nS=^fo35RFe!WXqpsN&%{{8=J~Ewx!h*+eL^7F`@%Wr}9zXo5W)`h40;mCv+{3Bn_5 zR0BK|PV$P1oLG_>-740r+{+5E<>15N^B%i8R)i<%*!%DX3&^;{Pquz(r}SH?x|-P^ zE6HrFdQn*u#PvmM*UIlr+T-N8JV=_RYgZ$hy(I%b;kiunUCsO~n=ZB4{NmQt@&b2M(0-lIw>#RE^@|L-Q_S6R-)F;FjwLI!ox}*}Z(9L7u%zNp+W89zey=HYw`v zj4Lf-n$1A2FT?T9%8l!!FZ2wv79&&bW5~!hS$fC0CF<^r{#-zfev<<;-DIfB=mP6F zIp1VtXD7wog1vOmqoM3`Wi*lL3H8Mg-bZ4mL2o#PTsMuYM})-9RN;eIp=->kKAqLn z3QjL7KU)i{TJh-(BQ!~5<4r49!}D5gXI+6Gn&`TdGaxuis4NTC6bQ+WS?FJ{Htg%b zfGw>*=bUgsJAGcU_)2$ScP+r>-s9QY2!#T4z;;Xuc-`FK`td-5+ssT`-9p_&Q(IFD z$ZcR?;QI07YjhSC7Ot7nQhEmndZ2qOX6nqaKUsb}ti5>}{3i?TziF<~(f)!4E8unF z&27mWF~6Xp`$=={YAGlP=@T0fq0|2!;+__2XV$~N(DLFZNkck1t)->q`$~O%{o}`a z_f7g68v6eqnwS5O=JquGVq_S8()`Ulb)sd_J^~Cb6ySC9=>Cnqi8imPk*Ni@g@wAE zrk0x8>Gjcp;Jp#Ojk@s>F;jgL%fE-MO`%Qk+;jqX{X#S2kM3|WFwPs2i`QHP1>0Tl zI4LQ66M&&Jfr{*6v=#iS%tQ0DxQo;BebxOuO+xKLL-Mn#^gp>4aTlu_7$E+Q%)7<` zeDLqByODX%y5Hjd#yqC~t$Bd8@n4zuTioB6$NaxF53p+gEAxJfyKUahbF_PLkCx_t zYabw-{8#q<7Wa4d{o&UCkE?G$;3@x2Sh;!K1c|$=?5IV%>5SkclQ0^(El_0{)p25 zxqZLI{hfUPmj*2K|CxP%gz5j>zTe{h&OU%!|Nq)|JGJ~(payu_zf#n{&Mj}}bHCyN z%;+CH|3O~(D;~he{Ue^+x$ZA`=l~ngA3Xm!+5HvEzccS&SOpC7^hX8XumArBKnVYF zl)B#hy8X~wO!zMH=6jHvAAsW~sr-4Ax^4iH`McGje}%odEBNy$bwvZb{avl-eU|&R zqJOf8{mJt0LcROw`xT9UqSpY|5x=T$yw7lh_&EYMH3>hDQr8rWpA5e&GrUi9zs&GY zqPaUnzqRFlrP-eZSlG9f{6X-qa*z8eZa}{_2OH;4l3&zr{T0zo!Nt#`)K%^d(QnPU z0sLBt|1E-F)OG!HC2ewU{mf8{nngf-n&Ihf0{D}Ah=h;bieoe&uRVy z<5B(osm*uF3Apm_}`2%0O-T7VRGY^zMh)K7>cauDjB8r!~1!o;r{?;XrxB~ literal 0 HcmV?d00001 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: