Files
create/src/Gui/SoDatumLabel.cpp
tetektoza 246c4a1f0b Gui: Render constraint text and arrowheads above geometry lines
Add Z offset for arrowheads and explicitly enable depth testing for
constraint lines in SoDatumLabel. This ensures constraint lines render
below geometry (respecting zConstr level) while text and arrowheads
render on top for better visibility and selection.
2026-01-06 20:07:25 +01:00

2013 lines
65 KiB
C++

/***************************************************************************
* Copyright (c) 2011-2012 Luke Parry <l.parry@warwick.ac.uk> *
* *
* 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>
#ifdef FC_OS_WIN32
# include <windows.h>
# undef min
# undef max
#endif
#ifdef FC_OS_MACOSX
# include <OpenGL/gl.h>
#else
# include <GL/gl.h>
#endif
#include <algorithm>
#include <cmath>
#include <limits>
#include <numbers>
#include <QFontMetrics>
#include <QPainter>
#include <Inventor/SoPrimitiveVertex.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/elements/SoFocalDistanceElement.h>
#include <Inventor/elements/SoViewportRegionElement.h>
#include <Inventor/elements/SoViewVolumeElement.h>
#include <Inventor/misc/SoState.h>
#include <Base/Tools.h>
#include <Gui/BitmapFactory.h>
#include <Gui/Tools.h>
#include "SoDatumLabel.h"
// NOLINTBEGIN(readability-magic-numbers,cppcoreguidelines-pro-bounds-pointer-arithmetic)
constexpr const float ZCONSTR {0.006F};
// Z offset for arrowheads and text to render them ON TOP of geometry lines.
// Geometry lines are at Z ~0.005-0.008, so this offset ensures arrowheads
// and text selection primitives are above them and remain selectable.
constexpr const float ZARROW_TEXT_OFFSET {0.010F};
using namespace Gui;
// ------------------------------------------------------
namespace
{
void glVertex(const SbVec3f& pt)
{
glVertex3f(pt[0], pt[1], pt[2]);
}
void glVertexes(const std::vector<SbVec3f>& pts)
{
for (auto pt : pts) {
glVertex3f(pt[0], pt[1], pt[2]);
}
}
void glDrawLine(const SbVec3f& p1, const SbVec3f& p2)
{
glBegin(GL_LINES);
glVertexes({p1, p2});
glEnd();
}
void glDrawArc(
const SbVec3f& center,
float radius,
float startAngle = 0.,
float endAngle = 2.0 * std::numbers::pi,
int countSegments = 0
)
{
float range = endAngle - startAngle;
if (countSegments == 0) {
countSegments = std::max(6, abs(int(25.0 * range / std::numbers::pi)));
}
float segment = range / (countSegments - 1);
glBegin(GL_LINE_STRIP);
for (int i = 0; i < countSegments; i++) {
float theta = startAngle + segment * i;
SbVec3f v1 = center + radius * SbVec3f(cos(theta), sin(theta), 0);
glVertex(v1);
}
glEnd();
}
void glDrawArrow(const SbVec3f& base, const SbVec3f& dir, float width, float length)
{
// Calculate arrowhead points
SbVec3f normal(dir[1], -dir[0], 0);
SbVec3f arrowLeft = base - length * dir + width * normal;
SbVec3f arrowRight = base - length * dir - width * normal;
// Draw arrowheads at elevated Z to render ON TOP of geometry lines
glBegin(GL_TRIANGLES);
glVertex3f(base[0], base[1], ZARROW_TEXT_OFFSET);
glVertex3f(arrowLeft[0], arrowLeft[1], ZARROW_TEXT_OFFSET);
glVertex3f(arrowRight[0], arrowRight[1], ZARROW_TEXT_OFFSET);
glEnd();
}
} // namespace
SO_NODE_SOURCE(SoDatumLabel)
void SoDatumLabel::initClass()
{
SO_NODE_INIT_CLASS(SoDatumLabel, SoShape, "Shape");
}
// NOLINTNEXTLINE
SoDatumLabel::SoDatumLabel()
{
SO_NODE_CONSTRUCTOR(SoDatumLabel);
SO_NODE_ADD_FIELD(string, (""));
SO_NODE_ADD_FIELD(textColor, (SbVec3f(1.0F, 1.0F, 1.0F)));
SO_NODE_ADD_FIELD(pnts, (SbVec3f(.0F, .0F, .0F)));
SO_NODE_ADD_FIELD(norm, (SbVec3f(.0F, .0F, 1.F)));
SO_NODE_ADD_FIELD(name, ("Helvetica"));
SO_NODE_ADD_FIELD(size, (10.F));
SO_NODE_ADD_FIELD(lineWidth, (2.F));
SO_NODE_ADD_FIELD(sampling, (2.F));
SO_NODE_ADD_FIELD(datumtype, (SoDatumLabel::DISTANCE));
SO_NODE_DEFINE_ENUM_VALUE(Type, DISTANCE);
SO_NODE_DEFINE_ENUM_VALUE(Type, DISTANCEX);
SO_NODE_DEFINE_ENUM_VALUE(Type, DISTANCEY);
SO_NODE_DEFINE_ENUM_VALUE(Type, ANGLE);
SO_NODE_DEFINE_ENUM_VALUE(Type, RADIUS);
SO_NODE_DEFINE_ENUM_VALUE(Type, DIAMETER);
SO_NODE_DEFINE_ENUM_VALUE(Type, ARCLENGTH);
SO_NODE_SET_SF_ENUM_TYPE(datumtype, Type);
SO_NODE_ADD_FIELD(param1, (0.F));
SO_NODE_ADD_FIELD(param2, (0.F));
SO_NODE_ADD_FIELD(param4, (0.F));
SO_NODE_ADD_FIELD(param5, (0.F));
SO_NODE_ADD_FIELD(param6, (0.F));
SO_NODE_ADD_FIELD(param7, (0.F));
SO_NODE_ADD_FIELD(param8, (0.F));
useAntialiasing = true;
this->imgWidth = 0;
this->imgHeight = 0;
this->glimagevalid = false;
}
void SoDatumLabel::drawImage()
{
const SbString* s = string.getValues(0);
int num = string.getNum();
if (num == 0) {
this->image = SoSFImage();
return;
}
QFont font(QString::fromLatin1(name.getValue(), -1), size.getValue());
QFontMetrics fm(font);
QString str = QString::fromUtf8(s[0].getString());
int w = Gui::QtTools::horizontalAdvance(fm, str);
int h = fm.height();
// No Valid text
if (!w) {
this->image = SoSFImage();
return;
}
const SbColor& t = textColor.getValue();
QColor front;
front.setRgbF(t[0], t[1], t[2]);
QImage image(w * sampling.getValue(), h * sampling.getValue(), QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(sampling.getValue());
image.fill(0x00000000);
QPainter painter(&image);
if (useAntialiasing) {
painter.setRenderHint(QPainter::Antialiasing);
}
painter.setPen(front);
painter.setFont(font);
painter.drawText(0, 0, w, h, Qt::AlignLeft, str);
painter.end();
Gui::BitmapFactory().convert(image, this->image);
}
namespace Gui
{
// helper class to determine the bounding box of a datum label
class DatumLabelBox
{
public:
DatumLabelBox(float scale, SoDatumLabel* label)
: scale {scale}
, label {label}
{}
void computeBBox(SbBox3f& box, SbVec3f& center) const
{
std::vector<SbVec3f> corners;
if (label->datumtype.getValue() == SoDatumLabel::DISTANCE
|| label->datumtype.getValue() == SoDatumLabel::DISTANCEX
|| label->datumtype.getValue() == SoDatumLabel::DISTANCEY) {
corners = computeDistanceBBox();
}
else if (label->datumtype.getValue() == SoDatumLabel::RADIUS
|| label->datumtype.getValue() == SoDatumLabel::DIAMETER) {
corners = computeRadiusDiameterBBox();
}
else if (label->datumtype.getValue() == SoDatumLabel::ANGLE) {
corners = computeAngleBBox();
}
else if (label->datumtype.getValue() == SoDatumLabel::SYMMETRIC) {
corners = computeSymmetricBBox();
}
else if (label->datumtype.getValue() == SoDatumLabel::ARCLENGTH) {
corners = computeArcLengthBBox();
}
getBBox(corners, box, center);
}
private:
void getBBox(const std::vector<SbVec3f>& corners, SbBox3f& box, SbVec3f& center) const
{
constexpr float floatMax = std::numeric_limits<float>::max();
if (corners.size() > 1) {
float minX = floatMax;
float minY = floatMax;
float maxX = -floatMax;
float maxY = -floatMax;
for (SbVec3f it : corners) {
minX = (it[0] < minX) ? it[0] : minX;
minY = (it[1] < minY) ? it[1] : minY;
maxX = (it[0] > maxX) ? it[0] : maxX;
maxY = (it[1] > maxY) ? it[1] : maxY;
}
// Store the bounding box
box.setBounds(SbVec3f(minX, minY, 0.0F), SbVec3f(maxX, maxY, 0.0F));
center = box.getCenter();
}
}
std::vector<SbVec3f> computeDistanceBBox() const
{
SbVec2s imgsize;
int nc {};
int srcw = 1;
int srch = 1;
const unsigned char* dataptr = label->image.getValue(imgsize, nc);
if (dataptr) {
srcw = imgsize[0];
srch = imgsize[1];
}
float aspectRatio = (float)srcw / (float)srch;
float imgHeight = scale * (float)(srch);
float imgWidth = aspectRatio * imgHeight;
// get the points stored in the pnt field
const SbVec3f* points = label->pnts.getValues(0);
if (label->pnts.getNum() < 2) {
return {};
}
// use the shared geometry calculation for consistency
SoDatumLabel::DistanceGeometry geom = label->calculateDistanceGeometry(points);
std::vector<SbVec3f> corners;
float margin = imgHeight / 4.0F;
// include main points and extension line endpoints
corners.push_back(geom.p1);
corners.push_back(geom.p2);
corners.push_back(geom.perp1);
corners.push_back(geom.perp2);
// include text label area
corners.push_back(
geom.textOffset + geom.dir * (imgWidth / 2.0F + margin) + geom.normal * (srch + margin)
);
corners.push_back(
geom.textOffset - geom.dir * (imgWidth / 2.0F + margin) + geom.normal * (srch + margin)
);
corners.push_back(
geom.textOffset + geom.dir * (imgWidth / 2.0F + margin) - geom.normal * margin
);
corners.push_back(
geom.textOffset - geom.dir * (imgWidth / 2.0F + margin) - geom.normal * margin
);
// include arrow head positions for better selection
// arrows are positioned at dimension line endpoints (par1, par4)
corners.push_back(geom.par1);
corners.push_back(geom.par4);
corners.push_back(geom.ar1);
corners.push_back(geom.ar2);
corners.push_back(geom.ar3);
corners.push_back(geom.ar4);
return corners;
}
std::vector<SbVec3f> computeRadiusDiameterBBox() const
{
// get the points stored in the pnt field
const SbVec3f* points = label->pnts.getValues(0);
if (label->pnts.getNum() < 2) {
return {};
}
// use the shared geometry calculation for consistency
SoDatumLabel::DiameterGeometry geom = label->calculateDiameterGeometry(points);
std::vector<SbVec3f> corners;
// include main points and line segment points around text
corners.push_back(geom.p1);
corners.push_back(geom.p2);
corners.push_back(geom.pnt1);
corners.push_back(geom.pnt2);
// include arrow head positions for better selection
// first arrow head at p2
corners.push_back(geom.ar0);
corners.push_back(geom.ar1);
corners.push_back(geom.ar2);
// second arrow head for diameter (if applicable)
if (geom.isDiameter) {
corners.push_back(geom.ar0_1);
corners.push_back(geom.ar1_1);
corners.push_back(geom.ar2_1);
}
// sample points along the arc helper
constexpr int numArcSamples = 6;
const auto includeArcHelper = [&corners, &geom](float startAngle, float range) {
if (range != 0.0) {
for (int i = 0; i <= numArcSamples; i++) {
float t = static_cast<float>(i) / static_cast<float>(numArcSamples);
float angle = startAngle + t * range;
SbVec3f arcPoint = geom.center
+ SbVec3f(geom.radius * cos(angle), geom.radius * sin(angle), 0);
corners.push_back(arcPoint);
}
}
};
includeArcHelper(geom.startAngle, geom.startRange);
includeArcHelper(geom.endAngle, geom.endRange);
return corners;
}
std::vector<SbVec3f> computeAngleBBox() const
{
SbVec2s imgsize;
int nc {};
int srcw = 1;
int srch = 1;
const unsigned char* dataptr = label->image.getValue(imgsize, nc);
if (dataptr) {
srcw = imgsize[0];
srch = imgsize[1];
}
float aspectRatio = (float)srcw / (float)srch;
float imgHeight = scale * (float)(srch);
float imgWidth = aspectRatio * imgHeight;
// get the points stored in the pnt field
const SbVec3f* points = label->pnts.getValues(0);
if (label->pnts.getNum() < 1) {
return {};
}
// use the shared geometry calculation for consistency
SoDatumLabel::AngleGeometry geom = label->calculateAngleGeometry(points);
std::vector<SbVec3f> corners;
// include extension line endpoints
corners.push_back(geom.pnt1);
corners.push_back(geom.pnt2);
corners.push_back(geom.pnt3);
corners.push_back(geom.pnt4);
// include text label area
SbVec3f img1 = SbVec3f(-imgWidth / 2.0F, -imgHeight / 2, 0.0F);
SbVec3f img2 = SbVec3f(-imgWidth / 2.0F, imgHeight / 2, 0.0F);
SbVec3f img3 = SbVec3f(imgWidth / 2.0F, -imgHeight / 2, 0.0F);
SbVec3f img4 = SbVec3f(imgWidth / 2.0F, imgHeight / 2, 0.0F);
img1 += geom.textOffset;
img2 += geom.textOffset;
img3 += geom.textOffset;
img4 += geom.textOffset;
corners.push_back(img1);
corners.push_back(img2);
corners.push_back(img3);
corners.push_back(img4);
// include arrow head positions for better selection
corners.push_back(geom.startArrowBase);
corners.push_back(geom.endArrowBase);
// include arrow tips (base + direction * length)
corners.push_back(geom.startArrowBase + geom.dirStart * geom.arrowLength);
corners.push_back(geom.endArrowBase + geom.dirEnd * geom.arrowLength);
return corners;
}
std::vector<SbVec3f> computeSymmetricBBox() const
{
// get the points stored in the pnt field
const SbVec3f* points = label->pnts.getValues(0);
if (label->pnts.getNum() < 2) {
return {};
}
// use shared geometry calculation
SoDatumLabel::SymmetricGeometry geom = label->calculateSymmetricGeometry(points);
// include all visual elements in bounding box
std::vector<SbVec3f> corners;
// main points (existing)
corners.push_back(geom.p1);
corners.push_back(geom.p2);
// first arrow triangle points
corners.push_back(geom.ar0); // arrow tip
corners.push_back(geom.ar1); // arrow base point 1
corners.push_back(geom.ar2); // arrow base point 2
// second arrow triangle points
corners.push_back(geom.ar3); // arrow tip
corners.push_back(geom.ar4); // arrow base point 1
corners.push_back(geom.ar5); // arrow base point 2
return corners;
}
std::vector<SbVec3f> computeArcLengthBBox() const
{
// get the points stored in the pnt field
const SbVec3f* points = label->pnts.getValues(0);
if (label->pnts.getNum() < 3) {
return {};
}
// use shared geometry calculation
SoDatumLabel::ArcLengthGeometry geom = label->calculateArcLengthGeometry(points);
// get text area for existing text coverage
SbVec2s imgsize;
int nc {};
int srcw = 1;
int srch = 1;
const unsigned char* dataptr = label->image.getValue(imgsize, nc);
if (dataptr) {
srcw = imgsize[0];
srch = imgsize[1];
}
float aspectRatio = (float)srcw / (float)srch;
float imgHeight = scale * (float)(srch);
float imgWidth = aspectRatio * imgHeight;
// text orientation
SbVec3f dir = (geom.p2 - geom.p1);
dir.normalize();
SbVec3f normal = SbVec3f(-dir[1], dir[0], 0);
// include all visual elements in bounding box
std::vector<SbVec3f> corners;
// text area (existing coverage)
float margin = imgHeight / 4.0F;
corners.push_back(
geom.textOffset + dir * (imgWidth / 2.0F + margin) - normal * (imgHeight / 2.0F + margin)
);
corners.push_back(
geom.textOffset - dir * (imgWidth / 2.0F + margin) - normal * (imgHeight / 2.0F + margin)
);
corners.push_back(
geom.textOffset + dir * (imgWidth / 2.0F + margin) + normal * (imgHeight / 2.0F + margin)
);
corners.push_back(
geom.textOffset - dir * (imgWidth / 2.0F + margin) + normal * (imgHeight / 2.0F + margin)
);
// extension line endpoints
corners.push_back(geom.pnt1); // start point
corners.push_back(geom.pnt2); // extension end 1
corners.push_back(geom.pnt3); // end point
corners.push_back(geom.pnt4); // extension end 2
// arc sample points (8 points along the curve for better coverage)
int numSamples = 8;
for (int i = 0; i < numSamples; i++) {
float t = (float)i / (numSamples - 1);
float angle = geom.startangle + t * (geom.endangle - geom.startangle);
SbVec3f arcPoint = geom.arcCenter
+ SbVec3f(geom.arcRadius * cos(angle), geom.arcRadius * sin(angle), 0);
corners.push_back(arcPoint);
}
// arrow head tips (base + direction * length)
float arrowLength = geom.margin * 2;
SbVec3f startArrowTip = geom.pnt2 + geom.dirStart * arrowLength;
SbVec3f endArrowTip = geom.pnt4 + geom.dirEnd * arrowLength;
corners.push_back(startArrowTip);
corners.push_back(endArrowTip);
return corners;
}
private:
float scale;
SoDatumLabel* label;
};
} // namespace Gui
void SoDatumLabel::computeBBox(SoAction* action, SbBox3f& box, SbVec3f& center)
{
SoState* state = action->getState();
float scale = getScaleFactor(state);
Gui::DatumLabelBox datumBox(scale, this);
datumBox.computeBBox(box, center);
}
SbVec3f SoDatumLabel::getLabelTextCenter()
{
// Get the points stored
int numPts = this->pnts.getNum();
if (numPts < 2) {
return {};
}
const SbVec3f* points = this->pnts.getValues(0);
SbVec3f p1 = points[0];
SbVec3f p2 = points[1];
if (datumtype.getValue() == SoDatumLabel::DISTANCE
|| datumtype.getValue() == SoDatumLabel::DISTANCEX
|| datumtype.getValue() == SoDatumLabel::DISTANCEY) {
return getLabelTextCenterDistance(p1, p2);
}
if (datumtype.getValue() == SoDatumLabel::RADIUS
|| datumtype.getValue() == SoDatumLabel::DIAMETER) {
return getLabelTextCenterDiameter(p1, p2);
}
if (datumtype.getValue() == SoDatumLabel::ANGLE) {
return getLabelTextCenterAngle(p1);
}
if (datumtype.getValue() == SoDatumLabel::ARCLENGTH) {
if (numPts >= 3) {
SbVec3f p3 = points[2];
return getLabelTextCenterArcLength(p1, p2, p3);
}
}
return p1;
}
SbVec3f SoDatumLabel::getLabelTextCenterDistance(const SbVec3f& p1, const SbVec3f& p2)
{
float length = param1.getValue();
float length2 = param2.getValue();
SbVec3f dir;
SbVec3f normal;
constexpr float floatEpsilon = std::numeric_limits<float>::epsilon();
if (datumtype.getValue() == SoDatumLabel::DISTANCE) {
dir = (p2 - p1);
}
else if (datumtype.getValue() == SoDatumLabel::DISTANCEX) {
dir = SbVec3f((p2[0] - p1[0] >= floatEpsilon) ? 1 : -1, 0, 0);
}
else if (datumtype.getValue() == SoDatumLabel::DISTANCEY) {
dir = SbVec3f(0, (p2[1] - p1[1] >= floatEpsilon) ? 1 : -1, 0);
}
dir.normalize();
normal = SbVec3f(-dir[1], dir[0], 0);
float normproj12 = (p2 - p1).dot(normal);
SbVec3f p1_ = p1 + normproj12 * normal;
SbVec3f midpos = (p1_ + p2) / 2;
SbVec3f textCenter = midpos + normal * length + dir * length2;
return textCenter;
}
SbVec3f SoDatumLabel::getLabelTextCenterDiameter(const SbVec3f& p1, const SbVec3f& p2)
{
SbVec3f dir = (p2 - p1);
dir.normalize();
float length = this->param1.getValue();
SbVec3f textCenter = p2 + length * dir;
return textCenter;
}
SbVec3f SoDatumLabel::getLabelTextCenterAngle(const SbVec3f& p0)
{
// Load the Parameters
float length = param1.getValue();
float startangle = param2.getValue();
float range = param3.getValue();
float len2 = 2.0F * length;
// Useful Information
// v0 - vector for text position
// p0 - vector for angle intersect
SbVec3f v0(cos(startangle + range / 2), sin(startangle + range / 2), 0);
SbVec3f textCenter = p0 + v0 * len2;
return textCenter;
}
SbVec3f SoDatumLabel::getLabelTextCenterArcLength(
const SbVec3f& ctr,
const SbVec3f& p1,
const SbVec3f& p2
) const
{
float length = this->param1.getValue();
// Angles calculations
SbVec3f vc1 = (p1 - ctr);
SbVec3f vc2 = (p2 - ctr);
float startangle = atan2f(vc1[1], vc1[0]);
float endangle = atan2f(vc2[1], vc2[0]);
if (endangle < startangle) {
endangle += 2.F * std::numbers::pi_v<float>;
}
// Text location
SbVec3f vm = (p1 + p2) / 2 - ctr;
vm.normalize();
SbVec3f textCenter;
if (endangle - startangle <= std::numbers::pi) {
textCenter = ctr + vm * (length + this->imgHeight);
}
else {
textCenter = ctr - vm * (length + 2. * this->imgHeight);
}
return textCenter;
}
void SoDatumLabel::generateDistancePrimitives(SoAction* action, const SbVec3f& p1, const SbVec3f& p2)
{
SbVec3f points[2] = {p1, p2};
DistanceGeometry geom = calculateDistanceGeometry(points);
// generate selectable primitive for txt label at elevated Z for selection above geometry
SbVec3f img1 = SbVec3f(-this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img2 = SbVec3f(-this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img3 = SbVec3f(this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img4 = SbVec3f(this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
float s = sin(geom.angle);
float c = cos(geom.angle);
img1 = SbVec3f((img1[0] * c) - (img1[1] * s), (img1[0] * s) + (img1[1] * c), ZARROW_TEXT_OFFSET);
img2 = SbVec3f((img2[0] * c) - (img2[1] * s), (img2[0] * s) + (img2[1] * c), ZARROW_TEXT_OFFSET);
img3 = SbVec3f((img3[0] * c) - (img3[1] * s), (img3[0] * s) + (img3[1] * c), ZARROW_TEXT_OFFSET);
img4 = SbVec3f((img4[0] * c) - (img4[1] * s), (img4[0] * s) + (img4[1] * c), ZARROW_TEXT_OFFSET);
img1 += geom.textOffset;
img2 += geom.textOffset;
img3 += geom.textOffset;
img4 += geom.textOffset;
// text label selection primitive
SoPrimitiveVertex pv;
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
this->beginShape(action, TRIANGLE_STRIP);
pv.setPoint(img1);
shapeVertex(&pv);
pv.setPoint(img2);
shapeVertex(&pv);
pv.setPoint(img3);
shapeVertex(&pv);
pv.setPoint(img4);
shapeVertex(&pv);
this->endShape();
// beginning of generation of selectable primitives for lines
float lineWidth = geom.margin * 0.8f; // adjust the width for selection
// ext lines
generateLineSelectionPrimitive(action, geom.p1, geom.perp1, lineWidth);
generateLineSelectionPrimitive(action, geom.p2, geom.perp2, lineWidth);
// dim lines
generateLineSelectionPrimitive(action, geom.par1, geom.par2, lineWidth);
generateLineSelectionPrimitive(action, geom.par3, geom.par4, lineWidth);
// begin generation of selectable primitives for arrow-heads at elevated Z
this->beginShape(action, TRIANGLES);
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
// 1st arrow-head
pv.setPoint(SbVec3f(geom.par1[0], geom.par1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar1[0], geom.ar1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar2[0], geom.ar2[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
// 2nd arrow-head
pv.setPoint(SbVec3f(geom.par4[0], geom.par4[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar3[0], geom.ar3[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar4[0], geom.ar4[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
this->endShape();
}
void SoDatumLabel::generateDiameterPrimitives(SoAction* action, const SbVec3f& p1, const SbVec3f& p2)
{
SbVec3f points[2] = {p1, p2};
DiameterGeometry geom = calculateDiameterGeometry(points);
// generate selectable primitive for text label at elevated Z for selection above geometry
SbVec3f img1 = SbVec3f(-this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img2 = SbVec3f(-this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img3 = SbVec3f(this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img4 = SbVec3f(this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
float s = sin(geom.angle);
float c = cos(geom.angle);
img1 = SbVec3f((img1[0] * c) - (img1[1] * s), (img1[0] * s) + (img1[1] * c), ZARROW_TEXT_OFFSET);
img2 = SbVec3f((img2[0] * c) - (img2[1] * s), (img2[0] * s) + (img2[1] * c), ZARROW_TEXT_OFFSET);
img3 = SbVec3f((img3[0] * c) - (img3[1] * s), (img3[0] * s) + (img3[1] * c), ZARROW_TEXT_OFFSET);
img4 = SbVec3f((img4[0] * c) - (img4[1] * s), (img4[0] * s) + (img4[1] * c), ZARROW_TEXT_OFFSET);
img1 += geom.textOffset;
img2 += geom.textOffset;
img3 += geom.textOffset;
img4 += geom.textOffset;
// txt label selection primitive
SoPrimitiveVertex pv;
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
this->beginShape(action, TRIANGLE_STRIP);
pv.setPoint(img1);
shapeVertex(&pv);
pv.setPoint(img2);
shapeVertex(&pv);
pv.setPoint(img3);
shapeVertex(&pv);
pv.setPoint(img4);
shapeVertex(&pv);
this->endShape();
// generate selectable primitives for dimension lines
float lineWidth = geom.margin * 0.8f;
// main dimension lines (from center/start to text area and from text area to end)
generateLineSelectionPrimitive(action, geom.p1, geom.pnt1, lineWidth);
generateLineSelectionPrimitive(action, geom.pnt2, geom.p2, lineWidth);
// Generate selectable primitives for arrow heads at elevated Z
this->beginShape(action, TRIANGLES);
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
// first arrow-head
pv.setPoint(SbVec3f(geom.ar0[0], geom.ar0[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar1[0], geom.ar1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar2[0], geom.ar2[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
// second arrow-head but only for diameter
if (geom.isDiameter) {
pv.setPoint(SbVec3f(geom.ar0_1[0], geom.ar0_1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar1_1[0], geom.ar1_1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar2_1[0], geom.ar2_1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
}
this->endShape();
const auto generateSelectablePrimitiveForArcHelper = [&, this](float startAngle, float range) {
if (range != 0.0) {
int countSegments = std::max(6, abs(int(50.0 * range / (2 * std::numbers::pi))));
double segment = range / (countSegments - 1);
// create selectable line segments for the arc
for (int i = 0; i < countSegments - 1; i++) {
double theta1 = startAngle + segment * i;
double theta2 = startAngle + segment * (i + 1);
SbVec3f v1 = geom.center
+ SbVec3f(geom.radius * cos(theta1), geom.radius * sin(theta1), 0);
SbVec3f v2 = geom.center
+ SbVec3f(geom.radius * cos(theta2), geom.radius * sin(theta2), 0);
generateLineSelectionPrimitive(action, v1, v2, lineWidth * 0.5f);
}
}
};
generateSelectablePrimitiveForArcHelper(geom.startAngle, geom.startRange);
generateSelectablePrimitiveForArcHelper(geom.endAngle, geom.endRange);
}
void SoDatumLabel::generateAnglePrimitives(SoAction* action, const SbVec3f& p0)
{
// use shared geometry calculation
SbVec3f points[1] = {p0};
AngleGeometry geom = calculateAngleGeometry(points);
// generate selectable primitive for text label at elevated Z for selection above geometry
SbVec3f img1 = SbVec3f(-this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img2 = SbVec3f(-this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img3 = SbVec3f(this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img4 = SbVec3f(this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
img1 += geom.textOffset;
img2 += geom.textOffset;
img3 += geom.textOffset;
img4 += geom.textOffset;
// text label selection primitive
SoPrimitiveVertex pv;
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
this->beginShape(action, TRIANGLE_STRIP);
pv.setPoint(img1);
shapeVertex(&pv);
pv.setPoint(img2);
shapeVertex(&pv);
pv.setPoint(img3);
shapeVertex(&pv);
pv.setPoint(img4);
shapeVertex(&pv);
this->endShape();
// generate selectable primitives for dimension lines
float lineWidth = geom.margin * 0.8f;
// extension lines
generateLineSelectionPrimitive(action, geom.pnt1, geom.pnt2, lineWidth);
generateLineSelectionPrimitive(action, geom.pnt3, geom.pnt4, lineWidth);
// generate selectable primitives for arc segments
float arcWidth = geom.margin * 0.6f;
// arc before text
generateArcSelectionPrimitive(
action,
geom.p0,
geom.r,
geom.startangle,
geom.startangle + geom.range / 2.0 - geom.textMargin,
arcWidth
);
// arc after text
generateArcSelectionPrimitive(
action,
geom.p0,
geom.r,
geom.startangle + geom.range / 2.0 + geom.textMargin,
geom.endangle,
arcWidth
);
// generate selectable primitives for arrow heads
generateArrowSelectionPrimitive(
action,
geom.startArrowBase,
geom.dirStart,
geom.arrowWidth,
geom.arrowLength
);
generateArrowSelectionPrimitive(
action,
geom.endArrowBase,
geom.dirEnd,
geom.arrowWidth,
geom.arrowLength
);
}
void SoDatumLabel::generateSymmetricPrimitives(SoAction* action, const SbVec3f& p1, const SbVec3f& p2)
{
// use shared geometry calculation
SbVec3f points[2] = {p1, p2};
SymmetricGeometry geom = calculateSymmetricGeometry(points);
// generate selectable primitives for lines
float lineWidth = geom.margin * 0.8f;
// lines from endpoints to arrow tips
generateLineSelectionPrimitive(action, geom.p1, geom.ar0, lineWidth);
generateLineSelectionPrimitive(action, geom.p2, geom.ar3, lineWidth);
// generate selectable primitives for arrow heads as triangles at elevated Z
SoPrimitiveVertex pv;
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
this->beginShape(action, TRIANGLES);
// first arrow
pv.setPoint(SbVec3f(geom.ar0[0], geom.ar0[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar1[0], geom.ar1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar2[0], geom.ar2[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
// second arrow
pv.setPoint(SbVec3f(geom.ar3[0], geom.ar3[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar4[0], geom.ar4[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(geom.ar5[0], geom.ar5[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
this->endShape();
}
void SoDatumLabel::generateArcLengthPrimitives(
SoAction* action,
const SbVec3f& ctr,
const SbVec3f& p1,
const SbVec3f& p2
)
{
// use shared geometry calculation
SbVec3f points[3] = {ctr, p1, p2};
ArcLengthGeometry geom = calculateArcLengthGeometry(points);
// generate selectable primitive for text label at elevated Z for selection above geometry
SbVec3f img1 = SbVec3f(-this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img2 = SbVec3f(-this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img3 = SbVec3f(this->imgWidth / 2, -this->imgHeight / 2, ZARROW_TEXT_OFFSET);
SbVec3f img4 = SbVec3f(this->imgWidth / 2, this->imgHeight / 2, ZARROW_TEXT_OFFSET);
float s = sin(geom.angle);
float c = cos(geom.angle);
img1 = SbVec3f((img1[0] * c) - (img1[1] * s), (img1[0] * s) + (img1[1] * c), ZARROW_TEXT_OFFSET);
img2 = SbVec3f((img2[0] * c) - (img2[1] * s), (img2[0] * s) + (img2[1] * c), ZARROW_TEXT_OFFSET);
img3 = SbVec3f((img3[0] * c) - (img3[1] * s), (img3[0] * s) + (img3[1] * c), ZARROW_TEXT_OFFSET);
img4 = SbVec3f((img4[0] * c) - (img4[1] * s), (img4[0] * s) + (img4[1] * c), ZARROW_TEXT_OFFSET);
img1 += geom.textOffset;
img2 += geom.textOffset;
img3 += geom.textOffset;
img4 += geom.textOffset;
// text label selection primitive
SoPrimitiveVertex pv;
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
this->beginShape(action, TRIANGLE_STRIP);
pv.setPoint(img1);
shapeVertex(&pv);
pv.setPoint(img2);
shapeVertex(&pv);
pv.setPoint(img3);
shapeVertex(&pv);
pv.setPoint(img4);
shapeVertex(&pv);
this->endShape();
// generate selectable primitives for lines
float lineWidth = geom.margin * 0.8f;
// extension lines
generateLineSelectionPrimitive(action, geom.pnt1, geom.pnt2, lineWidth);
generateLineSelectionPrimitive(action, geom.pnt3, geom.pnt4, lineWidth);
// generate selectable primitive for arc
generateArcSelectionPrimitive(
action,
geom.arcCenter,
geom.arcRadius,
geom.startangle,
geom.endangle,
lineWidth
);
// generate selectable primitives for arrow heads
float arrowLength = geom.margin * 2;
float arrowWidth = geom.margin * 0.5F;
generateArrowSelectionPrimitive(action, geom.pnt2, geom.dirStart, arrowWidth, arrowLength);
generateArrowSelectionPrimitive(action, geom.pnt4, geom.dirEnd, arrowWidth, arrowLength);
}
void SoDatumLabel::generatePrimitives(SoAction* action)
{
// Initialisation check (needs something more sensible) prevents an infinite loop bug
constexpr float floatEpsilon = std::numeric_limits<float>::epsilon();
if (this->imgHeight <= floatEpsilon || this->imgWidth <= floatEpsilon) {
return;
}
int numPts = this->pnts.getNum();
if (numPts < 2) {
return;
}
// Get the points stored
const SbVec3f* points = this->pnts.getValues(0);
SbVec3f p1 = points[0];
SbVec3f p2 = points[1];
// Change the offset and bounding box parameters depending on Datum Type
if (this->datumtype.getValue() == DISTANCE || this->datumtype.getValue() == DISTANCEX
|| this->datumtype.getValue() == DISTANCEY) {
generateDistancePrimitives(action, p1, p2);
}
else if (this->datumtype.getValue() == RADIUS || this->datumtype.getValue() == DIAMETER) {
generateDiameterPrimitives(action, p1, p2);
}
else if (this->datumtype.getValue() == ANGLE) {
generateAnglePrimitives(action, p1);
}
else if (this->datumtype.getValue() == SYMMETRIC) {
generateSymmetricPrimitives(action, p1, p2);
}
else if (this->datumtype.getValue() == ARCLENGTH) {
if (numPts >= 3) {
SbVec3f p3 = points[2];
generateArcLengthPrimitives(action, p1, p2, p3);
}
}
}
void SoDatumLabel::notify(SoNotList* l)
{
SoField* f = l->getLastField();
if (f == &this->string) {
this->glimagevalid = false;
}
else if (f == &this->textColor) {
this->glimagevalid = false;
}
else if (f == &this->name) {
this->glimagevalid = false;
}
else if (f == &this->size) {
this->glimagevalid = false;
}
else if (f == &this->image) {
this->glimagevalid = false;
}
inherited::notify(l);
}
float SoDatumLabel::getScaleFactor(SoState* state) const
{
/**Remark from Stefan Tröger:
* The scale calculation is based on knowledge of SbViewVolume::getWorldToScreenScale
* implementation internals. The factor returned from this function is calculated from the view
* frustums nearplane width, height is not taken into account, and hence we divide it with the
* viewport width to get the exact pixel scale factor. This is not documented and therefore may
* change on later coin versions!
*/
const SbViewVolume& vv = SoViewVolumeElement::get(state);
// As reference use the center point the camera is looking at on the focal plane
// because then independent of the camera we get a constant scale factor when panning.
// If we used (0,0,0) instead then the scale factor would change heavily in perspective
// rendering mode. See #0002921 and #0002922.
// It's important to use the distance to the focal plane an not near or far plane because
// depending on additionally displayed objects they may change heavily and thus impact the
// scale factor. See #7082 and #7860.
float focal = SoFocalDistanceElement::get(state);
SbVec3f center = vv.getSightPoint(focal);
float scale = vv.getWorldToScreenScale(center, 1.F);
const SbViewportRegion& vp = SoViewportRegionElement::get(state);
SbVec2s vp_size = vp.getViewportSizePixels();
scale /= float(vp_size[0]);
return scale;
}
void SoDatumLabel::GLRender(SoGLRenderAction* action)
{
SoState* state = action->getState();
if (!shouldGLRender(action)) {
return;
}
if (action->handleTransparency(true)) {
return;
}
const float scale = getScaleFactor(state);
bool hasText = hasDatumText();
int srcw = 1;
int srch = 1;
if (hasText) {
getDimension(scale, srcw, srch);
}
if (this->datumtype.getValue() == SYMMETRIC) {
this->imgHeight = scale * 25.0F;
this->imgWidth = scale * 25.0F;
}
// Get the points stored in the pnt field
const SbVec3f* points = this->pnts.getValues(0);
state->push();
// Set General OpenGL Properties
glPushAttrib(GL_ENABLE_BIT | GL_PIXEL_MODE_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDisable(GL_LIGHTING);
glDisable(GL_CULL_FACE);
// Explicitly enable depth testing for constraint lines. This is needed because the
// constraint group may have depth testing disabled (to allow icons to render on top),
// but we want constraint LINES to render BELOW geometry lines.
// Arrowheads are drawn at elevated Z (ZARROW_TEXT_OFFSET) so they render on top.
// Text rendering disables depth testing separately.
glEnable(GL_DEPTH_TEST);
// Enable Anti-alias
if (action->isSmoothing()) {
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
}
// Position for Datum Text Label
float angle = 0;
// Get the colour
const SbColor& t = textColor.getValue();
// Set GL Properties
glLineWidth(this->lineWidth.getValue());
glColor3f(t[0], t[1], t[2]);
SbVec3f textOffset;
if (this->datumtype.getValue() == DISTANCE || this->datumtype.getValue() == DISTANCEX
|| this->datumtype.getValue() == DISTANCEY) {
drawDistance(points, angle, textOffset);
}
else if (this->datumtype.getValue() == RADIUS || this->datumtype.getValue() == DIAMETER) {
drawRadiusOrDiameter(points, angle, textOffset);
}
else if (this->datumtype.getValue() == ANGLE) {
drawAngle(points, angle, textOffset);
}
else if (this->datumtype.getValue() == SYMMETRIC) {
drawSymmetric(points);
}
else if (this->datumtype.getValue() == ARCLENGTH) {
drawArcLength(points, angle, textOffset);
}
if (hasText) {
drawText(state, srcw, srch, angle, textOffset);
}
glPopAttrib();
state->pop();
}
bool SoDatumLabel::hasDatumText() const
{
const SbString* s = string.getValues(0);
return (s->getLength() > 0);
}
void SoDatumLabel::getDimension(float scale, int& srcw, int& srch)
{
SbVec2s imgsize;
int nc {};
if (!this->glimagevalid) {
drawImage();
this->glimagevalid = true;
}
const unsigned char* dataptr = this->image.getValue(imgsize, nc);
if (!dataptr) { // no image
return;
}
srcw = imgsize[0];
srch = imgsize[1];
float aspectRatio = (float)srcw / (float)srch;
this->imgHeight = scale * (float)(srch) / sampling.getValue();
this->imgWidth = aspectRatio * (float)this->imgHeight;
}
void SoDatumLabel::drawDistance(const SbVec3f* points, float& angle, SbVec3f& textOffset)
{
SoDatumLabel::DistanceGeometry geom = this->calculateDistanceGeometry(points);
angle = geom.angle;
textOffset = geom.textOffset;
// Get the colour
const SbColor& t = textColor.getValue();
// Set GL Properties
glLineWidth(this->lineWidth.getValue());
glColor3f(t[0], t[1], t[2]);
// Perp Lines
glBegin(GL_LINES);
if (this->param1.getValue() != 0.) {
glVertex2f(geom.p1[0], geom.p1[1]);
glVertex2f(geom.perp1[0], geom.perp1[1]);
glVertex2f(geom.p2[0], geom.p2[1]);
glVertex2f(geom.perp2[0], geom.perp2[1]);
}
glVertex2f(geom.par1[0], geom.par1[1]);
glVertex2f(geom.par2[0], geom.par2[1]);
glVertex2f(geom.par3[0], geom.par3[1]);
glVertex2f(geom.par4[0], geom.par4[1]);
glEnd();
// Draw the arrowheads at elevated Z to render ON TOP of geometry lines
glBegin(GL_TRIANGLES);
glVertex3f(geom.par1[0], geom.par1[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar1[0], geom.ar1[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar2[0], geom.ar2[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.par4[0], geom.par4[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar3[0], geom.ar3[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar4[0], geom.ar4[1], ZARROW_TEXT_OFFSET);
glEnd();
if (this->datumtype.getValue() == DISTANCE) {
drawDistance(points);
}
}
void SoDatumLabel::drawDistance(const SbVec3f* points)
{
// Draw arc helpers if needed
float range1 = this->param4.getValue();
if (range1 != 0.0) {
float startAngle1 = this->param3.getValue();
float radius1 = this->param5.getValue();
SbVec3f center1 = points[2];
glDrawArc(center1, radius1, startAngle1, startAngle1 + range1);
}
float range2 = this->param7.getValue();
if (range2 != 0.0) {
float startAngle2 = this->param6.getValue();
float radius2 = this->param8.getValue();
SbVec3f center2 = points[3];
glDrawArc(center2, radius2, startAngle2, startAngle2 + range2);
}
}
void SoDatumLabel::drawRadiusOrDiameter(const SbVec3f* points, float& angle, SbVec3f& textOffset)
{
// Use shared geometry calculation
DiameterGeometry geom = calculateDiameterGeometry(points);
angle = geom.angle;
textOffset = geom.textOffset;
// Draw the Lines
glBegin(GL_LINES);
glVertex2f(geom.p1[0], geom.p1[1]);
glVertex2f(geom.pnt1[0], geom.pnt1[1]);
glVertex2f(geom.pnt2[0], geom.pnt2[1]);
glVertex2f(geom.p2[0], geom.p2[1]);
glEnd();
// Draw arrowhead at elevated Z to render ON TOP of geometry lines
glBegin(GL_TRIANGLES);
glVertex3f(geom.ar0[0], geom.ar0[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar1[0], geom.ar1[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar2[0], geom.ar2[1], ZARROW_TEXT_OFFSET);
glEnd();
if (geom.isDiameter) {
// Draw second arrowhead at elevated Z
glBegin(GL_TRIANGLES);
glVertex3f(geom.ar0_1[0], geom.ar0_1[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar1_1[0], geom.ar1_1[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar2_1[0], geom.ar2_1[1], ZARROW_TEXT_OFFSET);
glEnd();
}
// Draw arc helpers if needed
if (geom.startRange != 0.0) {
glDrawArc(geom.center, geom.radius, geom.startAngle, geom.startAngle + geom.startRange);
}
if (geom.endRange != 0.0) {
glDrawArc(geom.center, geom.radius, geom.endAngle, geom.endAngle + geom.endRange);
}
}
void SoDatumLabel::drawAngle(const SbVec3f* points, float& angle, SbVec3f& textOffset)
{
// use shared geometry calculation
AngleGeometry geom = calculateAngleGeometry(points);
angle = geom.angle;
textOffset = geom.textOffset;
// draw arc segments
glDrawArc(geom.p0, geom.r, geom.startangle, geom.startangle + geom.range / 2.0 - geom.textMargin);
glDrawArc(geom.p0, geom.r, geom.startangle + geom.range / 2.0 + geom.textMargin, geom.endangle);
// draw extension lines
glDrawLine(geom.pnt1, geom.pnt2);
glDrawLine(geom.pnt3, geom.pnt4);
// draw arrowheads
glDrawArrow(geom.startArrowBase, geom.dirStart, geom.arrowWidth, geom.arrowLength);
glDrawArrow(geom.endArrowBase, geom.dirEnd, geom.arrowWidth, geom.arrowLength);
}
void SoDatumLabel::drawSymmetric(const SbVec3f* points)
{
// use shared geometry calculation
SymmetricGeometry geom = calculateSymmetricGeometry(points);
// draw first constraint line (at constraint Z)
glBegin(GL_LINES);
glVertex3f(geom.p1[0], geom.p1[1], ZCONSTR);
glVertex3f(geom.ar0[0], geom.ar0[1], ZCONSTR);
glEnd();
// draw first arrowhead at elevated Z to render ON TOP of geometry lines
glBegin(GL_LINES);
glVertex3f(geom.ar0[0], geom.ar0[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar1[0], geom.ar1[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar0[0], geom.ar0[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar2[0], geom.ar2[1], ZARROW_TEXT_OFFSET);
glEnd();
// draw second constraint line (at constraint Z)
glBegin(GL_LINES);
glVertex3f(geom.p2[0], geom.p2[1], ZCONSTR);
glVertex3f(geom.ar3[0], geom.ar3[1], ZCONSTR);
glEnd();
// draw second arrowhead at elevated Z to render ON TOP of geometry lines
glBegin(GL_LINES);
glVertex3f(geom.ar3[0], geom.ar3[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar4[0], geom.ar4[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar3[0], geom.ar3[1], ZARROW_TEXT_OFFSET);
glVertex3f(geom.ar5[0], geom.ar5[1], ZARROW_TEXT_OFFSET);
glEnd();
}
void SoDatumLabel::drawArcLength(const SbVec3f* points, float& angle, SbVec3f& textOffset)
{
// use shared geometry calculation
ArcLengthGeometry geom = calculateArcLengthGeometry(points);
// set output parameters
angle = geom.angle;
textOffset = geom.textOffset;
// draw arc
glDrawArc(geom.arcCenter, geom.arcRadius, geom.startangle, geom.endangle);
// draw lines
glDrawLine(geom.pnt1, geom.pnt2);
glDrawLine(geom.pnt3, geom.pnt4);
// create the arrowheads
float arrowLength = geom.margin * 2;
float arrowWidth = geom.margin * 0.5F;
glDrawArrow(geom.pnt2, geom.dirStart, arrowWidth, arrowLength);
glDrawArrow(geom.pnt4, geom.dirEnd, arrowWidth, arrowLength);
}
// NOLINTNEXTLINE
void SoDatumLabel::drawText(SoState* state, int srcw, int srch, float angle, const SbVec3f& textOffset)
{
SbVec2s imgsize;
int nc {};
const unsigned char* dataptr = this->image.getValue(imgsize, nc);
// Get the camera z-direction
const SbViewVolume& vv = SoViewVolumeElement::get(state);
SbVec3f z = vv.zVector();
bool flip = norm.getValue().dot(z) > std::numeric_limits<float>::epsilon();
static bool init = false;
static bool npot = false;
if (!init) {
init = true;
std::string ext = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS)); // NOLINT
npot = (ext.find("GL_ARB_texture_non_power_of_two") != std::string::npos);
}
int w = srcw;
int h = srch;
if (!npot) {
// make power of two
if ((w & (w - 1)) != 0) {
int i = 1;
while (i < 8) {
if ((w >> i) == 0) {
break;
}
i++;
}
w = (1 << i);
}
// make power of two
if ((h & (h - 1)) != 0) {
int i = 1;
while (i < 8) {
if ((h >> i) == 0) {
break;
}
i++;
}
h = (1 << i);
}
}
glDisable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D); // Enable Textures
glEnable(GL_BLEND);
// glGenTextures/glBindTexture was commented out but it must be active, see:
// #0000971: Tracing over a background image in Sketcher: image is overwritten by first
// dimensional constraint text #0001185: Planer image changes to number graphic when a part
// design constraint is made after the planar image
//
// Copy the text bitmap into memory and bind
GLuint myTexture {};
// generate a texture
glGenTextures(1, &myTexture);
glBindTexture(GL_TEXTURE_2D, myTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
if (!npot) {
QImage imagedata(w, h, QImage::Format_ARGB32_Premultiplied);
imagedata.fill(0x00000000);
int sx = (w - srcw) / 2;
int sy = (h - srch) / 2;
glTexImage2D(
GL_TEXTURE_2D,
0,
nc,
w,
h,
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
(const GLvoid*)imagedata.bits()
);
glTexSubImage2D(
GL_TEXTURE_2D,
0,
sx,
sy,
srcw,
srch,
GL_RGBA,
GL_UNSIGNED_BYTE,
(const GLvoid*)dataptr
);
}
else {
glTexImage2D(GL_TEXTURE_2D, 0, nc, srcw, srch, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid*)dataptr);
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
// Apply a rotation and translation matrix
glTranslatef(textOffset[0], textOffset[1], textOffset[2]);
glRotatef(Base::toDegrees<GLfloat>(angle), 0, 0, 1);
glBegin(GL_QUADS);
glColor3f(1.F, 1.F, 1.F);
glTexCoord2f(flip ? 0.F : 1.F, 1.F);
glVertex2f(-this->imgWidth / 2, this->imgHeight / 2);
glTexCoord2f(flip ? 0.F : 1.F, 0.F);
glVertex2f(-this->imgWidth / 2, -this->imgHeight / 2);
glTexCoord2f(flip ? 1.F : 0.F, 0.F);
glVertex2f(this->imgWidth / 2, -this->imgHeight / 2);
glTexCoord2f(flip ? 1.F : 0.F, 1.F);
glVertex2f(this->imgWidth / 2, this->imgHeight / 2);
glEnd();
// Reset the Mode
glPopMatrix();
// wmayer: see bug report below which is caused by generating but not
// deleting the texture.
// #0000721: massive memory leak when dragging an unconstrained model
glDeleteTextures(1, &myTexture);
}
void SoDatumLabel::setPoints(SbVec3f p1, SbVec3f p2)
{
pnts.setNum(2);
SbVec3f* verts = pnts.startEditing();
verts[0] = p1;
verts[1] = p2;
pnts.finishEditing();
}
// NOLINTEND(readability-magic-numbers,cppcoreguidelines-pro-bounds-pointer-arithmetic)
SoDatumLabel::DistanceGeometry SoDatumLabel::calculateDistanceGeometry(const SbVec3f* points) const
{
using std::numbers::pi;
SoDatumLabel::DistanceGeometry geom;
float length = this->param1.getValue();
float length2 = this->param2.getValue();
geom.p1 = points[0];
geom.p2 = points[1];
constexpr float floatEpsilon = std::numeric_limits<float>::epsilon();
if (this->datumtype.getValue() == DISTANCE) {
geom.dir = (geom.p2 - geom.p1);
}
else if (this->datumtype.getValue() == DISTANCEX) {
geom.dir = SbVec3f((geom.p2[0] - geom.p1[0] >= floatEpsilon) ? 1 : -1, 0, 0);
}
else if (this->datumtype.getValue() == DISTANCEY) {
geom.dir = SbVec3f(0, (geom.p2[1] - geom.p1[1] >= floatEpsilon) ? 1 : -1, 0);
}
geom.dir.normalize();
geom.normal = SbVec3f(-geom.dir[1], geom.dir[0], 0);
// when the datum line is not parallel to p1-p2 the projection of
// p1-p2 on normal is not zero, p2 is considered as reference and p1
// is replaced by its projection p1_
float normproj12 = (geom.p2 - geom.p1).dot(geom.normal);
SbVec3f p1_ = geom.p1 + normproj12 * geom.normal;
geom.midpos = (p1_ + geom.p2) / 2;
// Get magnitude of angle between horizontal
geom.angle = atan2f(geom.dir[1], geom.dir[0]);
if (geom.angle > pi / 2 + pi / 12) {
geom.angle -= (float)pi;
}
else if (geom.angle <= -pi / 2 + pi / 12) {
geom.angle += (float)pi;
}
geom.textOffset = geom.midpos + geom.normal * length + geom.dir * length2;
geom.margin = this->imgHeight / 3.0F;
float offset1 = ((length + normproj12 < 0) ? -1.F : 1.F) * geom.margin;
float offset2 = ((length < 0) ? -1 : 1) * geom.margin;
geom.perp1 = p1_ + geom.normal * (length + offset1);
geom.perp2 = geom.p2 + geom.normal * (length + offset2);
// Calculate the coordinates for the parallel datum lines
geom.par1 = p1_ + geom.normal * length;
geom.par2 = geom.midpos + geom.normal * length
+ geom.dir * (length2 - this->imgWidth / 2 - geom.margin);
geom.par3 = geom.midpos + geom.normal * length
+ geom.dir * (length2 + this->imgWidth / 2 + geom.margin);
geom.par4 = geom.p2 + geom.normal * length;
geom.flipTriang = false;
if ((geom.par3 - geom.par1).dot(geom.dir) > (geom.par4 - geom.par1).length()) {
// Increase Margin to improve visibility
float tmpMargin = this->imgHeight / 0.75F;
geom.par3 = geom.par4;
if ((geom.par2 - geom.par1).dot(geom.dir) > (geom.par4 - geom.par1).length()) {
geom.par3 = geom.par2;
geom.par2 = geom.par1 - geom.dir * tmpMargin;
geom.flipTriang = true;
}
}
else if ((geom.par2 - geom.par1).dot(geom.dir) < 0.F) {
float tmpMargin = this->imgHeight / 0.75F;
geom.par2 = geom.par1;
if ((geom.par3 - geom.par1).dot(geom.dir) < 0.F) {
geom.par2 = geom.par3;
geom.par3 = geom.par4 + geom.dir * tmpMargin;
geom.flipTriang = true;
}
}
geom.arrowWidth = geom.margin * 0.5F;
geom.ar1 = geom.par1 + ((geom.flipTriang) ? -1 : 1) * geom.dir * 0.866F * 2 * geom.margin;
geom.ar2 = geom.ar1 + geom.normal * geom.arrowWidth;
geom.ar1 -= geom.normal * geom.arrowWidth;
geom.ar3 = geom.par4 - ((geom.flipTriang) ? -1 : 1) * geom.dir * 0.866F * 2 * geom.margin;
geom.ar4 = geom.ar3 + geom.normal * geom.arrowWidth;
geom.ar3 -= geom.normal * geom.arrowWidth;
return geom;
}
SoDatumLabel::DiameterGeometry SoDatumLabel::calculateDiameterGeometry(const SbVec3f* points) const
{
DiameterGeometry geom;
// Get the Points
geom.p1 = points[0];
geom.p2 = points[1];
geom.dir = (geom.p2 - geom.p1);
geom.center = geom.p1;
geom.radius = (geom.p2 - geom.p1).length();
geom.isDiameter = (this->datumtype.getValue() == DIAMETER);
if (geom.isDiameter) {
geom.center = (geom.p1 + geom.p2) / 2;
geom.radius = geom.radius / 2;
}
geom.dir.normalize();
geom.normal = SbVec3f(-geom.dir[1], geom.dir[0], 0);
float length = this->param1.getValue();
geom.pos = geom.p2 + length * geom.dir;
// Get magnitude of angle between horizontal
geom.angle = atan2f(geom.dir[1], geom.dir[0]);
if (geom.angle > std::numbers::pi / 2 + std::numbers::pi / 12) {
geom.angle -= (float)std::numbers::pi;
}
else if (geom.angle <= -std::numbers::pi / 2 + std::numbers::pi / 12) {
geom.angle += (float)std::numbers::pi;
}
geom.textOffset = geom.pos;
geom.margin = this->imgHeight / 3.0F;
// Create the first arrowhead
geom.arrowWidth = geom.margin * 0.5F;
geom.ar0 = geom.p2;
geom.ar1 = geom.p2 - geom.dir * 0.866F * 2 * geom.margin;
geom.ar2 = geom.ar1 + geom.normal * geom.arrowWidth;
geom.ar1 -= geom.normal * geom.arrowWidth;
SbVec3f p3 = geom.pos + geom.dir * (this->imgWidth / 2 + geom.margin);
if ((p3 - geom.p1).length() > (geom.p2 - geom.p1).length()) {
geom.p2 = p3;
}
// Calculate the line segment points around text
geom.pnt1 = geom.pos - geom.dir * (geom.margin + this->imgWidth / 2);
geom.pnt2 = geom.pos + geom.dir * (geom.margin + this->imgWidth / 2);
if (geom.isDiameter) {
// Create second arrowhead for diameter
geom.ar0_1 = geom.p1;
geom.ar1_1 = geom.p1 + geom.dir * 0.866F * 2 * geom.margin;
geom.ar2_1 = geom.ar1_1 + geom.normal * geom.arrowWidth;
geom.ar1_1 -= geom.normal * geom.arrowWidth;
}
// Arc helper parameters
geom.startAngle = this->param3.getValue();
geom.startRange = this->param4.getValue();
geom.endAngle = this->param5.getValue();
geom.endRange = this->param6.getValue();
return geom;
}
SoDatumLabel::AngleGeometry SoDatumLabel::calculateAngleGeometry(const SbVec3f* points) const
{
AngleGeometry geom;
// only the angle intersection point is needed
geom.p0 = points[0];
geom.margin = this->imgHeight / 3.0F;
// load the parameters
geom.length = this->param1.getValue();
geom.startangle = this->param2.getValue();
geom.range = this->param3.getValue();
geom.endangle = geom.startangle + geom.range;
geom.endLineLength1 = std::max(this->param4.getValue(), geom.margin);
geom.endLineLength2 = std::max(this->param5.getValue(), geom.margin);
geom.endLineLength12 = std::max(-this->param4.getValue(), geom.margin);
geom.endLineLength22 = std::max(-this->param5.getValue(), geom.margin);
geom.r = 2 * geom.length;
// set the text label angle to zero
geom.angle = 0.F;
// useful information
// v0 - vector for text position
// p0 - vector for angle intersect
geom.v0 = SbVec3f(cos(geom.startangle + geom.range / 2), sin(geom.startangle + geom.range / 2), 0);
// leave some space for the text
geom.textMargin = std::min(0.2F * abs(geom.range), this->imgWidth / (2 * geom.r));
geom.textOffset = geom.p0 + geom.v0 * geom.r;
// direction vectors for start and end lines
geom.v1 = SbVec3f(cos(geom.startangle), sin(geom.startangle), 0);
geom.v2 = SbVec3f(cos(geom.endangle), sin(geom.endangle), 0);
if (geom.range < 0 || geom.length < 0) {
std::swap(geom.v1, geom.v2);
geom.textMargin = -geom.textMargin;
}
geom.pnt1 = geom.p0 + (geom.r - geom.endLineLength1) * geom.v1;
geom.pnt2 = geom.p0 + (geom.r + geom.endLineLength12) * geom.v1;
geom.pnt3 = geom.p0 + (geom.r - geom.endLineLength2) * geom.v2;
geom.pnt4 = geom.p0 + (geom.r + geom.endLineLength22) * geom.v2;
// create the arrowheads
geom.arrowLength = geom.margin * 2;
geom.arrowWidth = geom.margin * 0.5F;
geom.dirStart = SbVec3f(geom.v1[1], -geom.v1[0], 0);
geom.startArrowBase = geom.p0 + geom.r * geom.v1;
geom.dirEnd = SbVec3f(-geom.v2[1], geom.v2[0], 0);
geom.endArrowBase = geom.p0 + geom.r * geom.v2;
return geom;
}
SoDatumLabel::SymmetricGeometry SoDatumLabel::calculateSymmetricGeometry(const SbVec3f* points) const
{
SymmetricGeometry geom;
geom.p1 = points[0];
geom.p2 = points[1];
geom.dir = (geom.p2 - geom.p1);
geom.dir.normalize();
geom.normal = SbVec3f(-geom.dir[1], geom.dir[0], 0);
geom.margin = this->imgHeight / 4.0F;
// calculate coordinates for the first arrow
geom.ar0 = geom.p1 + geom.dir * 4 * geom.margin; // tip of arrow
geom.ar1 = geom.ar0 - geom.dir * 0.866F * 2 * geom.margin;
geom.ar2 = geom.ar1 + geom.normal * geom.margin;
geom.ar1 -= geom.normal * geom.margin;
// calculate coordinates for the second arrow
geom.ar3 = geom.p2 - geom.dir * 4 * geom.margin; // tip of 2nd arrow
geom.ar4 = geom.ar3 + geom.dir * 0.866F * 2 * geom.margin;
geom.ar5 = geom.ar4 + geom.normal * geom.margin;
geom.ar4 -= geom.normal * geom.margin;
return geom;
}
void SoDatumLabel::generateLineSelectionPrimitive(
SoAction* action,
const SbVec3f& start,
const SbVec3f& end,
float width
)
{
// create a thicker line used for selection
SbVec3f dir = end - start;
dir.normalize();
SbVec3f perp = SbVec3f(-dir[1], dir[0], 0) * (width / 2.0f);
SbVec3f p1 = start + perp;
SbVec3f p2 = start - perp;
SbVec3f p3 = end + perp;
SbVec3f p4 = end - perp;
SoPrimitiveVertex pv;
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
this->beginShape(action, TRIANGLE_STRIP);
pv.setPoint(p1);
shapeVertex(&pv);
pv.setPoint(p2);
shapeVertex(&pv);
pv.setPoint(p3);
shapeVertex(&pv);
pv.setPoint(p4);
shapeVertex(&pv);
this->endShape();
}
void SoDatumLabel::generateArcSelectionPrimitive(
SoAction* action,
const SbVec3f& center,
float radius,
float startAngle,
float endAngle,
float width
)
{
// create selectable arc by generating line segments
int countSegments = std::max(6, abs(int(50.0 * (endAngle - startAngle) / (2 * std::numbers::pi))));
double segment = (endAngle - startAngle) / (countSegments - 1);
for (int i = 0; i < countSegments - 1; i++) {
double theta1 = startAngle + segment * i;
double theta2 = startAngle + segment * (i + 1);
SbVec3f v1 = center + SbVec3f(radius * cos(theta1), radius * sin(theta1), 0);
SbVec3f v2 = center + SbVec3f(radius * cos(theta2), radius * sin(theta2), 0);
generateLineSelectionPrimitive(action, v1, v2, width);
}
}
void SoDatumLabel::generateArrowSelectionPrimitive(
SoAction* action,
const SbVec3f& base,
const SbVec3f& dir,
float width,
float length
)
{
// create selectable arrow as a triangle at elevated Z for selection above geometry
SbVec3f tip = base + dir * length;
SbVec3f perp = SbVec3f(-dir[1], dir[0], 0) * (width / 2.0f);
SbVec3f p1 = base + perp;
SbVec3f p2 = base - perp;
SoPrimitiveVertex pv;
pv.setNormal(SbVec3f(0.F, 0.F, 1.F));
this->beginShape(action, TRIANGLES);
pv.setPoint(SbVec3f(tip[0], tip[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(p1[0], p1[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
pv.setPoint(SbVec3f(p2[0], p2[1], ZARROW_TEXT_OFFSET));
shapeVertex(&pv);
this->endShape();
}
SoDatumLabel::ArcLengthGeometry SoDatumLabel::calculateArcLengthGeometry(const SbVec3f* points) const
{
using std::numbers::pi;
ArcLengthGeometry geom;
geom.ctr = points[0];
geom.p1 = points[1];
geom.p2 = points[2];
geom.length = this->param1.getValue();
geom.margin = this->imgHeight / 3.0F;
// angles calculations
SbVec3f vc1 = (geom.p1 - geom.ctr);
SbVec3f vc2 = (geom.p2 - geom.ctr);
geom.startangle = atan2f(vc1[1], vc1[0]);
geom.endangle = atan2f(vc2[1], vc2[0]);
if (geom.endangle < geom.startangle) {
geom.endangle += 2.0F * (float)pi;
}
geom.range = geom.endangle - geom.startangle;
geom.radius = vc1.length();
// text orientation
SbVec3f dir = (geom.p2 - geom.p1);
dir.normalize();
// get magnitude of angle between horizontal
geom.angle = atan2f(dir[1], dir[0]);
if (geom.angle > pi / 2 + pi / 12) {
geom.angle -= (float)pi;
}
else if (geom.angle <= -pi / 2 + pi / 12) {
geom.angle += (float)pi;
}
// text location
geom.textOffset = getLabelTextCenterArcLength(geom.ctr, geom.p1, geom.p2);
// lines direction
geom.vm = (geom.p1 + geom.p2) / 2 - geom.ctr;
geom.vm.normalize();
// determine if this is a large arc (> pi)
geom.isLargeArc = (geom.range > pi);
// lines points
geom.pnt1 = geom.p1;
geom.pnt3 = geom.p2;
if (geom.isLargeArc) {
geom.pnt2 = geom.p1 + geom.length * geom.vm;
geom.pnt4 = geom.p2 + geom.length * geom.vm;
// recalculate angles for the outer arc
SbVec3f vc1_outer = (geom.pnt2 - geom.ctr);
SbVec3f vc2_outer = (geom.pnt4 - geom.ctr);
geom.arcCenter = geom.ctr;
geom.arcRadius = vc1_outer.length();
// update angles for outer arc
geom.startangle = atan2f(vc1_outer[1], vc1_outer[0]);
geom.endangle = atan2f(vc2_outer[1], vc2_outer[0]);
}
else {
geom.pnt2 = geom.p1 + (geom.length - geom.radius) * geom.vm;
geom.pnt4 = geom.p2 + (geom.length - geom.radius) * geom.vm;
// arc center and radius for inner arc
geom.arcCenter = geom.ctr + (geom.length - geom.radius) * geom.vm;
geom.arcRadius = geom.radius;
}
// normals for the arrowheads at arc start and end
geom.dirStart = SbVec3f(sin(geom.startangle), -cos(geom.startangle), 0);
geom.dirEnd = SbVec3f(-sin(geom.endangle), cos(geom.endangle), 0);
return geom;
}