Files
create/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp
PaddleStroke 6731e605f5 Sketcher: Constraint rendering (#24534)
* Sketcher: Constraint rendering

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update EditModeConstraintCoinManager.h

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update EditModeConstraintCoinManager.cpp

* Update SoDatumLabel.cpp

* Update SoDatumLabel.cpp

* Update EditModeConstraintCoinManager.cpp

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update EditModeConstraintCoinManager.cpp

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* revert changes drawing the box around fx

* Update EditModeConstraintCoinManager.cpp

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-02 15:51:14 +01:00

2950 lines
140 KiB
C++

/***************************************************************************
* Copyright (c) 2021 Abdullah Tahiri <abdullah.tahiri.yo@gmail.com> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include <FCConfig.h>
#include <QPainter>
#include <QRegularExpression>
#include <limits>
#include <memory>
#include <Inventor/SbImage.h>
#include <Inventor/SbVec3f.h>
#include <Inventor/SoPickedPoint.h>
#include <Inventor/nodes/SoAnnotation.h>
#include <Inventor/nodes/SoDrawStyle.h>
#include <Inventor/nodes/SoGroup.h>
#include <Inventor/nodes/SoImage.h>
#include <Inventor/nodes/SoInfo.h>
#include <Inventor/nodes/SoMaterial.h>
#include <Inventor/nodes/SoMaterialBinding.h>
#include <Inventor/nodes/SoPickStyle.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoTranslation.h>
#include <Base/Converter.h>
#include <Base/Exception.h>
#include <Base/Tools.h>
#include <Base/UnitsApi.h>
#include <Base/Vector3D.h>
#include <Gui/BitmapFactory.h>
#include <Gui/Inventor/SmSwitchboard.h>
#include <Gui/SoDatumLabel.h>
#include <Gui/Tools.h>
#include <Gui/Utilities.h>
#include <Mod/Part/App/Geometry.h>
#include <Mod/Sketcher/App/Constraint.h>
#include <Mod/Sketcher/App/GeoEnum.h>
#include <Mod/Sketcher/App/GeoList.h>
#include <Mod/Sketcher/App/GeometryFacade.h>
#include <Mod/Sketcher/App/SolverGeometryExtension.h>
#include "EditModeConstraintCoinManager.h"
#include "SoZoomTranslation.h"
#include "Utils.h"
#include "ViewProviderSketch.h"
#include "ViewProviderSketchCoinAttorney.h"
using namespace Gui;
using namespace SketcherGui;
using namespace Sketcher;
//**************************** EditModeConstraintCoinManager class ******************************
EditModeConstraintCoinManager::EditModeConstraintCoinManager(
ViewProviderSketch& vp,
DrawingParameters& drawingParams,
GeometryLayerParameters& geometryLayerParams,
ConstraintParameters& constraintParams,
EditModeScenegraphNodes& editModeScenegraph,
CoinMapping& coinMap)
: viewProvider(vp)
, drawingParameters(drawingParams)
, geometryLayerParameters(geometryLayerParams)
, constraintParameters(constraintParams)
, editModeScenegraphNodes(editModeScenegraph)
, coinMapping(coinMap)
{}
EditModeConstraintCoinManager::~EditModeConstraintCoinManager()
{}
void EditModeConstraintCoinManager::updateVirtualSpace()
{
const std::vector<Sketcher::Constraint*>& constrlist =
ViewProviderSketchCoinAttorney::getConstraints(viewProvider);
bool isshownvirtualspace = ViewProviderSketchCoinAttorney::isShownVirtualSpace(viewProvider);
if (constrlist.size() == vConstrType.size()) {
editModeScenegraphNodes.constrGroup->enable.setNum(constrlist.size());
SbBool* sws = editModeScenegraphNodes.constrGroup->enable.startEditing();
for (size_t i = 0; i < constrlist.size(); i++) {
sws[i] = !(constrlist[i]->isInVirtualSpace != isshownvirtualspace)
&& constrlist[i]->isVisible; // XOR of constraint mode and VP mode
}
editModeScenegraphNodes.constrGroup->enable.finishEditing();
}
}
void EditModeConstraintCoinManager::processConstraints(const GeoListFacade& geolistfacade)
{
using std::numbers::pi;
const auto& constrlist = ViewProviderSketchCoinAttorney::getConstraints(viewProvider);
auto zConstrH = ViewProviderSketchCoinAttorney::getViewOrientationFactor(viewProvider)
* drawingParameters.zConstr;
// After an undo/redo it can happen that we have an empty geometry list but a non-empty
// constraint list In this case just ignore the constraints. (See bug #0000421)
if (geolistfacade.geomlist.size() <= 2 && !constrlist.empty()) {
rebuildConstraintNodes(geolistfacade);
return;
}
int extGeoCount = geolistfacade.getExternalCount();
int intGeoCount = geolistfacade.getInternalCount();
// reset point if the constraint type has changed
Restart:
// check if a new constraint arrived
if (constrlist.size() != vConstrType.size()) {
rebuildConstraintNodes(geolistfacade);
}
assert(int(constrlist.size()) == editModeScenegraphNodes.constrGroup->getNumChildren());
assert(int(vConstrType.size()) == editModeScenegraphNodes.constrGroup->getNumChildren());
// update the virtual space
updateVirtualSpace();
auto getNormal = [](const GeoListFacade& geolistfacade,
const int geoid,
const Base::Vector3d& pointoncurve) {
auto geom = geolistfacade.getGeometryFromGeoId(geoid);
auto curve = dynamic_cast<const Part::GeomCurve*>(geom);
auto line = dynamic_cast<const Part::GeomLineSegment*>(curve);
if (line) {
Base::Vector3d linedir = line->getEndPoint() - line->getStartPoint();
return Base::Vector3d(-linedir.y, linedir.x, 0);
}
else {
Base::Vector3d normal;
try {
if (!(curve && curve->normalAt(pointoncurve, normal))) {
normal = Base::Vector3d(1, 0, 0);
}
}
catch (const Base::CADKernelError&) {
normal = Base::Vector3d(1, 0, 0);
}
return normal;
}
};
// go through the constraints and update the position
int i = 0;
for (std::vector<Sketcher::Constraint*>::const_iterator it = constrlist.begin();
it != constrlist.end();
++it, i++) {
// check if the type has changed
if ((*it)->Type != vConstrType[i]) {
// clearing the type vector will force a rebuild of the visual nodes
vConstrType.clear();
// TODO: The 'goto' here is unsafe as it can happen that we cause an endless loop (see
// bug #0001956).
goto Restart;
}
try { // because calculateNormalAtPoint, used in there, can throw
// root separator for this constraint
SoSeparator* sep =
static_cast<SoSeparator*>(editModeScenegraphNodes.constrGroup->getChild(i));
const Constraint* Constr = *it;
if (Constr->First < -extGeoCount || Constr->First >= intGeoCount
|| (Constr->Second != GeoEnum::GeoUndef
&& (Constr->Second < -extGeoCount || Constr->Second >= intGeoCount))
|| (Constr->Third != GeoEnum::GeoUndef
&& (Constr->Third < -extGeoCount || Constr->Third >= intGeoCount))) {
// Constraint can refer to non-existent geometry during undo/redo
continue;
}
// distinguish different constraint types to build up
switch (Constr->Type) {
case Block:
case Horizontal: // write the new position of the Horizontal constraint Same as
// vertical position.
case Vertical: // write the new position of the Vertical constraint
{
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
bool alignment = Constr->Type != Block && Constr->Second != GeoEnum::GeoUndef;
// get the geometry
const Part::Geometry* geo = geolistfacade.getGeometryFromGeoId(Constr->First);
if (!alignment) {
// Vertical & Horiz can only be a GeomLineSegment, but Blocked can be
// anything.
Base::Vector3d midpos;
Base::Vector3d dir;
Base::Vector3d norm;
if (geo->is<Part::GeomLineSegment>()) {
const Part::GeomLineSegment* lineSeg =
static_cast<const Part::GeomLineSegment*>(geo);
// calculate the half distance between the start and endpoint
midpos = ((lineSeg->getEndPoint() + lineSeg->getStartPoint()) / 2);
// Get a set of vectors perpendicular and tangential to these
dir = (lineSeg->getEndPoint() - lineSeg->getStartPoint()).Normalize();
norm = Base::Vector3d(-dir.y, dir.x, 0);
}
else if (geo->is<Part::GeomBSplineCurve>()) {
const Part::GeomBSplineCurve* bsp =
static_cast<const Part::GeomBSplineCurve*>(geo);
midpos = Base::Vector3d(0, 0, 0);
std::vector<Base::Vector3d> poles = bsp->getPoles();
// Move center of gravity towards start not to collide with bspline
// degree information.
double ws = 1.0 / poles.size();
double w = 1.0;
for (std::vector<Base::Vector3d>::iterator it = poles.begin();
it != poles.end();
++it) {
midpos += w * (*it);
w -= ws;
}
midpos /= poles.size();
dir = (bsp->getEndPoint() - bsp->getStartPoint()).Normalize();
norm = Base::Vector3d(-dir.y, dir.x, 0);
}
else {
double ra = 0, rb = 0;
double angle, // rotation of object as a whole
angleplus = 0.; // arc angle (t parameter for ellipses)
if (geo->is<Part::GeomCircle>()) {
const Part::GeomCircle* circle =
static_cast<const Part::GeomCircle*>(geo);
ra = circle->getRadius();
angle = pi / 4;
midpos = circle->getCenter();
}
else if (geo->is<Part::GeomArcOfCircle>()) {
const Part::GeomArcOfCircle* arc =
static_cast<const Part::GeomArcOfCircle*>(geo);
ra = arc->getRadius();
double startangle, endangle;
arc->getRange(startangle, endangle, /*emulateCCW=*/true);
angle = (startangle + endangle) / 2;
midpos = arc->getCenter();
}
else if (geo->is<Part::GeomEllipse>()) {
const Part::GeomEllipse* ellipse =
static_cast<const Part::GeomEllipse*>(geo);
ra = ellipse->getMajorRadius();
rb = ellipse->getMinorRadius();
Base::Vector3d majdir = ellipse->getMajorAxisDir();
angle = atan2(majdir.y, majdir.x);
angleplus = pi / 4;
midpos = ellipse->getCenter();
}
else if (geo->is<Part::GeomArcOfEllipse>()) {
const Part::GeomArcOfEllipse* aoe =
static_cast<const Part::GeomArcOfEllipse*>(geo);
ra = aoe->getMajorRadius();
rb = aoe->getMinorRadius();
double startangle, endangle;
aoe->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = aoe->getMajorAxisDir();
angle = atan2(majdir.y, majdir.x);
angleplus = (startangle + endangle) / 2;
midpos = aoe->getCenter();
}
else if (geo->is<Part::GeomArcOfHyperbola>()) {
const Part::GeomArcOfHyperbola* aoh =
static_cast<const Part::GeomArcOfHyperbola*>(geo);
ra = aoh->getMajorRadius();
rb = aoh->getMinorRadius();
double startangle, endangle;
aoh->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = aoh->getMajorAxisDir();
angle = atan2(majdir.y, majdir.x);
angleplus = (startangle + endangle) / 2;
midpos = aoh->getCenter();
}
else if (geo->is<Part::GeomArcOfParabola>()) {
const Part::GeomArcOfParabola* aop =
static_cast<const Part::GeomArcOfParabola*>(geo);
ra = aop->getFocal();
double startangle, endangle;
aop->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = -aop->getXAxisDir();
angle = atan2(majdir.y, majdir.x);
angleplus = (startangle + endangle) / 2;
midpos = aop->getFocus();
}
else {
break;
}
if (geo->is<Part::GeomEllipse>() || geo->is<Part::GeomArcOfEllipse>()
|| geo->is<Part::GeomArcOfHyperbola>()) {
Base::Vector3d majDir, minDir, rvec;
majDir = Base::Vector3d(cos(angle),
sin(angle),
0); // direction of major axis of ellipse
minDir = Base::Vector3d(-majDir.y,
majDir.x,
0); // direction of minor axis of ellipse
rvec =
(ra * cos(angleplus)) * majDir + (rb * sin(angleplus)) * minDir;
midpos += rvec;
rvec.Normalize();
norm = rvec;
dir = Base::Vector3d(
-rvec.y,
rvec.x,
0); // DeepSOIC: I'm not sure what dir is supposed to mean.
}
else {
norm = Base::Vector3d(cos(angle), sin(angle), 0);
dir = Base::Vector3d(-norm.y, norm.x, 0);
midpos += ra * norm;
}
}
Base::Vector3d relpos = seekConstraintPosition(
midpos,
norm,
dir,
2.5,
editModeScenegraphNodes.constrGroup->getChild(i));
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->abPos =
SbVec3f(midpos.x, midpos.y, zConstrH); // Absolute Reference
// Reference Position that is scaled according to zoom
translation->translation = SbVec3f(relpos.x, relpos.y, 0);
}
else {
assert(Constr->Second >= -extGeoCount && Constr->Second < intGeoCount);
assert(Constr->FirstPos != Sketcher::PointPos::none
&& Constr->SecondPos != Sketcher::PointPos::none);
Base::Vector3d midpos1, dir1, norm1;
Base::Vector3d midpos2, dir2, norm2;
midpos1 = geolistfacade.getPoint(Constr->First, Constr->FirstPos);
midpos2 = geolistfacade.getPoint(Constr->Second, Constr->SecondPos);
dir1 = (midpos2 - midpos1).Normalize();
dir2 = -dir1;
norm1 = Base::Vector3d(-dir1.y, dir1.x, 0.);
norm2 = norm1;
Base::Vector3d relpos1 = seekConstraintPosition(
midpos1,
norm1,
dir1,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->abPos = SbVec3f(midpos1.x, midpos1.y, zConstrH);
translation->translation = SbVec3f(relpos1.x, relpos1.y, 0);
Base::Vector3d relpos2 = seekConstraintPosition(
midpos2,
norm2,
dir2,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
Base::Vector3d secondPos = midpos2 - midpos1;
translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::SecondTranslationIndex)));
translation->abPos = SbVec3f(secondPos.x, secondPos.y, zConstrH);
translation->translation =
SbVec3f(relpos2.x - relpos1.x, relpos2.y - relpos1.y, 0);
}
} break;
case Perpendicular: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
assert(Constr->Second >= -extGeoCount && Constr->Second < intGeoCount);
// get the geometry
const Part::Geometry* geo1 = geolistfacade.getGeometryFromGeoId(Constr->First);
const Part::Geometry* geo2 = geolistfacade.getGeometryFromGeoId(Constr->Second);
Base::Vector3d midpos1, dir1, norm1;
Base::Vector3d midpos2, dir2, norm2;
bool twoIcons = false; // a very local flag. It's set to true to indicate that
// the second dir+norm are valid and should be used
if (Constr->Third != GeoEnum::GeoUndef || // perpty via point
Constr->FirstPos
!= Sketcher::PointPos::none) { // endpoint-to-curve or
// endpoint-to-endpoint perpty
int ptGeoId;
Sketcher::PointPos ptPosId;
do { // dummy loop to use break =) Maybe goto?
ptGeoId = Constr->First;
ptPosId = Constr->FirstPos;
if (ptPosId != Sketcher::PointPos::none) {
break;
}
ptGeoId = Constr->Second;
ptPosId = Constr->SecondPos;
if (ptPosId != Sketcher::PointPos::none) {
break;
}
ptGeoId = Constr->Third;
ptPosId = Constr->ThirdPos;
if (ptPosId != Sketcher::PointPos::none) {
break;
}
assert(0); // no point found!
} while (false);
midpos1 = geolistfacade.getPoint(ptGeoId, ptPosId);
norm1 = getNormal(geolistfacade, Constr->Second, midpos1);
// TODO: Check the method above. This was the old one making use of the
// solver.
// norm1 = getSolvedSketch().calculateNormalAtPoint(Constr->Second,
// midpos1.x, midpos1.y);
norm1.Normalize();
dir1 = norm1;
dir1.RotateZ(-pi / 2.0);
}
else if (Constr->FirstPos == Sketcher::PointPos::none) {
if (geo1->is<Part::GeomLineSegment>()) {
const Part::GeomLineSegment* lineSeg1 =
static_cast<const Part::GeomLineSegment*>(geo1);
midpos1 = ((lineSeg1->getEndPoint() + lineSeg1->getStartPoint()) / 2);
dir1 =
(lineSeg1->getEndPoint() - lineSeg1->getStartPoint()).Normalize();
norm1 = Base::Vector3d(-dir1.y, dir1.x, 0.);
}
else if (geo1->is<Part::GeomArcOfCircle>()) {
const Part::GeomArcOfCircle* arc =
static_cast<const Part::GeomArcOfCircle*>(geo1);
double startangle, endangle, midangle;
arc->getRange(startangle, endangle, /*emulateCCW=*/true);
midangle = (startangle + endangle) / 2;
norm1 = Base::Vector3d(cos(midangle), sin(midangle), 0);
dir1 = Base::Vector3d(-norm1.y, norm1.x, 0);
midpos1 = arc->getCenter() + arc->getRadius() * norm1;
}
else if (geo1->is<Part::GeomCircle>()) {
const Part::GeomCircle* circle =
static_cast<const Part::GeomCircle*>(geo1);
norm1 = Base::Vector3d(cos(pi / 4), sin(pi / 4), 0);
dir1 = Base::Vector3d(-norm1.y, norm1.x, 0);
midpos1 = circle->getCenter() + circle->getRadius() * norm1;
}
else {
break;
}
if (geo2->is<Part::GeomLineSegment>()) {
const Part::GeomLineSegment* lineSeg2 =
static_cast<const Part::GeomLineSegment*>(geo2);
midpos2 = ((lineSeg2->getEndPoint() + lineSeg2->getStartPoint()) / 2);
dir2 =
(lineSeg2->getEndPoint() - lineSeg2->getStartPoint()).Normalize();
norm2 = Base::Vector3d(-dir2.y, dir2.x, 0.);
}
else if (geo2->is<Part::GeomArcOfCircle>()) {
const Part::GeomArcOfCircle* arc =
static_cast<const Part::GeomArcOfCircle*>(geo2);
double startangle, endangle, midangle;
arc->getRange(startangle, endangle, /*emulateCCW=*/true);
midangle = (startangle + endangle) / 2;
norm2 = Base::Vector3d(cos(midangle), sin(midangle), 0);
dir2 = Base::Vector3d(-norm2.y, norm2.x, 0);
midpos2 = arc->getCenter() + arc->getRadius() * norm2;
}
else if (geo2->is<Part::GeomCircle>()) {
const Part::GeomCircle* circle =
static_cast<const Part::GeomCircle*>(geo2);
norm2 = Base::Vector3d(cos(pi / 4), sin(pi / 4), 0);
dir2 = Base::Vector3d(-norm2.y, norm2.x, 0);
midpos2 = circle->getCenter() + circle->getRadius() * norm2;
}
else {
break;
}
twoIcons = true;
}
Base::Vector3d relpos1 =
seekConstraintPosition(midpos1,
norm1,
dir1,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->abPos = SbVec3f(midpos1.x, midpos1.y, zConstrH);
translation->translation = SbVec3f(relpos1.x, relpos1.y, 0);
if (twoIcons) {
Base::Vector3d relpos2 = seekConstraintPosition(
midpos2,
norm2,
dir2,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
Base::Vector3d secondPos = midpos2 - midpos1;
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::SecondTranslationIndex)));
translation->abPos = SbVec3f(secondPos.x, secondPos.y, zConstrH);
translation->translation =
SbVec3f(relpos2.x - relpos1.x, relpos2.y - relpos1.y, 0);
}
} break;
case Parallel:
case Equal: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
assert(Constr->Second >= -extGeoCount && Constr->Second < intGeoCount);
// get the geometry
const Part::Geometry* geo1 = geolistfacade.getGeometryFromGeoId(Constr->First);
const Part::Geometry* geo2 = geolistfacade.getGeometryFromGeoId(Constr->Second);
Base::Vector3d midpos1, dir1, norm1;
Base::Vector3d midpos2, dir2, norm2;
if (!geo1->is<Part::GeomLineSegment>() || !geo2->is<Part::GeomLineSegment>()) {
if (Constr->Type == Equal) {
double r1a = 0, r1b = 0, r2a = 0, r2b = 0;
double angle1,
angle1plus = 0., angle2,
angle2plus =
0.; // angle1 = rotation of object as a whole; angle1plus = arc
// angle (t parameter for ellipses).
if (geo1->is<Part::GeomCircle>()) {
const Part::GeomCircle* circle =
static_cast<const Part::GeomCircle*>(geo1);
r1a = circle->getRadius();
angle1 = pi / 4;
midpos1 = circle->getCenter();
}
else if (geo1->is<Part::GeomArcOfCircle>()) {
const Part::GeomArcOfCircle* arc =
static_cast<const Part::GeomArcOfCircle*>(geo1);
r1a = arc->getRadius();
double startangle, endangle;
arc->getRange(startangle, endangle, /*emulateCCW=*/true);
angle1 = (startangle + endangle) / 2;
midpos1 = arc->getCenter();
}
else if (geo1->is<Part::GeomEllipse>()) {
const Part::GeomEllipse* ellipse =
static_cast<const Part::GeomEllipse*>(geo1);
r1a = ellipse->getMajorRadius();
r1b = ellipse->getMinorRadius();
Base::Vector3d majdir = ellipse->getMajorAxisDir();
angle1 = atan2(majdir.y, majdir.x);
angle1plus = pi / 4;
midpos1 = ellipse->getCenter();
}
else if (geo1->is<Part::GeomArcOfEllipse>()) {
const Part::GeomArcOfEllipse* aoe =
static_cast<const Part::GeomArcOfEllipse*>(geo1);
r1a = aoe->getMajorRadius();
r1b = aoe->getMinorRadius();
double startangle, endangle;
aoe->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = aoe->getMajorAxisDir();
angle1 = atan2(majdir.y, majdir.x);
angle1plus = (startangle + endangle) / 2;
midpos1 = aoe->getCenter();
}
else if (geo1->is<Part::GeomArcOfHyperbola>()) {
const Part::GeomArcOfHyperbola* aoh =
static_cast<const Part::GeomArcOfHyperbola*>(geo1);
r1a = aoh->getMajorRadius();
r1b = aoh->getMinorRadius();
double startangle, endangle;
aoh->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = aoh->getMajorAxisDir();
angle1 = atan2(majdir.y, majdir.x);
angle1plus = (startangle + endangle) / 2;
midpos1 = aoh->getCenter();
}
else if (geo1->is<Part::GeomArcOfParabola>()) {
const Part::GeomArcOfParabola* aop =
static_cast<const Part::GeomArcOfParabola*>(geo1);
r1a = aop->getFocal();
double startangle, endangle;
aop->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = -aop->getXAxisDir();
angle1 = atan2(majdir.y, majdir.x);
angle1plus = (startangle + endangle) / 2;
midpos1 = aop->getFocus();
}
else {
break;
}
if (geo2->is<Part::GeomCircle>()) {
const Part::GeomCircle* circle =
static_cast<const Part::GeomCircle*>(geo2);
r2a = circle->getRadius();
angle2 = pi / 4;
midpos2 = circle->getCenter();
}
else if (geo2->is<Part::GeomArcOfCircle>()) {
const Part::GeomArcOfCircle* arc =
static_cast<const Part::GeomArcOfCircle*>(geo2);
r2a = arc->getRadius();
double startangle, endangle;
arc->getRange(startangle, endangle, /*emulateCCW=*/true);
angle2 = (startangle + endangle) / 2;
midpos2 = arc->getCenter();
}
else if (geo2->is<Part::GeomEllipse>()) {
const Part::GeomEllipse* ellipse =
static_cast<const Part::GeomEllipse*>(geo2);
r2a = ellipse->getMajorRadius();
r2b = ellipse->getMinorRadius();
Base::Vector3d majdir = ellipse->getMajorAxisDir();
angle2 = atan2(majdir.y, majdir.x);
angle2plus = pi / 4;
midpos2 = ellipse->getCenter();
}
else if (geo2->is<Part::GeomArcOfEllipse>()) {
const Part::GeomArcOfEllipse* aoe =
static_cast<const Part::GeomArcOfEllipse*>(geo2);
r2a = aoe->getMajorRadius();
r2b = aoe->getMinorRadius();
double startangle, endangle;
aoe->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = aoe->getMajorAxisDir();
angle2 = atan2(majdir.y, majdir.x);
angle2plus = (startangle + endangle) / 2;
midpos2 = aoe->getCenter();
}
else if (geo2->is<Part::GeomArcOfHyperbola>()) {
const Part::GeomArcOfHyperbola* aoh =
static_cast<const Part::GeomArcOfHyperbola*>(geo2);
r2a = aoh->getMajorRadius();
r2b = aoh->getMinorRadius();
double startangle, endangle;
aoh->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = aoh->getMajorAxisDir();
angle2 = atan2(majdir.y, majdir.x);
angle2plus = (startangle + endangle) / 2;
midpos2 = aoh->getCenter();
}
else if (geo2->is<Part::GeomArcOfParabola>()) {
const Part::GeomArcOfParabola* aop =
static_cast<const Part::GeomArcOfParabola*>(geo2);
r2a = aop->getFocal();
double startangle, endangle;
aop->getRange(startangle, endangle, /*emulateCCW=*/true);
Base::Vector3d majdir = -aop->getXAxisDir();
angle2 = atan2(majdir.y, majdir.x);
angle2plus = (startangle + endangle) / 2;
midpos2 = aop->getFocus();
}
else {
break;
}
if (geo1->is<Part::GeomEllipse>() || geo1->is<Part::GeomArcOfEllipse>()
|| geo1->is<Part::GeomArcOfHyperbola>()) {
Base::Vector3d majDir, minDir, rvec;
majDir = Base::Vector3d(cos(angle1),
sin(angle1),
0); // direction of major axis of ellipse
minDir = Base::Vector3d(-majDir.y,
majDir.x,
0); // direction of minor axis of ellipse
rvec = (r1a * cos(angle1plus)) * majDir
+ (r1b * sin(angle1plus)) * minDir;
midpos1 += rvec;
rvec.Normalize();
norm1 = rvec;
dir1 = Base::Vector3d(
-rvec.y,
rvec.x,
0); // DeepSOIC: I'm not sure what dir is supposed to mean.
}
else {
norm1 = Base::Vector3d(cos(angle1), sin(angle1), 0);
dir1 = Base::Vector3d(-norm1.y, norm1.x, 0);
midpos1 += r1a * norm1;
}
if (geo2->is<Part::GeomEllipse>() || geo2->is<Part::GeomArcOfEllipse>()
|| geo2->is<Part::GeomArcOfHyperbola>()) {
Base::Vector3d majDir, minDir, rvec;
majDir = Base::Vector3d(cos(angle2),
sin(angle2),
0); // direction of major axis of ellipse
minDir = Base::Vector3d(-majDir.y,
majDir.x,
0); // direction of minor axis of ellipse
rvec = (r2a * cos(angle2plus)) * majDir
+ (r2b * sin(angle2plus)) * minDir;
midpos2 += rvec;
rvec.Normalize();
norm2 = rvec;
dir2 = Base::Vector3d(-rvec.y, rvec.x, 0);
}
else {
norm2 = Base::Vector3d(cos(angle2), sin(angle2), 0);
dir2 = Base::Vector3d(-norm2.y, norm2.x, 0);
midpos2 += r2a * norm2;
}
}
else { // Parallel can only apply to a GeomLineSegment
break;
}
}
else {
const Part::GeomLineSegment* lineSeg1 =
static_cast<const Part::GeomLineSegment*>(geo1);
const Part::GeomLineSegment* lineSeg2 =
static_cast<const Part::GeomLineSegment*>(geo2);
// calculate the half distance between the start and endpoint
midpos1 = ((lineSeg1->getEndPoint() + lineSeg1->getStartPoint()) / 2);
midpos2 = ((lineSeg2->getEndPoint() + lineSeg2->getStartPoint()) / 2);
// Get a set of vectors perpendicular and tangential to these
dir1 = (lineSeg1->getEndPoint() - lineSeg1->getStartPoint()).Normalize();
dir2 = (lineSeg2->getEndPoint() - lineSeg2->getStartPoint()).Normalize();
norm1 = Base::Vector3d(-dir1.y, dir1.x, 0.);
norm2 = Base::Vector3d(-dir2.y, dir2.x, 0.);
}
Base::Vector3d relpos1 =
seekConstraintPosition(midpos1,
norm1,
dir1,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
Base::Vector3d relpos2 =
seekConstraintPosition(midpos2,
norm2,
dir2,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->abPos =
SbVec3f(midpos1.x, midpos1.y, zConstrH); // Absolute Reference
// Reference Position that is scaled according to zoom
translation->translation = SbVec3f(relpos1.x, relpos1.y, 0);
Base::Vector3d secondPos = midpos2 - midpos1;
translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::SecondTranslationIndex)));
translation->abPos =
SbVec3f(secondPos.x, secondPos.y, zConstrH); // Absolute Reference
// Reference Position that is scaled according to zoom
translation->translation =
SbVec3f(relpos2.x - relpos1.x, relpos2.y - relpos1.y, 0);
} break;
case Distance:
case DistanceX:
case DistanceY: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
double helperStartAngle1 = 0.; // for arc helpers
double helperStartAngle2 = 0.;
double helperRange1 = 0.;
double helperRange2 = 0.;
double radius1 = 0.;
double radius2 = 0.;
Base::Vector3d center1(0., 0., 0.);
Base::Vector3d center2(0., 0., 0.);
int numPoints = 2;
// pnt1 will be initialized to (0,0,0) if only one point is given
auto pnt1 = geolistfacade.getPoint(Constr->First, Constr->FirstPos);
Base::Vector3d pnt2(0., 0., 0.);
if (Constr->SecondPos != Sketcher::PointPos::none) {
// point to point distance
pnt2 = geolistfacade.getPoint(Constr->Second, Constr->SecondPos);
}
else if (Constr->Second != GeoEnum::GeoUndef) {
auto geo1 = geolistfacade.getGeometryFromGeoId(Constr->First);
auto geo2 = geolistfacade.getGeometryFromGeoId(Constr->Second);
if (isLineSegment(*geo2)) {
// NOLINTNEXTLINE
auto lineSeg = static_cast<const Part::GeomLineSegment*>(geo2);
Base::Vector3d l2p1 = lineSeg->getStartPoint();
Base::Vector3d l2p2 = lineSeg->getEndPoint();
if (Constr->FirstPos != Sketcher::PointPos::none) {
// point to line distance
// calculate the projection of p1 onto lineSeg
pnt2.ProjectToLine(pnt1 - l2p1, l2p2 - l2p1);
pnt2 += pnt1;
}
else if (isCircleOrArc(*geo1)) {
// circular to line distance
auto [radius, ct] = getRadiusCenterCircleArc(geo1);
// project the center on the line (translated to origin)
pnt1.ProjectToLine(ct - l2p1, l2p2 - l2p1);
Base::Vector3d dir = pnt1;
dir.Normalize();
pnt1 += ct;
pnt2 = ct + dir * radius;
}
}
else if (isCircleOrArc(*geo2)) {
if (Constr->FirstPos != Sketcher::PointPos::none) {
// point to circular distance
auto [rad, ct] = getRadiusCenterCircleArc(geo2);
Base::Vector3d v = pnt1 - ct;
v = v.Normalize();
pnt2 = ct + rad * v;
}
else if (isCircleOrArc(*geo1)) {
// circular to circular distance
GetCirclesMinimalDistance(geo1, geo2, pnt1, pnt2);
}
}
else {
break;
}
}
else if (Constr->FirstPos != Sketcher::PointPos::none) {
// one point distance
pnt1 = Base::Vector3d(0., 0., 0.);
pnt2 = geolistfacade.getPoint(Constr->First, Constr->FirstPos);
}
else if (Constr->First != GeoEnum::GeoUndef) {
auto geo = geolistfacade.getGeometryFromGeoId(Constr->First);
if (isLineSegment(*geo)) {
// segment distance
auto lineSeg = static_cast<const Part::GeomLineSegment*>(geo);
pnt1 = lineSeg->getStartPoint();
pnt2 = lineSeg->getEndPoint();
}
else if (isArcOfCircle(*geo)) {
// arc length
auto arc = static_cast<const Part::GeomArcOfCircle*>(geo);
int index = static_cast<int>(ConstraintNodePosition::DatumLabelIndex);
auto* asciiText = static_cast<SoDatumLabel*>(sep->getChild(index));
center1 = arc->getCenter();
pnt1 = arc->getStartPoint();
pnt2 = arc->getEndPoint();
double startAngle, endAngle;
arc->getRange(startAngle, endAngle, /*emulateCCW=*/false);
asciiText->datumtype = SoDatumLabel::ARCLENGTH;
asciiText->param1 = Constr->LabelDistance;
asciiText->string =
SbString(getPresentationString(Constr, "").toUtf8().constData());
asciiText->pnts.setNum(3);
SbVec3f* verts = asciiText->pnts.startEditing();
verts[0] = SbVec3f(center1.x, center1.y, center1.z);
verts[1] = SbVec3f(pnt1.x, pnt1.y, pnt1.z);
verts[2] = SbVec3f(pnt2.x, pnt2.y, pnt2.z);
asciiText->pnts.finishEditing();
break;
}
else {
break;
}
}
else {
break;
}
int index = static_cast<int>(ConstraintNodePosition::DatumLabelIndex);
auto* asciiText = static_cast<SoDatumLabel*>(sep->getChild(index)); // NOLINT
// Get presentation string (w/o units if option is set)
asciiText->string =
SbString(getPresentationString(Constr).toUtf8().constData());
if (Constr->Type == Distance) {
asciiText->datumtype = SoDatumLabel::DISTANCE;
}
else if (Constr->Type == DistanceX) {
asciiText->datumtype = SoDatumLabel::DISTANCEX;
}
else if (Constr->Type == DistanceY) {
asciiText->datumtype = SoDatumLabel::DISTANCEY;
}
// Check if arc helpers are needed
if (Constr->Second != GeoEnum::GeoUndef) {
auto geo1 = geolistfacade.getGeometryFromGeoId(Constr->First);
auto geo2 = geolistfacade.getGeometryFromGeoId(Constr->Second);
if (isArcOfCircle(*geo1) && Constr->FirstPos == Sketcher::PointPos::none) {
auto arc = static_cast<const Part::GeomArcOfCircle*>(geo1); // NOLINT
radius1 = arc->getRadius();
center1 = arc->getCenter();
double angle =
toVector2d(isLineSegment(*geo2) ? pnt2 - center1 : pnt1 - center1)
.Angle();
double startAngle, endAngle;
arc->getRange(startAngle, endAngle, /*emulateCCW=*/true);
findHelperAngles(helperStartAngle1,
helperRange1,
angle,
startAngle,
endAngle);
if (helperRange1 != 0.) {
// We override to draw the full helper as it does not look good
// otherwise We still use findHelperAngles before to find if helper
// is needed.
helperStartAngle1 = endAngle;
helperRange1 = 2 * pi - (endAngle - startAngle);
numPoints++;
}
}
if (isArcOfCircle(*geo2) && Constr->SecondPos == Sketcher::PointPos::none) {
auto arc = static_cast<const Part::GeomArcOfCircle*>(geo2); // NOLINT
radius2 = arc->getRadius();
center2 = arc->getCenter();
double angle =
toVector2d(pnt2 - center2).Angle(); // between -pi and pi
double startAngle, endAngle; // between 0 and 2*pi
arc->getRange(startAngle, endAngle, /*emulateCCW=*/true);
findHelperAngles(helperStartAngle2,
helperRange2,
angle,
startAngle,
endAngle);
if (helperRange2 != 0.) {
helperStartAngle2 = endAngle;
helperRange2 = 2 * pi - (endAngle - startAngle);
numPoints++;
}
}
}
// Assign the Datum Points
asciiText->pnts.setNum(numPoints);
SbVec3f* verts = asciiText->pnts.startEditing();
verts[0] = SbVec3f(pnt1.x, pnt1.y, zConstrH);
verts[1] = SbVec3f(pnt2.x, pnt2.y, zConstrH);
if (numPoints > 2) {
if (helperRange1 != 0.) {
verts[2] = SbVec3f(center1.x, center1.y, zConstrH);
asciiText->param3 = helperStartAngle1;
asciiText->param4 = helperRange1;
asciiText->param5 = radius1;
}
else {
verts[2] = SbVec3f(center2.x, center2.y, zConstrH);
asciiText->param3 = helperStartAngle2;
asciiText->param4 = helperRange2;
asciiText->param5 = radius2;
}
if (numPoints > 3) {
verts[3] = SbVec3f(center2.x, center2.y, zConstrH);
asciiText->param6 = helperStartAngle2;
asciiText->param7 = helperRange2;
asciiText->param8 = radius2;
}
else {
asciiText->param6 = 0.;
asciiText->param7 = 0.;
asciiText->param8 = 0.;
}
}
else {
asciiText->param3 = 0.;
asciiText->param4 = 0.;
asciiText->param5 = 0.;
}
asciiText->pnts.finishEditing();
// Assign the Label Distance
asciiText->param1 = Constr->LabelDistance;
asciiText->param2 = Constr->LabelPosition;
} break;
case PointOnObject:
case Tangent:
case SnellsLaw: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
assert(Constr->Second >= -extGeoCount && Constr->Second < intGeoCount);
Base::Vector3d pos, relPos;
if (Constr->Type == PointOnObject || Constr->Type == SnellsLaw
|| (Constr->Type == Tangent && Constr->Third != GeoEnum::GeoUndef)
|| // Tangency via point
(Constr->Type == Tangent
&& Constr->FirstPos
!= Sketcher::PointPos::none) // endpoint-to-curve or
// endpoint-to-endpoint tangency
) {
// find the point of tangency/point that is on object
// just any point among first/second/third should be OK
int ptGeoId;
Sketcher::PointPos ptPosId;
do { // dummy loop to use break =) Maybe goto?
ptGeoId = Constr->First;
ptPosId = Constr->FirstPos;
if (ptPosId != Sketcher::PointPos::none) {
break;
}
ptGeoId = Constr->Second;
ptPosId = Constr->SecondPos;
if (ptPosId != Sketcher::PointPos::none) {
break;
}
ptGeoId = Constr->Third;
ptPosId = Constr->ThirdPos;
if (ptPosId != Sketcher::PointPos::none) {
break;
}
assert(0); // no point found!
} while (false);
pos = geolistfacade.getPoint(ptGeoId, ptPosId);
auto norm = getNormal(geolistfacade, Constr->Second, pos);
// TODO: Check substitution
// Base::Vector3d norm =
// getSolvedSketch().calculateNormalAtPoint(Constr->Second, pos.x, pos.y);
norm.Normalize();
Base::Vector3d dir = norm;
dir.RotateZ(-pi / 2.0);
relPos = seekConstraintPosition(
pos,
norm,
dir,
2.5,
editModeScenegraphNodes.constrGroup->getChild(i));
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->abPos = SbVec3f(pos.x, pos.y, zConstrH); // Absolute Reference
translation->translation = SbVec3f(relPos.x, relPos.y, 0);
}
else if (Constr->Type == Tangent) {
// get the geometry
const Part::Geometry* geo1 =
geolistfacade.getGeometryFromGeoId(Constr->First);
const Part::Geometry* geo2 =
geolistfacade.getGeometryFromGeoId(Constr->Second);
if (geo1->is<Part::GeomLineSegment>()
&& geo2->is<Part::GeomLineSegment>()) {
const Part::GeomLineSegment* lineSeg1 =
static_cast<const Part::GeomLineSegment*>(geo1);
const Part::GeomLineSegment* lineSeg2 =
static_cast<const Part::GeomLineSegment*>(geo2);
// tangency between two lines
Base::Vector3d midpos1 =
((lineSeg1->getEndPoint() + lineSeg1->getStartPoint()) / 2);
Base::Vector3d midpos2 =
((lineSeg2->getEndPoint() + lineSeg2->getStartPoint()) / 2);
Base::Vector3d dir1 =
(lineSeg1->getEndPoint() - lineSeg1->getStartPoint()).Normalize();
Base::Vector3d dir2 =
(lineSeg2->getEndPoint() - lineSeg2->getStartPoint()).Normalize();
Base::Vector3d norm1 = Base::Vector3d(-dir1.y, dir1.x, 0.f);
Base::Vector3d norm2 = Base::Vector3d(-dir2.y, dir2.x, 0.f);
Base::Vector3d relpos1 = seekConstraintPosition(
midpos1,
norm1,
dir1,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
Base::Vector3d relpos2 = seekConstraintPosition(
midpos2,
norm2,
dir2,
4.0,
editModeScenegraphNodes.constrGroup->getChild(i));
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->abPos =
SbVec3f(midpos1.x, midpos1.y, zConstrH); // Absolute Reference
translation->translation = SbVec3f(relpos1.x, relpos1.y, 0);
Base::Vector3d secondPos = midpos2 - midpos1;
translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::SecondTranslationIndex)));
translation->abPos = SbVec3f(secondPos.x,
secondPos.y,
zConstrH); // Absolute Reference
translation->translation =
SbVec3f(relpos2.x - relpos1.x, relpos2.y - relpos1.y, 0);
break;
}
else if (geo2->is<Part::GeomLineSegment>()) {
std::swap(geo1, geo2);
}
if (geo1->is<Part::GeomLineSegment>()) {
const Part::GeomLineSegment* lineSeg =
static_cast<const Part::GeomLineSegment*>(geo1);
Base::Vector3d dir =
(lineSeg->getEndPoint() - lineSeg->getStartPoint()).Normalize();
Base::Vector3d norm(-dir.y, dir.x, 0);
if (geo2->is<Part::GeomCircle>()) {
const Part::GeomCircle* circle =
static_cast<const Part::GeomCircle*>(geo2);
// tangency between a line and a circle
float length =
(circle->getCenter() - lineSeg->getStartPoint()) * dir;
pos = lineSeg->getStartPoint() + dir * length;
relPos = norm * 1; // TODO Huh?
}
else if (geo2->is<Part::GeomEllipse>()
|| geo2->is<Part::GeomArcOfEllipse>()) {
Base::Vector3d center;
if (geo2->is<Part::GeomEllipse>()) {
const Part::GeomEllipse* ellipse =
static_cast<const Part::GeomEllipse*>(geo2);
center = ellipse->getCenter();
}
else {
const Part::GeomArcOfEllipse* aoc =
static_cast<const Part::GeomArcOfEllipse*>(geo2);
center = aoc->getCenter();
}
// tangency between a line and an ellipse
float length = (center - lineSeg->getStartPoint()) * dir;
pos = lineSeg->getStartPoint() + dir * length;
relPos = norm * 1;
}
else if (geo2->is<Part::GeomArcOfCircle>()) {
const Part::GeomArcOfCircle* arc =
static_cast<const Part::GeomArcOfCircle*>(geo2);
// tangency between a line and an arc
float length = (arc->getCenter() - lineSeg->getStartPoint()) * dir;
pos = lineSeg->getStartPoint() + dir * length;
relPos = norm * 1; // TODO Huh?
}
}
if (geo1->is<Part::GeomCircle>() && geo2->is<Part::GeomCircle>()) {
const Part::GeomCircle* circle1 =
static_cast<const Part::GeomCircle*>(geo1);
const Part::GeomCircle* circle2 =
static_cast<const Part::GeomCircle*>(geo2);
// tangency between two circles
Base::Vector3d dir =
(circle2->getCenter() - circle1->getCenter()).Normalize();
pos = circle1->getCenter() + dir * circle1->getRadius();
relPos = dir * 1;
}
else if (geo2->is<Part::GeomCircle>()) {
std::swap(geo1, geo2);
}
if (geo1->is<Part::GeomCircle>() && geo2->is<Part::GeomArcOfCircle>()) {
const Part::GeomCircle* circle =
static_cast<const Part::GeomCircle*>(geo1);
const Part::GeomArcOfCircle* arc =
static_cast<const Part::GeomArcOfCircle*>(geo2);
// tangency between a circle and an arc
Base::Vector3d dir =
(arc->getCenter() - circle->getCenter()).Normalize();
pos = circle->getCenter() + dir * circle->getRadius();
relPos = dir * 1;
}
else if (geo1->is<Part::GeomArcOfCircle>()
&& geo2->is<Part::GeomArcOfCircle>()) {
const Part::GeomArcOfCircle* arc1 =
static_cast<const Part::GeomArcOfCircle*>(geo1);
const Part::GeomArcOfCircle* arc2 =
static_cast<const Part::GeomArcOfCircle*>(geo2);
// tangency between two arcs
Base::Vector3d dir =
(arc2->getCenter() - arc1->getCenter()).Normalize();
pos = arc1->getCenter() + dir * arc1->getRadius();
relPos = dir * 1;
}
auto translation = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->abPos = SbVec3f(pos.x, pos.y, zConstrH); // Absolute Reference
translation->translation = SbVec3f(relPos.x, relPos.y, 0);
}
} break;
case Symmetric: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
assert(Constr->Second >= -extGeoCount && Constr->Second < intGeoCount);
Base::Vector3d pnt1 = geolistfacade.getPoint(Constr->First, Constr->FirstPos);
Base::Vector3d pnt2 = geolistfacade.getPoint(Constr->Second, Constr->SecondPos);
SbVec3f p1(pnt1.x, pnt1.y, zConstrH);
SbVec3f p2(pnt2.x, pnt2.y, zConstrH);
SbVec3f dir = (p2 - p1);
dir.normalize();
SbVec3f norm(-dir[1], dir[0], 0);
SoDatumLabel* asciiText = static_cast<SoDatumLabel*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::DatumLabelIndex)));
asciiText->datumtype = SoDatumLabel::SYMMETRIC;
asciiText->pnts.setNum(2);
SbVec3f* verts = asciiText->pnts.startEditing();
verts[0] = p1;
verts[1] = p2;
asciiText->pnts.finishEditing();
auto translation = static_cast<SoTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
translation->translation = (p1 + p2) / 2;
} break;
case Angle: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
assert((Constr->Second >= -extGeoCount && Constr->Second < intGeoCount)
|| Constr->Second == GeoEnum::GeoUndef);
SbVec3f p0;
double distance = Constr->LabelDistance;
double startangle, range;
double endLineLength1 = 0.0;
double endLineLength2 = 0.0;
if (Constr->Second != GeoEnum::GeoUndef) {
Base::Vector3d dir1, dir2;
if (Constr->Third == GeoEnum::GeoUndef) { // angle between two lines
const Part::Geometry* geo1 =
geolistfacade.getGeometryFromGeoId(Constr->First);
const Part::Geometry* geo2 =
geolistfacade.getGeometryFromGeoId(Constr->Second);
if (!isLineSegment(*geo1) || !isLineSegment(*geo2)) {
break;
}
auto* line1 = static_cast<const Part::GeomLineSegment*>(geo1);
auto* line2 = static_cast<const Part::GeomLineSegment*>(geo2);
bool flip1 = (Constr->FirstPos == PointPos::end);
bool flip2 = (Constr->SecondPos == PointPos::end);
dir1 = (flip1 ? -1. : 1.)
* (line1->getEndPoint() - line1->getStartPoint()).Normalize();
dir2 = (flip2 ? -1. : 1.)
* (line2->getEndPoint() - line2->getStartPoint()).Normalize();
Base::Vector3d pnt1 =
flip1 ? line1->getEndPoint() : line1->getStartPoint();
Base::Vector3d pnt2 =
flip2 ? line2->getEndPoint() : line2->getStartPoint();
Base::Vector3d pnt12 =
flip1 ? line1->getStartPoint() : line1->getEndPoint();
Base::Vector3d pnt22 =
flip2 ? line2->getStartPoint() : line2->getEndPoint();
// line-line intersection
Base::Vector3d intersection;
{
double det = dir1.x * dir2.y - dir1.y * dir2.x;
if ((det > 0 ? det : -det) < 1e-10) {
// lines are coincident (or parallel) and in this case the
// center of the point pairs with the shortest distance is
// used
Base::Vector3d p1[2], p2[2];
p1[0] = line1->getStartPoint();
p1[1] = line1->getEndPoint();
p2[0] = line2->getStartPoint();
p2[1] = line2->getEndPoint();
double length = std::numeric_limits<double>::max();
for (int i = 0; i <= 1; i++) {
for (int j = 0; j <= 1; j++) {
double tmp = (p2[j] - p1[i]).Length();
if (tmp < length) {
length = tmp;
double x = (p2[j].x + p1[i].x) / 2;
double y = (p2[j].y + p1[i].y) / 2;
intersection = Base::Vector3d(x, y, 0.);
}
}
}
}
else {
double c1 = dir1.y * pnt1.x - dir1.x * pnt1.y;
double c2 = dir2.y * pnt2.x - dir2.x * pnt2.y;
double x = (dir1.x * c2 - dir2.x * c1) / det;
double y = (dir1.y * c2 - dir2.y * c1) / det;
intersection = Base::Vector3d(x, y, 0.);
}
}
p0.setValue(intersection.x, intersection.y, 0.);
range = Constr->getValue(); // WYSIWYG
startangle = atan2(dir1.y, dir1.x);
Base::Vector3d vl1 = dir1 * 2 * distance - (pnt1 - intersection);
Base::Vector3d vl2 = dir2 * 2 * distance - (pnt2 - intersection);
Base::Vector3d vl12 = dir1 * 2 * distance - (pnt12 - intersection);
Base::Vector3d vl22 = dir2 * 2 * distance - (pnt22 - intersection);
endLineLength1 = vl12.Dot(dir1) > 0 ? vl12.Length()
: vl1.Dot(dir1) < 0 ? -vl1.Length()
: 0.0;
endLineLength2 = vl22.Dot(dir2) > 0 ? vl22.Length()
: vl2.Dot(dir2) < 0 ? -vl2.Length()
: 0.0;
}
else { // angle-via-point
Base::Vector3d p =
geolistfacade.getPoint(Constr->Third, Constr->ThirdPos);
p0 = SbVec3f(p.x, p.y, 0);
dir1 = getNormal(geolistfacade, Constr->First, p);
// TODO: Check
// dir1 = getSolvedSketch().calculateNormalAtPoint(Constr->First,
// p.x, p.y);
dir1.RotateZ(-pi / 2); // convert to vector of tangency by rotating
dir2 = getNormal(geolistfacade, Constr->Second, p);
// TODO: Check
// dir2 = getSolvedSketch().calculateNormalAtPoint(Constr->Second,
// p.x, p.y);
dir2.RotateZ(-pi / 2);
startangle = atan2(dir1.y, dir1.x);
range = atan2(dir1.x * dir2.y - dir1.y * dir2.x,
dir1.x * dir2.x + dir1.y * dir2.y);
}
}
else if (Constr->First != GeoEnum::GeoUndef) {
const Part::Geometry* geo =
geolistfacade.getGeometryFromGeoId(Constr->First);
if (geo->is<Part::GeomLineSegment>()) {
auto* lineSeg = static_cast<const Part::GeomLineSegment*>(geo);
p0 = Base::convertTo<SbVec3f>(
(lineSeg->getEndPoint() + lineSeg->getStartPoint()) / 2);
double l1 = 2 * distance
- (lineSeg->getEndPoint() - lineSeg->getStartPoint()).Length() / 2;
endLineLength1 = 2 * distance;
endLineLength2 = l1 > 0. ? l1 : 0.;
Base::Vector3d dir = lineSeg->getEndPoint() - lineSeg->getStartPoint();
startangle = 0.;
range = atan2(dir.y, dir.x);
}
else if (geo->is<Part::GeomArcOfCircle>()) {
auto* arc = static_cast<const Part::GeomArcOfCircle*>(geo);
p0 = Base::convertTo<SbVec3f>(arc->getCenter());
endLineLength1 = 2 * distance - arc->getRadius();
endLineLength2 = endLineLength1;
double endangle;
arc->getRange(startangle, endangle, /*emulateCCWXY=*/true);
range = endangle - startangle;
}
else {
break;
}
}
else {
break;
}
SoDatumLabel* asciiText = static_cast<SoDatumLabel*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::DatumLabelIndex)));
asciiText->string =
SbString(getPresentationString(Constr).toUtf8().constData());
asciiText->datumtype = SoDatumLabel::ANGLE;
asciiText->param1 = distance;
asciiText->param2 = startangle;
asciiText->param3 = range;
asciiText->param4 = endLineLength1;
asciiText->param5 = endLineLength2;
asciiText->pnts.setNum(2);
SbVec3f* verts = asciiText->pnts.startEditing();
verts[0] = p0;
asciiText->pnts.finishEditing();
} break;
case Diameter: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
Base::Vector3d pnt1(0., 0., 0.), pnt2(0., 0., 0.);
double startHelperAngle = 0.;
double startHelperRange = 0.;
double endHelperAngle = 0.;
double endHelperRange = 0.;
if (Constr->First == GeoEnum::GeoUndef) {
break;
}
const Part::Geometry* geo = geolistfacade.getGeometryFromGeoId(Constr->First);
if (geo->is<Part::GeomArcOfCircle>()) {
auto* arc = static_cast<const Part::GeomArcOfCircle*>(geo);
double radius = arc->getRadius();
double angle = (double)Constr->LabelPosition; // between -pi and pi
double startAngle, endAngle; // between 0 and 2*pi
arc->getRange(startAngle, endAngle, /*emulateCCW=*/true);
if (angle == 10) {
angle = (startAngle + endAngle) / 2;
}
findHelperAngles(startHelperAngle,
startHelperRange,
angle,
startAngle,
endAngle);
findHelperAngles(endHelperAngle,
endHelperRange,
angle + pi,
startAngle,
endAngle);
Base::Vector3d center = arc->getCenter();
pnt1 = center - radius * Base::Vector3d(cos(angle), sin(angle), 0.);
pnt2 = center + radius * Base::Vector3d(cos(angle), sin(angle), 0.);
}
else if (geo->is<Part::GeomCircle>()) {
auto* circle = static_cast<const Part::GeomCircle*>(geo);
double radius = circle->getRadius();
double angle = (double)Constr->LabelPosition;
if (angle == 10) {
angle = 0;
}
Base::Vector3d center = circle->getCenter();
pnt1 = center - radius * Base::Vector3d(cos(angle), sin(angle), 0.);
pnt2 = center + radius * Base::Vector3d(cos(angle), sin(angle), 0.);
}
else {
break;
}
SbVec3f p1(pnt1.x, pnt1.y, zConstrH);
SbVec3f p2(pnt2.x, pnt2.y, zConstrH);
SoDatumLabel* asciiText = static_cast<SoDatumLabel*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::DatumLabelIndex)));
// Get display string with units hidden if so requested
asciiText->string =
SbString(getPresentationString(Constr, "").toUtf8().constData());
asciiText->datumtype = SoDatumLabel::DIAMETER;
asciiText->param1 = Constr->LabelDistance;
asciiText->param2 = Constr->LabelPosition;
asciiText->param3 = static_cast<float>(startHelperAngle);
asciiText->param4 = static_cast<float>(startHelperRange);
asciiText->param5 = static_cast<float>(endHelperAngle);
asciiText->param6 = static_cast<float>(endHelperRange);
asciiText->pnts.setNum(2);
SbVec3f* verts = asciiText->pnts.startEditing();
verts[0] = p1;
verts[1] = p2;
asciiText->pnts.finishEditing();
} break;
case Weight:
case Radius: {
assert(Constr->First >= -extGeoCount && Constr->First < intGeoCount);
Base::Vector3d pnt1(0., 0., 0.), pnt2(0., 0., 0.);
double helperStartAngle = 0.;
double helperRange = 0.;
if (Constr->First == GeoEnum::GeoUndef) {
break;
}
const Part::Geometry* geo = geolistfacade.getGeometryFromGeoId(Constr->First);
if (geo->is<Part::GeomArcOfCircle>()) {
auto* arc = static_cast<const Part::GeomArcOfCircle*>(geo);
double radius = arc->getRadius();
double angle = (double)Constr->LabelPosition; // between -pi and pi
double startAngle, endAngle; // between 0 and 2*pi
arc->getRange(startAngle, endAngle, /*emulateCCW=*/true);
if (angle == 10) {
angle = (startAngle + endAngle) / 2;
}
findHelperAngles(helperStartAngle,
helperRange,
angle,
startAngle,
endAngle);
pnt1 = arc->getCenter();
pnt2 = pnt1 + radius * Base::Vector3d(cos(angle), sin(angle), 0.);
}
else if (geo->is<Part::GeomCircle>()) {
auto* circle = static_cast<const Part::GeomCircle*>(geo);
auto gf = GeometryFacade::getFacade(geo);
double radius;
radius = circle->getRadius();
double angle = (double)Constr->LabelPosition;
if (angle == 10) {
angle = 0;
}
pnt1 = circle->getCenter();
pnt2 = pnt1 + radius * Base::Vector3d(cos(angle), sin(angle), 0.);
}
else {
break;
}
SbVec3f p1(pnt1.x, pnt1.y, zConstrH);
SbVec3f p2(pnt2.x, pnt2.y, zConstrH);
SoDatumLabel* asciiText = static_cast<SoDatumLabel*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::DatumLabelIndex)));
// Get display string with units hidden if so requested
if (Constr->Type == Weight) {
asciiText->string =
SbString(QString::number(Constr->getValue()).toStdString().c_str());
}
else {
asciiText->string =
SbString(getPresentationString(Constr, "R").toUtf8().constData());
}
asciiText->datumtype = SoDatumLabel::RADIUS;
asciiText->param1 = Constr->LabelDistance;
asciiText->param2 = Constr->LabelPosition;
asciiText->param3 = helperStartAngle;
asciiText->param4 = helperRange;
asciiText->pnts.setNum(2);
SbVec3f* verts = asciiText->pnts.startEditing();
verts[0] = p1;
verts[1] = p2;
asciiText->pnts.finishEditing();
} break;
case Coincident: // nothing to do for coincident
case None:
case InternalAlignment:
case NumConstraintTypes:
break;
}
}
catch (Base::Exception& e) {
Base::Console().developerError("EditModeConstraintCoinManager",
"Exception during draw: %s\n",
e.what());
e.reportException();
}
catch (...) {
Base::Console().developerError("EditModeConstraintCoinManager",
"Exception during draw: unknown\n");
}
}
}
void EditModeConstraintCoinManager::findHelperAngles(double& helperStartAngle,
double& helperRange,
double angle,
double startAngle,
double endAngle)
{
using std::numbers::pi;
double margin = 0.2; // about 10deg
if (angle < 0) {
angle = angle + 2 * pi;
}
// endAngle can be more than 2*pi as its startAngle + arcAngle
if (endAngle > 2 * pi && angle < endAngle - 2 * pi) {
angle = angle + 2 * pi;
}
if (!(angle > startAngle && angle < endAngle)) {
if ((angle < startAngle && startAngle - angle < angle + 2 * pi - endAngle)
|| (angle > endAngle && startAngle + 2 * pi - angle < angle - endAngle)) {
if (angle > startAngle) {
angle -= 2 * pi;
}
helperStartAngle = angle - margin;
helperRange = startAngle - angle + margin;
}
else {
if (angle < endAngle) {
angle += 2 * pi;
}
helperStartAngle = endAngle;
helperRange = angle - endAngle + margin;
}
}
}
Base::Vector3d EditModeConstraintCoinManager::seekConstraintPosition(const Base::Vector3d& origPos,
const Base::Vector3d& norm,
const Base::Vector3d& dir,
float step,
const SoNode* constraint)
{
auto rp = ViewProviderSketchCoinAttorney::getRayPickAction(viewProvider);
float scaled_step = step * ViewProviderSketchCoinAttorney::getScaleFactor(viewProvider);
int multiplier = 0;
Base::Vector3d relPos, freePos;
bool isConstraintAtPosition = true;
while (isConstraintAtPosition && multiplier < 10) {
// Calculate new position of constraint
relPos = norm * 0.5f + dir * multiplier;
freePos = origPos + relPos * scaled_step;
// Prevent crash : https://forum.freecad.org/viewtopic.php?f=8&t=65305
if (!rp) {
return relPos * step;
}
rp->setRadius(0.1f);
rp->setPickAll(true);
rp->setRay(SbVec3f(freePos.x, freePos.y, -1.f), SbVec3f(0, 0, 1));
// problem
rp->apply(editModeScenegraphNodes.constrGroup); // We could narrow it down to just the
// SoGroup containing the constraints
// returns a copy of the point
SoPickedPoint* pp = rp->getPickedPoint();
const SoPickedPointList ppl = rp->getPickedPointList();
if (ppl.getLength() <= 1 && pp) {
SoPath* path = pp->getPath();
int length = path->getLength();
SoNode* tailFather1 = path->getNode(length - 2);
SoNode* tailFather2 = path->getNode(length - 3);
// checking if a constraint is the same as the one selected
if (tailFather1 == constraint || tailFather2 == constraint) {
isConstraintAtPosition = false;
}
}
else {
isConstraintAtPosition = false;
}
multiplier *= -1; // search in both sides
if (multiplier >= 0) {
multiplier++; // Increment the multiplier
}
}
if (multiplier == 10) {
relPos = norm * 0.5f; // no free position found
}
return relPos * step;
}
void EditModeConstraintCoinManager::updateConstraintColor(
const std::vector<Sketcher::Constraint*>& constraints)
{
// Because coincident constraints are selected using the point color, we need to edit the point
// materials.
std::vector<int> PtNum;
std::vector<SbColor*> pcolor; // point color
std::vector<std::vector<int>> CurvNum;
std::vector<std::vector<SbColor*>> color; // curve color
for (int l = 0; l < geometryLayerParameters.getCoinLayerCount(); l++) {
PtNum.push_back(editModeScenegraphNodes.PointsMaterials[l]->diffuseColor.getNum());
pcolor.push_back(editModeScenegraphNodes.PointsMaterials[l]->diffuseColor.startEditing());
CurvNum.emplace_back();
color.emplace_back();
for (int t = 0; t < geometryLayerParameters.getSubLayerCount(); t++) {
CurvNum[l].push_back(
editModeScenegraphNodes.CurvesMaterials[l][t]->diffuseColor.getNum());
color[l].push_back(
editModeScenegraphNodes.CurvesMaterials[l][t]->diffuseColor.startEditing());
}
}
int maxNumberOfConstraints = std::min(editModeScenegraphNodes.constrGroup->getNumChildren(),
static_cast<int>(constraints.size()));
// colors of the constraints
for (int i = 0; i < maxNumberOfConstraints; i++) {
SoSeparator* s =
static_cast<SoSeparator*>(editModeScenegraphNodes.constrGroup->getChild(i));
// Check Constraint Type
Sketcher::Constraint* constraint = constraints[i];
ConstraintType type = constraint->Type;
// It may happen that color updates are triggered by programmatic selection changes before a
// command final update. Then constraints may have been changed and the color will be
// updated as part
if (type != vConstrType[i]) {
break;
}
bool hasDatumLabel = (type == Sketcher::Angle || type == Sketcher::Radius
|| type == Sketcher::Diameter || type == Sketcher::Weight
|| type == Sketcher::Symmetric || type == Sketcher::Distance
|| type == Sketcher::DistanceX || type == Sketcher::DistanceY);
// Non DatumLabel Nodes will have a material excluding coincident
bool hasMaterial = false;
SoMaterial* m = nullptr;
if (!hasDatumLabel && type != Sketcher::Coincident && type != Sketcher::InternalAlignment) {
int matIndex = static_cast<int>(ConstraintNodePosition::MaterialIndex);
if (matIndex < s->getNumChildren()) {
hasMaterial = true;
m = static_cast<SoMaterial*>(s->getChild(matIndex));
}
}
auto selectpoint = [this, pcolor, PtNum](int geoid, Sketcher::PointPos pos) {
if (geoid >= 0) {
auto multifieldIndex = coinMapping.getIndexLayer(geoid, pos);
if (multifieldIndex != MultiFieldId::Invalid) {
int index = multifieldIndex.fieldIndex;
int layer = multifieldIndex.layerId;
if (layer < static_cast<int>(PtNum.size()) && index >= 0
&& index < PtNum[layer]) {
pcolor[layer][index] = drawingParameters.SelectColor;
}
}
}
};
auto selectline = [this, color, CurvNum](int geoid) {
if (geoid >= 0) {
auto multifieldIndex = coinMapping.getIndexLayer(geoid, Sketcher::PointPos::none);
if (multifieldIndex != MultiFieldId::Invalid) {
int index = multifieldIndex.fieldIndex;
int layer = multifieldIndex.layerId;
int sublayer = multifieldIndex.geoTypeId;
if (layer < static_cast<int>(CurvNum.size())
&& sublayer < static_cast<int>(CurvNum[layer].size()) && index >= 0
&& index < CurvNum[layer][sublayer]) {
color[layer][sublayer][index] = drawingParameters.SelectColor;
}
}
}
};
if (ViewProviderSketchCoinAttorney::isConstraintSelected(viewProvider, i)) {
if (hasDatumLabel) {
SoDatumLabel* l = static_cast<SoDatumLabel*>(
s->getChild(static_cast<int>(ConstraintNodePosition::DatumLabelIndex)));
l->textColor = drawingParameters.SelectColor;
}
else if (hasMaterial) {
m->diffuseColor = drawingParameters.SelectColor;
}
else if (type == Sketcher::Coincident) {
selectpoint(constraint->First, constraint->FirstPos);
selectpoint(constraint->Second, constraint->SecondPos);
}
else if (type == Sketcher::InternalAlignment) {
switch (constraint->AlignmentType) {
case EllipseMajorDiameter:
case EllipseMinorDiameter:
case HyperbolaMajor:
case HyperbolaMinor:
case ParabolaFocalAxis: {
selectline(constraint->First);
} break;
case EllipseFocus1:
case EllipseFocus2:
case HyperbolaFocus:
case ParabolaFocus:
case BSplineControlPoint:
case BSplineKnotPoint: {
selectpoint(constraint->First, constraint->FirstPos);
} break;
default:
break;
}
}
}
else if (ViewProviderSketchCoinAttorney::isConstraintPreselected(viewProvider, i)) {
if (hasDatumLabel) {
SoDatumLabel* l = static_cast<SoDatumLabel*>(
s->getChild(static_cast<int>(ConstraintNodePosition::DatumLabelIndex)));
l->textColor = drawingParameters.PreselectColor;
}
else if (hasMaterial) {
m->diffuseColor = drawingParameters.PreselectColor;
}
}
else {
if (hasDatumLabel) {
SoDatumLabel* l = static_cast<SoDatumLabel*>(
s->getChild(static_cast<int>(ConstraintNodePosition::DatumLabelIndex)));
l->textColor = constraint->isActive
? (constraint->isDriving ? drawingParameters.ConstrDimColor
: drawingParameters.NonDrivingConstrDimColor)
: drawingParameters.DeactivatedConstrDimColor;
}
else if (hasMaterial) {
m->diffuseColor = constraint->isActive
? (constraint->isDriving ? drawingParameters.ConstrDimColor
: drawingParameters.NonDrivingConstrDimColor)
: drawingParameters.DeactivatedConstrDimColor;
}
}
}
for (int l = 0; l < geometryLayerParameters.getCoinLayerCount(); l++) {
editModeScenegraphNodes.PointsMaterials[l]->diffuseColor.finishEditing();
for (int t = 0; t < geometryLayerParameters.getSubLayerCount(); t++) {
editModeScenegraphNodes.CurvesMaterials[l][t]->diffuseColor.finishEditing();
}
}
}
void EditModeConstraintCoinManager::rebuildConstraintNodes()
{
auto geolistfacade = ViewProviderSketchCoinAttorney::getGeoListFacade(viewProvider);
rebuildConstraintNodes(geolistfacade);
}
void EditModeConstraintCoinManager::setConstraintSelectability(bool enabled /* = true */)
{
if (enabled) {
editModeScenegraphNodes.constrGrpSelect->style.setValue(SoPickStyle::SHAPE);
}
else {
editModeScenegraphNodes.constrGrpSelect->style.setValue(SoPickStyle::UNPICKABLE);
}
}
void EditModeConstraintCoinManager::rebuildConstraintNodes(const GeoListFacade& geolistfacade)
{
const std::vector<Sketcher::Constraint*>& constrlist =
ViewProviderSketchCoinAttorney::getConstraints(viewProvider);
// clean up
Gui::coinRemoveAllChildren(editModeScenegraphNodes.constrGroup);
vConstrType.clear();
// Get sketch normal
Base::Vector3d RN(0, 0, 1);
// move to position of Sketch
Base::Placement Plz = ViewProviderSketchCoinAttorney::getEditingPlacement(viewProvider);
Base::Rotation tmp(Plz.getRotation());
tmp.multVec(RN, RN);
Plz.setRotation(tmp);
SbVec3f norm(RN.x, RN.y, RN.z);
rebuildConstraintNodes(geolistfacade, constrlist, norm);
}
void EditModeConstraintCoinManager::rebuildConstraintNodes(
const GeoListFacade& geolistfacade,
const std::vector<Sketcher::Constraint*> constrlist,
SbVec3f norm)
{
for (std::vector<Sketcher::Constraint*>::const_iterator it = constrlist.begin();
it != constrlist.end();
++it) {
// root separator for one constraint
SoSeparator* sep = new SoSeparator();
sep->ref();
// no caching for frequently-changing data structures
sep->renderCaching = SoSeparator::OFF;
// every constrained visual node gets its own material for preselection and selection
SoMaterial* mat = new SoMaterial;
mat->ref();
mat->diffuseColor = (*it)->isActive
? ((*it)->isDriving ? drawingParameters.ConstrDimColor
: drawingParameters.NonDrivingConstrDimColor)
: drawingParameters.DeactivatedConstrDimColor;
// distinguish different constraint types to build up
switch ((*it)->Type) {
case Distance:
case DistanceX:
case DistanceY:
case Radius:
case Diameter:
case Weight:
case Angle: {
SoDatumLabel* text = new SoDatumLabel();
text->norm.setValue(norm);
text->string = "";
text->textColor = (*it)->isActive
? ((*it)->isDriving ? drawingParameters.ConstrDimColor
: drawingParameters.NonDrivingConstrDimColor)
: drawingParameters.DeactivatedConstrDimColor;
text->size.setValue(drawingParameters.labelFontSize);
text->lineWidth = 2 * drawingParameters.pixelScalingFactor;
text->useAntialiasing = false;
sep->addChild(text);
editModeScenegraphNodes.constrGroup->addChild(sep);
vConstrType.push_back((*it)->Type);
// nodes not needed
sep->unref();
mat->unref();
continue; // jump to next constraint
} break;
case Horizontal:
case Vertical:
case Block: {
// #define CONSTRAINT_SEPARATOR_INDEX_MATERIAL_OR_DATUMLABEL 0
sep->addChild(mat);
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_TRANSLATION 1
sep->addChild(new SoZoomTranslation());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_ICON 2
sep->addChild(new SoImage());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_CONSTRAINTID 3
sep->addChild(new SoInfo());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_TRANSLATION 4
sep->addChild(new SoZoomTranslation());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_ICON 5
sep->addChild(new SoImage());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_CONSTRAINTID 6
sep->addChild(new SoInfo());
// remember the type of this constraint node
vConstrType.push_back((*it)->Type);
} break;
case Coincident: // no visual for coincident so far
vConstrType.push_back(Coincident);
break;
case Parallel:
case Perpendicular:
case Equal: {
// #define CONSTRAINT_SEPARATOR_INDEX_MATERIAL_OR_DATUMLABEL 0
sep->addChild(mat);
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_TRANSLATION 1
sep->addChild(new SoZoomTranslation());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_ICON 2
sep->addChild(new SoImage());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_CONSTRAINTID 3
sep->addChild(new SoInfo());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_TRANSLATION 4
sep->addChild(new SoZoomTranslation());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_ICON 5
sep->addChild(new SoImage());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_CONSTRAINTID 6
sep->addChild(new SoInfo());
// remember the type of this constraint node
vConstrType.push_back((*it)->Type);
} break;
case PointOnObject:
case Tangent:
case SnellsLaw: {
// #define CONSTRAINT_SEPARATOR_INDEX_MATERIAL_OR_DATUMLABEL 0
sep->addChild(mat);
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_TRANSLATION 1
sep->addChild(new SoZoomTranslation());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_ICON 2
sep->addChild(new SoImage());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_CONSTRAINTID 3
sep->addChild(new SoInfo());
if ((*it)->Type == Tangent) {
const Part::Geometry* geo1 = geolistfacade.getGeometryFromGeoId((*it)->First);
const Part::Geometry* geo2 = geolistfacade.getGeometryFromGeoId((*it)->Second);
if (!geo1 || !geo2) {
Base::Console().developerWarning(
"EditModeConstraintCoinManager",
"Tangent constraint references non-existing geometry\n");
}
else if (geo1->is<Part::GeomLineSegment>()
&& geo2->is<Part::GeomLineSegment>()) {
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_TRANSLATION 4
sep->addChild(new SoZoomTranslation());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_ICON 5
sep->addChild(new SoImage());
// #define CONSTRAINT_SEPARATOR_INDEX_SECOND_CONSTRAINTID 6
sep->addChild(new SoInfo());
}
}
vConstrType.push_back((*it)->Type);
} break;
case Symmetric: {
SoDatumLabel* arrows = new SoDatumLabel();
arrows->norm.setValue(norm);
arrows->string = "";
arrows->textColor = drawingParameters.ConstrDimColor;
arrows->lineWidth = 2 * drawingParameters.pixelScalingFactor;
// #define CONSTRAINT_SEPARATOR_INDEX_MATERIAL_OR_DATUMLABEL 0
sep->addChild(arrows);
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_TRANSLATION 1
sep->addChild(new SoTranslation());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_ICON 2
sep->addChild(new SoImage());
// #define CONSTRAINT_SEPARATOR_INDEX_FIRST_CONSTRAINTID 3
sep->addChild(new SoInfo());
vConstrType.push_back((*it)->Type);
} break;
case InternalAlignment: {
vConstrType.push_back((*it)->Type);
} break;
default:
vConstrType.push_back((*it)->Type);
}
editModeScenegraphNodes.constrGroup->addChild(sep);
// decrement ref counter again
sep->unref();
mat->unref();
}
}
QString EditModeConstraintCoinManager::getPresentationString(const Constraint* constraint,
std::string prefix)
{
/**
* Hide units if
* - user has requested it,
* - is being displayed in the base units, -and-
* - the schema being used has a clear base unit in the first place.
*
* Remove unit string if expected unit string matches actual unit string
* Example code from: Mod/TechDraw/App/DrawViewDimension.cpp:372
*
* Hide the default length unit
*/
auto fixValueStr = [&](const QString& valueStr, const auto& unitStr) -> std::optional<QString> {
if (!constraintParameters.bHideUnits || constraint->Type == Sketcher::Angle) {
return std::nullopt;
}
const auto baseUnitStr {Base::UnitsApi::getBasicLengthUnit()};
if (baseUnitStr.empty() || baseUnitStr != unitStr) {
return std::nullopt;
}
// trailing space or non-dig
const QRegularExpression rxUnits {QString::fromUtf8(" \\D*$")};
auto vStr = valueStr;
vStr.remove(rxUnits);
return {vStr};
};
// Get the current value string including units
double factor {};
std::string unitStr; // the actual unit string
const auto constrPresValue {constraint->getPresentationValue().getUserString(factor, unitStr)};
auto valueStr = QString::fromStdString(constrPresValue);
auto fixedValueStr = fixValueStr(valueStr, unitStr).value_or(valueStr);
if (!prefix.empty()) {
fixedValueStr.prepend(QString::fromStdString(prefix));
}
if (constraintParameters.bShowDimensionalName && !constraint->Name.empty()) {
/**
* Create the representation string from the user defined format string
* Format options are:
* %N - the constraint name parameter
* %V - the value of the dimensional constraint, including any unit characters
*/
auto sDimFmt {constraintParameters.sDimensionalStringFormat};
if (!sDimFmt.contains(QLatin1String("%V"))
&& !sDimFmt.contains(QLatin1String("%N"))) { // using default format "%N = %V"
fixedValueStr = QString::fromStdString(constraint->Name) + QString::fromLatin1(" = ")
+ fixedValueStr;
}
else {
sDimFmt.replace(QLatin1String("%N"), QString::fromStdString(constraint->Name));
sDimFmt.replace(QLatin1String("%V"), fixedValueStr);
fixedValueStr = sDimFmt;
}
}
int constraintIndex = -1;
const auto& constrlist = ViewProviderSketchCoinAttorney::getConstraints(viewProvider);
auto it = std::find(constrlist.begin(), constrlist.end(), constraint);
if (it != constrlist.end()) {
constraintIndex = std::distance(constrlist.begin(), it);
if (ViewProviderSketchCoinAttorney::constraintHasExpression(viewProvider,
constraintIndex)) {
fixedValueStr += QStringLiteral(" (ƒ𝑥)");
}
}
if (!constraint->isDriving) {
fixedValueStr = QStringLiteral("(") + fixedValueStr + QStringLiteral(")");
}
if (!constraint->isActive) {
QString result = QStringLiteral("\u0336");
for (auto c : std::as_const(fixedValueStr)) {
result += c + QStringLiteral("\u0336");
}
return result;
}
return fixedValueStr;
}
std::set<int> EditModeConstraintCoinManager::detectPreselectionConstr(const SoPickedPoint* Point)
{
std::set<int> constrIndices;
SoPath* path = Point->getPath();
// The picked node must be a child of the main constraint group.
SoNode* tailFather2 = path->getNode(path->getLength() - 3);
if (tailFather2 != editModeScenegraphNodes.constrGroup) {
return constrIndices;
}
SoNode* tail = path->getTail(); // This is the SoImage or SoDatumLabel node that was picked.
SoSeparator* sep = static_cast<SoSeparator*>(path->getNode(path->getLength() - 2));
for (int childIdx = 0; childIdx < sep->getNumChildren(); ++childIdx) {
if (tail == sep->getChild(childIdx) && dynamic_cast<SoImage*>(tail)) {
// The SoInfo node with the ID always follows the SoImage node.
if (childIdx + 1 < sep->getNumChildren()) {
SoInfo* constrIdsNode = dynamic_cast<SoInfo*>(sep->getChild(childIdx + 1));
if (!constrIdsNode) {
continue;
}
QString constrIdsStr =
QString::fromLatin1(constrIdsNode->string.getValue().getString());
if (combinedConstrBoxes.count(constrIdsStr)) {
// 1. Get the icon group size in device independent pixels.
SbVec3s iconGroupSize = getDisplayedSize(static_cast<SoImage*>(tail));
// 2. Get the icon group's absolute world position from its
// SoZoomTranslation node.
SoZoomTranslation* translation = nullptr;
SoNode* firstTransNode = sep->getChild(
static_cast<int>(ConstraintNodePosition::FirstTranslationIndex));
if (dynamic_cast<SoZoomTranslation*>(firstTransNode)) {
translation = static_cast<SoZoomTranslation*>(firstTransNode);
}
if (!translation) {
continue;
}
SbVec3f absPos = translation->abPos.getValue();
SbVec3f trans = translation->translation.getValue();
float scaleFactor = translation->getScaleFactor();
// If this is the second icon in a pair, add its relative translation.
if (int secondIndex = static_cast<int>(ConstraintNodePosition::SecondIconIndex);
secondIndex < sep->getNumChildren()) {
SoNode* secondIconNode = sep->getChild(secondIndex);
if (tail == secondIconNode) {
auto translation2 = static_cast<SoZoomTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::SecondTranslationIndex)));
absPos += translation2->abPos.getValue();
trans += translation2->translation.getValue();
scaleFactor = translation2->getScaleFactor();
}
}
// 3. Calculate the icon's center in world coordinates.
SbVec3f iconGroupWorldPos = absPos + scaleFactor * trans;
// 4. Project both the icon's center and the picked point to screen coordinates
// (device independent pixels). This is the key: both points are now in the same
// coordinate system.
SbVec2f iconGroupScreenCenter =
ViewProviderSketchCoinAttorney::getScreenCoordinates(
viewProvider,
SbVec2f(iconGroupWorldPos[0], iconGroupWorldPos[1]));
SbVec2f cursorScreenPos = ViewProviderSketchCoinAttorney::getScreenCoordinates(
viewProvider,
SbVec2f(Point->getPoint()[0], Point->getPoint()[1]));
// 5. Calculate cursor position relative to the icon group's top-left corner.
// - QRect/QImage assumes a top-left origin (Y increases downwards).
// - Coin3D screen coordinates have a bottom-left origin (Y increases
// upwards).
// - We must flip the Y-axis for the check.
int relativeX = static_cast<int>(cursorScreenPos[0] - iconGroupScreenCenter[0]
+ iconGroupSize[0] / 2.0f);
int relativeY = static_cast<int>(iconGroupScreenCenter[1] - cursorScreenPos[1]
+ iconGroupSize[1] / 2.0f);
// 6. Perform the hit test on each icon in the group.
for (const auto& boxInfo : combinedConstrBoxes[constrIdsStr]) {
if (boxInfo.first.contains(relativeX, relativeY)) {
constrIndices.insert(boxInfo.second.begin(), boxInfo.second.end());
}
}
}
else {
// Simple, non-merged icon.
QStringList constrIdStrings = constrIdsStr.split(QStringLiteral(","));
for (const QString& id : constrIdStrings) {
constrIndices.insert(id.toInt());
}
}
return constrIndices;
}
}
}
// Handle selection of datum labels (e.g., radius, distance dimensions).
if (dynamic_cast<SoDatumLabel*>(tail)) {
for (int i = 0; i < editModeScenegraphNodes.constrGroup->getNumChildren(); ++i) {
if (editModeScenegraphNodes.constrGroup->getChild(i) == sep) {
constrIndices.insert(i);
break;
}
}
}
return constrIndices;
}
SbVec3s EditModeConstraintCoinManager::getDisplayedSize(const SoImage* iconPtr) const
{
SbVec3s iconSize = iconPtr->image.getValue().getSize();
if (iconPtr->width.getValue() != -1) {
iconSize[0] = iconPtr->width.getValue();
}
if (iconPtr->height.getValue() != -1) {
iconSize[1] = iconPtr->height.getValue();
}
return iconSize;
}
// public function that triggers drawing of most constraint icons
void EditModeConstraintCoinManager::drawConstraintIcons()
{
auto geolistfacade = ViewProviderSketchCoinAttorney::getGeoListFacade(viewProvider);
drawConstraintIcons(geolistfacade);
}
void EditModeConstraintCoinManager::drawConstraintIcons(const GeoListFacade& geolistfacade)
{
const std::vector<Sketcher::Constraint*>& constraints =
ViewProviderSketchCoinAttorney::getConstraints(viewProvider);
std::vector<constrIconQueueItem> iconQueue;
int maxNumberOfConstraints = std::min(editModeScenegraphNodes.constrGroup->getNumChildren(),
static_cast<int>(constraints.size()));
for (int constrId = 0; constrId < maxNumberOfConstraints; ++constrId) {
Sketcher::Constraint* constraint = constraints[constrId];
// Check if Icon Should be created
bool multipleIcons = false;
QString icoType = iconTypeFromConstraint(constraint);
if (icoType.isEmpty()) {
continue;
}
if (constraint->Type != vConstrType[constrId]) {
break;
}
switch (constraint->Type) {
case Tangent: { // second icon is available only for collinear line segments
const Part::Geometry* geo1 = geolistfacade.getGeometryFromGeoId(constraint->First);
const Part::Geometry* geo2 = geolistfacade.getGeometryFromGeoId(constraint->Second);
if (geo1 && geo1->is<Part::GeomLineSegment>() && geo2
&& geo2->is<Part::GeomLineSegment>()) {
multipleIcons = true;
}
} break;
case Horizontal:
case Vertical: { // second icon is available only for point alignment
if (constraint->Second != GeoEnum::GeoUndef
&& constraint->FirstPos != Sketcher::PointPos::none
&& constraint->SecondPos != Sketcher::PointPos::none) {
multipleIcons = true;
}
} break;
case Parallel:
multipleIcons = true;
break;
case Perpendicular:
// second icon is available only when there is no common point
if (constraint->FirstPos == Sketcher::PointPos::none
&& constraint->Third == GeoEnum::GeoUndef) {
multipleIcons = true;
}
break;
case Equal:
multipleIcons = true;
break;
default:
break;
}
// Double-check that we can safely access the Inventor nodes
if (constrId >= editModeScenegraphNodes.constrGroup->getNumChildren()) {
Base::Console().developerWarning(
"EditModeConstraintManager",
"Can't update constraint icons because view is not in sync with sketch\n");
break;
}
// Find the Constraint Icon SoImage Node
SoSeparator* sep =
static_cast<SoSeparator*>(editModeScenegraphNodes.constrGroup->getChild(constrId));
int numChildren = sep->getNumChildren();
SbVec3f absPos;
// Somewhat hacky - we use SoZoomTranslations for most types of icon,
// but symmetry icons use SoTranslations...
SoTranslation* translationPtr = static_cast<SoTranslation*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::FirstTranslationIndex)));
if (dynamic_cast<SoZoomTranslation*>(translationPtr)) {
absPos = static_cast<SoZoomTranslation*>(translationPtr)->abPos.getValue();
}
else {
absPos = translationPtr->translation.getValue();
}
SoImage* coinIconPtr = dynamic_cast<SoImage*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::FirstIconIndex)));
SoInfo* infoPtr = static_cast<SoInfo*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::FirstConstraintIdIndex)));
constrIconQueueItem thisIcon;
thisIcon.type = icoType;
thisIcon.constraintId = constrId;
thisIcon.position = absPos;
thisIcon.destination = coinIconPtr;
thisIcon.infoPtr = infoPtr;
thisIcon.visible = (constraint->isInVirtualSpace
== ViewProviderSketchCoinAttorney::isShownVirtualSpace(viewProvider))
&& constraint->isVisible;
if (constraint->Type == Symmetric) {
Base::Vector3d startingpoint =
geolistfacade.getPoint(constraint->First, constraint->FirstPos);
Base::Vector3d endpoint =
geolistfacade.getPoint(constraint->Second, constraint->SecondPos);
SbVec3f pos0(startingpoint.x, startingpoint.y, startingpoint.z);
SbVec3f pos1(endpoint.x, endpoint.y, endpoint.z);
thisIcon.iconRotation =
ViewProviderSketchCoinAttorney::getRotation(viewProvider, pos0, pos1);
}
else {
thisIcon.iconRotation = 0;
}
if (multipleIcons) {
if (constraint->Name.empty()) {
thisIcon.label = QString::number(constrId + 1);
}
else {
thisIcon.label = QString::fromUtf8(constraint->Name.c_str());
}
iconQueue.push_back(thisIcon);
// Note that the second translation is meant to be applied after the first.
// So, to get the position of the second icon, we add the two translations together
//
// See note ~30 lines up.
if (numChildren > static_cast<int>(ConstraintNodePosition::SecondConstraintIdIndex)) {
translationPtr = static_cast<SoTranslation*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::SecondTranslationIndex)));
if (dynamic_cast<SoZoomTranslation*>(translationPtr)) {
thisIcon.position +=
static_cast<SoZoomTranslation*>(translationPtr)->abPos.getValue();
}
else {
thisIcon.position += translationPtr->translation.getValue();
}
thisIcon.destination = dynamic_cast<SoImage*>(
sep->getChild(static_cast<int>(ConstraintNodePosition::SecondIconIndex)));
thisIcon.infoPtr = static_cast<SoInfo*>(sep->getChild(
static_cast<int>(ConstraintNodePosition::SecondConstraintIdIndex)));
}
}
else {
if (constraint->Name.empty()) {
thisIcon.label = QString();
}
else {
thisIcon.label = QString::fromUtf8(constraint->Name.c_str());
}
}
iconQueue.push_back(thisIcon);
}
combineConstraintIcons(iconQueue);
}
void EditModeConstraintCoinManager::combineConstraintIcons(IconQueue iconQueue)
{
// getScaleFactor gives us a ratio of pixels per some kind of real units
float maxDistSquared = pow(ViewProviderSketchCoinAttorney::getScaleFactor(viewProvider), 2);
// There's room for optimisation here; we could reuse the combined icons...
combinedConstrBoxes.clear();
while (!iconQueue.empty()) {
// A group starts with an item popped off the back of our initial queue
IconQueue thisGroup;
thisGroup.push_back(iconQueue.back());
constrIconQueueItem init = iconQueue.back();
iconQueue.pop_back();
// we group only icons not being Symmetry icons, because we want those on the line
// and only icons that are visible
if (init.type != QStringLiteral("Constraint_Symmetric") && init.visible) {
IconQueue::iterator i = iconQueue.begin();
while (i != iconQueue.end()) {
if ((*i).visible) {
bool addedToGroup = false;
for (IconQueue::iterator j = thisGroup.begin(); j != thisGroup.end(); ++j) {
float distSquared = pow(i->position[0] - j->position[0], 2)
+ pow(i->position[1] - j->position[1], 2);
if (distSquared <= maxDistSquared
&& (*i).type != QStringLiteral("Constraint_Symmetric")) {
// Found an icon in iconQueue that's close enough to
// a member of thisGroup, so move it into thisGroup
thisGroup.push_back(*i);
i = iconQueue.erase(i);
addedToGroup = true;
break;
}
}
if (addedToGroup) {
if (i == iconQueue.end()) {
// We just got the last icon out of iconQueue
break;
}
else {
// Start looking through the iconQueue again, in case
// we have an icon that's now close enough to thisGroup
i = iconQueue.begin();
}
}
else {
++i;
}
}
else { // if !visible we skip it
i++;
}
}
}
if (thisGroup.size() == 1) {
drawTypicalConstraintIcon(thisGroup[0]);
}
else {
drawMergedConstraintIcons(thisGroup);
}
}
}
void EditModeConstraintCoinManager::drawMergedConstraintIcons(IconQueue iconQueue)
{
for (IconQueue::iterator i = iconQueue.begin(); i != iconQueue.end(); ++i) {
clearCoinImage(i->destination);
}
QImage compositeIcon;
SoImage* thisDest = iconQueue[0].destination;
SoInfo* thisInfo = iconQueue[0].infoPtr;
// Tracks all constraint IDs that are combined into this icon
QString idString;
int lastVPad = 0;
QStringList labels;
std::vector<int> ids;
QString thisType;
QColor iconColor;
QList<QColor> labelColors;
int maxColorPriority;
double iconRotation;
ConstrIconBBVec boundingBoxes;
while (!iconQueue.empty()) {
IconQueue::iterator i = iconQueue.begin();
labels.clear();
labels.append(i->label);
ids.clear();
ids.push_back(i->constraintId);
thisType = i->type;
iconColor = constrColor(i->constraintId);
labelColors.clear();
labelColors.append(iconColor);
iconRotation = i->iconRotation;
maxColorPriority = constrColorPriority(i->constraintId);
if (idString.length()) {
idString.append(QStringLiteral(","));
}
idString.append(QString::number(i->constraintId));
i = iconQueue.erase(i);
while (i != iconQueue.end()) {
if (i->type != thisType) {
++i;
continue;
}
labels.append(i->label);
ids.push_back(i->constraintId);
labelColors.append(constrColor(i->constraintId));
if (constrColorPriority(i->constraintId) > maxColorPriority) {
maxColorPriority = constrColorPriority(i->constraintId);
iconColor = constrColor(i->constraintId);
}
idString.append(QStringLiteral(",") + QString::number(i->constraintId));
i = iconQueue.erase(i);
}
// To be inserted into edit->combinedConstBoxes
std::vector<QRect> boundingBoxesVec;
int oldHeight = 0;
// Render the icon here.
if (compositeIcon.isNull()) {
compositeIcon = renderConstrIcon(thisType,
iconColor,
labels,
labelColors,
iconRotation,
&boundingBoxesVec,
&lastVPad);
}
else {
int thisVPad;
QImage partialIcon = renderConstrIcon(thisType,
iconColor,
labels,
labelColors,
iconRotation,
&boundingBoxesVec,
&thisVPad);
// Stack vertically for now. Down the road, it might make sense
// to figure out the best orientation automatically.
oldHeight = compositeIcon.height();
// This is overkill for the currently used (20 July 2014) font,
// since it always seems to have the same vertical pad, but this
// might not always be the case. The 3 pixel buffer might need
// to vary depending on font size too...
oldHeight -= std::max(lastVPad - 3, 0);
compositeIcon = compositeIcon.copy(0,
0,
std::max(partialIcon.width(), compositeIcon.width()),
partialIcon.height() + compositeIcon.height());
QPainter qp(&compositeIcon);
qp.drawImage(0, oldHeight, partialIcon);
lastVPad = thisVPad;
}
// Add bounding boxes for the icon we just rendered to boundingBoxes
std::vector<int>::iterator id = ids.begin();
std::set<int> nextIds;
for (std::vector<QRect>::iterator bb = boundingBoxesVec.begin();
bb != boundingBoxesVec.end();
++bb) {
nextIds.clear();
if (bb == boundingBoxesVec.begin()) {
// The first bounding box is for the icon at left, so assign
// all IDs for that type of constraint to the icon.
for (std::vector<int>::iterator j = ids.begin(); j != ids.end(); ++j) {
nextIds.insert(*j);
}
}
else {
nextIds.insert(*(id++));
}
ConstrIconBB newBB(bb->adjusted(0, oldHeight, 0, oldHeight), nextIds);
boundingBoxes.push_back(newBB);
}
}
combinedConstrBoxes[idString] = boundingBoxes;
thisInfo->string.setValue(idString.toLatin1().data());
sendConstraintIconToCoin(compositeIcon, thisDest);
}
/// Note: labels, labelColors, and boundingBoxes are all
/// assumed to be the same length.
QImage EditModeConstraintCoinManager::renderConstrIcon(const QString& type,
const QColor& iconColor,
const QStringList& labels,
const QList<QColor>& labelColors,
double iconRotation,
std::vector<QRect>* boundingBoxes,
int* vPad)
{
// Constants to help create constraint icons
QString joinStr = QStringLiteral(", ");
QPixmap pxMap;
std::stringstream constraintName;
constraintName << type.toLatin1().data()
<< drawingParameters.constraintIconSize; // allow resizing by embedding size
if (!Gui::BitmapFactory().findPixmapInCache(constraintName.str().c_str(), pxMap)) {
pxMap = Gui::BitmapFactory().pixmapFromSvg(
type.toLatin1().data(),
QSizeF(drawingParameters.constraintIconSize, drawingParameters.constraintIconSize));
Gui::BitmapFactory().addPixmapToCache(constraintName.str().c_str(),
pxMap); // Cache for speed, avoiding pixmapFromSvg
}
QImage icon = pxMap.toImage();
// The pixmap was already scaled so we don't need to scale the image
icon.setDevicePixelRatio(1.0f);
QFont font = ViewProviderSketchCoinAttorney::getApplicationFont(viewProvider);
font.setPixelSize(static_cast<int>(1.0 * drawingParameters.constraintIconSize));
font.setBold(true);
QFontMetrics qfm = QFontMetrics(font);
int labelWidth = qfm.boundingRect(labels.join(joinStr)).width();
// See Qt docs on qRect::bottom() for explanation of the +1
int pxBelowBase = qfm.boundingRect(labels.join(joinStr)).bottom() + 1;
if (vPad) {
*vPad = pxBelowBase;
}
QTransform rotation;
rotation.rotate(iconRotation);
QImage roticon = icon.transformed(rotation);
QImage image = roticon.copy(0, 0, roticon.width() + labelWidth, roticon.height() + pxBelowBase);
// Make a bounding box for the icon
if (boundingBoxes) {
boundingBoxes->push_back(QRect(0, 0, roticon.width(), roticon.height()));
}
// Render the Icons
QPainter qp(&image);
qp.setCompositionMode(QPainter::CompositionMode_SourceIn);
qp.fillRect(roticon.rect(), iconColor);
// Render constraint label if necessary
if (!labels.join(QString()).isEmpty()) {
qp.setCompositionMode(QPainter::CompositionMode_SourceOver);
qp.setFont(font);
int cursorOffset = 0;
// In Python: "for label, color in zip(labels, labelColors):"
QStringList::const_iterator labelItr;
QString labelStr;
QList<QColor>::const_iterator colorItr;
QRect labelBB;
for (labelItr = labels.begin(), colorItr = labelColors.begin();
labelItr != labels.end() && colorItr != labelColors.end();
++labelItr, ++colorItr) {
qp.setPen(*colorItr);
if (labelItr + 1 == labels.end()) { // if this is the last label
labelStr = *labelItr;
}
else {
labelStr = *labelItr + joinStr;
}
// Note: text can sometimes draw to the left of the starting
// position, eg italic fonts. Check QFontMetrics
// documentation for more info, but be mindful if the
// icon.width() is ever very small (or removed).
qp.drawText(icon.width() + cursorOffset, icon.height(), labelStr);
if (boundingBoxes) {
labelBB = qfm.boundingRect(labelStr);
labelBB.moveTo(icon.width() + cursorOffset,
icon.height() - qfm.height() + pxBelowBase);
boundingBoxes->push_back(labelBB);
}
cursorOffset += Gui::QtTools::horizontalAdvance(qfm, labelStr);
}
}
return image;
}
void EditModeConstraintCoinManager::drawTypicalConstraintIcon(const constrIconQueueItem& i)
{
QColor color = constrColor(i.constraintId);
QImage image = renderConstrIcon(i.type,
color,
QStringList(i.label),
QList<QColor>() << color,
i.iconRotation);
i.infoPtr->string.setValue(QString::number(i.constraintId).toLatin1().data());
sendConstraintIconToCoin(image, i.destination);
}
QString EditModeConstraintCoinManager::iconTypeFromConstraint(Constraint* constraint)
{
/*! TODO: Consider pushing this functionality up into Constraint
*
Abdullah: Please, don't. An icon is visualisation information and
does not belong in App, but in Gui. Rather consider refactoring it
in a separate class dealing with visualisation of constraints.*/
switch (constraint->Type) {
case Horizontal:
return QStringLiteral("Constraint_Horizontal");
case Vertical:
return QStringLiteral("Constraint_Vertical");
case PointOnObject:
return QStringLiteral("Constraint_PointOnObject");
case Tangent:
return QStringLiteral("Constraint_Tangent");
case Parallel:
return QStringLiteral("Constraint_Parallel");
case Perpendicular:
return QStringLiteral("Constraint_Perpendicular");
case Equal:
return QStringLiteral("Constraint_EqualLength");
case Symmetric:
return QStringLiteral("Constraint_Symmetric");
case SnellsLaw:
return QStringLiteral("Constraint_SnellsLaw");
case Block:
return QStringLiteral("Constraint_Block");
default:
return QString();
}
}
void EditModeConstraintCoinManager::sendConstraintIconToCoin(const QImage& icon,
SoImage* soImagePtr)
{
SoSFImage icondata = SoSFImage();
Gui::BitmapFactory().convert(icon, icondata);
SbVec2s iconSize(icon.width(), icon.height());
int four = 4;
soImagePtr->image.setValue(iconSize, 4, icondata.getValue(iconSize, four));
// Set Image Alignment to Center
soImagePtr->vertAlignment = SoImage::HALF;
soImagePtr->horAlignment = SoImage::CENTER;
}
void EditModeConstraintCoinManager::clearCoinImage(SoImage* soImagePtr)
{
soImagePtr->setToDefaults();
}
QColor EditModeConstraintCoinManager::constrColor(int constraintId)
{
auto toQColor = [](auto sbcolor) -> QColor {
return QColor((int)(sbcolor[0] * 255.0f),
(int)(sbcolor[1] * 255.0f),
(int)(sbcolor[2] * 255.0f));
};
const auto constraints = ViewProviderSketchCoinAttorney::getConstraints(viewProvider);
if (ViewProviderSketchCoinAttorney::isConstraintPreselected(viewProvider, constraintId)) {
return toQColor(drawingParameters.PreselectColor);
}
else if (ViewProviderSketchCoinAttorney::isConstraintSelected(viewProvider, constraintId)) {
return toQColor(drawingParameters.SelectColor);
}
else if (!constraints[constraintId]->isActive) {
return toQColor(drawingParameters.DeactivatedConstrDimColor);
}
else if (!constraints[constraintId]->isDriving) {
return toQColor(drawingParameters.NonDrivingConstrDimColor);
}
else {
return toQColor(drawingParameters.ConstrIcoColor);
}
}
int EditModeConstraintCoinManager::constrColorPriority(int constraintId)
{
if (ViewProviderSketchCoinAttorney::isConstraintPreselected(viewProvider, constraintId)) {
return 3;
}
else if (ViewProviderSketchCoinAttorney::isConstraintSelected(viewProvider, constraintId)) {
return 2;
}
else {
return 1;
}
}
SoSeparator* EditModeConstraintCoinManager::getConstraintIdSeparator(int i)
{
return dynamic_cast<SoSeparator*>(editModeScenegraphNodes.constrGroup->getChild(i));
}
void EditModeConstraintCoinManager::createEditModeInventorNodes()
{
// group node for the Constraint visual +++++++++++++++++++++++++++++++++++
SoMaterialBinding* MtlBind = new SoMaterialBinding;
MtlBind->setName("ConstraintMaterialBinding");
MtlBind->value = SoMaterialBinding::OVERALL;
editModeScenegraphNodes.EditRoot->addChild(MtlBind);
// use small line width for the Constraints
editModeScenegraphNodes.ConstraintDrawStyle = new SoDrawStyle;
editModeScenegraphNodes.ConstraintDrawStyle->setName("ConstraintDrawStyle");
editModeScenegraphNodes.ConstraintDrawStyle->lineWidth =
1 * drawingParameters.pixelScalingFactor;
editModeScenegraphNodes.EditRoot->addChild(editModeScenegraphNodes.ConstraintDrawStyle);
// add the group where all the constraints has its SoSeparator
editModeScenegraphNodes.constrGrpSelect =
new SoPickStyle(); // used to toggle constraints selectability
editModeScenegraphNodes.constrGrpSelect->style.setValue(SoPickStyle::SHAPE);
editModeScenegraphNodes.EditRoot->addChild(editModeScenegraphNodes.constrGrpSelect);
setConstraintSelectability(); // Ensure default value;
editModeScenegraphNodes.constrGroup = new SmSwitchboard();
editModeScenegraphNodes.constrGroup->setName("ConstraintGroup");
editModeScenegraphNodes.EditRoot->addChild(editModeScenegraphNodes.constrGroup);
SoPickStyle* ps = new SoPickStyle(); // used to following nodes aren't impacted
ps->style.setValue(SoPickStyle::SHAPE);
editModeScenegraphNodes.EditRoot->addChild(ps);
}